PVNDORABOX
Системная платформа. Исследовательская среда. Киберстратегия.

PANDORA BOX
+ ARENA

Модульная платформа PLATX соединяет низкоуровневые инструменты, доверенное исполнение, управляемые миссии и живую модель киберпротивоборства. От загрузочной цепочки и ядра — до пространственной консоли, синтетических миров и командной игры.

Продуктовая архитектура выпуска 2027. Большой технический атлас PANDORA BOX: устройство, границы полномочий, интерфейсы и сценарии работы.

PANDORA BOX: тёмная фигура над тремя всадниками и открытым кубом, фиолетовые и золотые линии
PLATX / CENTRAL / TEAM SERVER / TESSERACT / ARENAОЖИДАЙТЕ В 2027
64домена в карте платформы
79уникальных корневых CLI-команд
294заголовков в справочнике контрактов
2027горизонт выхода PANDORA BOX + ARENA
01 / SYSTEM ARCHITECTURE

Одна платформа. Несколько плоскостей управления.

PLATX организует возможности вокруг экземпляров, владельцев ресурсов и версионированных контрактов. Модуль получает ровно те связи, которые определены его профилем. Консоль, сценарий, удалённая миссия и игровой интерфейс обращаются к одной модели операций.

PLATX / NODE

Исполнительный узел

Узел связывает конкретную среду, набор модулей, аппаратные механизмы и действующую политику. Локальное решение о доступе остаётся у владельца ресурса. Входящая команда проходит тот же путь проверки, что и локальная операция.

У экземпляра есть идентичность и поколение. Перезапуск изменяет контекст жизни объекта: старая ссылка не превращается в право воздействовать на новый процесс.

CENTRAL / TEAM SERVER

Распределённая работа

Central даёт представление узлов, состояния, миссий и результатов. Team Server связывает командную сессию, операторские роли и общий ход исследования. Доставка запроса и разрешение его выполнить остаются разными событиями.

Связь через RA2C и mesh учитывает идентичность peer, состояние канала, повторы и срок операции. Восстановление не повторяет произвольно действие с уже неизвестным исходом.

TESSERACT / ARENA

Пространственный интерфейс

Tesseract показывает объекты и отношения платформы как рабочее пространство. ARENA добавляет город, команды, ресурсы, темп противоборства и последствия решений. Модель опирается на состояние, которое можно объяснить и воспроизвести.

Игровое представление, видимость для участника и ground truth разведены. После матча можно разобрать причинную цепочку, а не только посмотреть итоговый счёт.

ПлоскостьЗа что отвечаетЧто передаёт соседямКто принимает решение
Core / lifecycleПорядок запуска, остановки, восстановления, владение ресурсамиСостояние экземпляра, generation, опубликованные возможностиСериализованный владелец жизненного цикла
Fabric / providerСвязь потребителя с поставщиком, транзакция и журналВерсия, доступность, результат привязки и операцииПотребитель в пределах собственного контракта
SENSE / ObserveИсточники, факты, качество и потери наблюденияВерсионированные события с происхождениемПолитика, аналитик или отдельный response-контур
Trust / SelfProtectОснования доверия и обеспечение заданной защитыПроверенный статус, качество, область действия, evidenceВерификатор, issuer и resource gate на своих границах
Mission / ARENAЦель, разрешённая работа, бюджеты и оценка исходаПлан, ход исполнения, результаты, debriefController и Judge с разной ответственностью
02 / CONTROLLED BY DESIGN

Контракт важнее обходного пути.

Сила PANDORA BOX складывается из соединяемых возможностей. Чтобы это соединение оставалось управляемым, PLATX делает происхождение полномочия, срок жизни объекта, результат операции и полноту наблюдения частью интерфейса.

Owner + generation

Владелец определяет, кто создаёт объект и завершает его жизнь. Поколение различает последовательные экземпляры с одинаковым именем. Таймер, callback, подписка, задача и capability не переживают своего владельца по неявной договорённости.

Когда модуль уходит, публикация отзывается, новые обращения закрываются, текущие операции завершаются по правилам drain. Только затем освобождается память и прочие ресурсы. Поздний результат получает статус устаревшего обращения.

Capability + version

Возможность — именованный контракт с поставщиком и версией. Потребителю не нужно подключать чужой внутренний заголовок или знать структуру приватного контекста. Несовместимость выясняется на границе привязки.

Требуемая capability, необязательное расширение и качество реализации задаются раздельно. Профиль не превращает отсутствие обязательной функции в успешную работу с незаметно сниженной защитой.

Intent → operation → result

CLI, DSL и MSX описывают намерение, но право на эффект проверяется в операции. В запросе сохраняются цель, контекст, дедлайн и correlation. В результате различаются отказ допуска, ошибка исполнения, неизвестный исход и подтверждённое завершение.

Это позволяет объединять ручную работу и автоматизацию без отдельной привилегированной тропы для сценария. Аудит связывает операцию с её инициатором и действовавшей политикой.

Bounded resources

Очередь, журнал, сессия, модель мира и транзакция имеют пределы. Исчерпание лимита становится состоянием с определённой семантикой. Система учитывает потерю события и прекращает обещать полноту там, где данные отсутствуют.

Повторы и восстановление расходуют явный бюджет. Неидемпотентный эффект нельзя безопасно повторить лишь потому, что транспорт потерял ответ. Для такого случая нужны идентификатор операции и проверка уже полученного результата.

ValidateПроверка формата, версии, размеров, профиля, обязательных механизмов и полномочий.
PrepareРезервирование ресурсов и создание нового состояния без публикации незавершённого результата.
CommitФиксация поколения и доступности; событие и журнал связывают результат с операцией.
Drain / revokeЗакрытие допуска, завершение владельцев, отзыв связей и проверка очистки.
Единый интерфейс не стирает различия механизмов. Пользователь видит, где выполняется защита, кто наблюдал её состояние и насколько сильны основания итогового вывода.
03 / TRUST FROM BOOT TO OPERATION

PLATXBoot. SelfProtect. Trusted Execution.

Доверенное исполнение начинается до пользовательской команды и продолжается после запуска. PANDORA BOX связывает загрузочную цепочку, состояние провайдеров защиты, внешний контроль доказательств и ограничение конкретных действий.

01 / PLATXBOOT

Загрузочная цепочка

PLATXBoot описывает состояние EFI, ESP и порядка загрузки; связывает измерения с TPM PCR и проверкой модулей. Консоль предоставляет отдельные операции для статуса, отчёта, сканирования ESP, контроля загрузочных записей и whitelist модулей ядра.

Установка и удаление загрузочной записи — операции с собственными аргументами и результатом. Измеренное значение, ожидаемое значение и решение о доверии не смешиваются в одно поле «загрузка безопасна».

Синтаксис boot и platxboot →
02 / SELFPROTECT

Защита работающей системы

SelfProtect соединяет политику, защищаемые активы, набор перехватчиков и конкретный provider. В отчёте видны health, locus, quality, generation и доступные capabilities. Пользовательский наблюдатель, BPF, LKM и гипервизор различаются по месту исполнения и полномочиям.

Активация политики проходит validate, prepare, self-test и commit. Потеря обязательного механизма отражается как деградация или нарушение. Временное обслуживание ограничено сроком и не продлевается старым токеном.

Досье SelfProtect →
03 / TRUST

Основания для допуска

Trust проверяет пакеты доказательств, внешние якоря истории, криптографические наборы и условия защиты. Верификатор устанавливает, что подтверждено; издатель полномочий задаёт разрешение; потребитель ресурса проверяет его применимость.

Доверие связано с текущим контекстом: идентичностью, политикой, свежестью, nonce, целью и поколением. Старый положительный ответ не становится бессрочным правом доступа.

Досье Trust →
СигналЧто он сообщаетЧто требуется для вывода
Presence / probeУстройство или механизм доступен для обращенияПроверка применимости к конкретному профилю и операции
Self-testПровайдер выполнил собственную проверкуНезависимое основание там, где self-report недостаточен
Measurement / PCRИзмеренное состояние в заданной схемеОжидаемые значения, корректный event log, свежесть и связь с платформой
Attestation evidenceМатериал для верификатора с происхождением и контекстомПроверка подписи, политики, срока, nonce и области применимости
Grant / leaseОграниченное разрешение субъекту на ресурс и действиеПроверка потребителем при каждом значимом переходе
Forensic chainСвязанная история событий, checkpoints и обнаруживаемые разрывыВнешний якорь и честный учёт недоступных интервалов

В итоге безопасность выражается набором проверяемых свойств. Контроль доступа, целостность истории, защита ключа, изоляция вычисления и качество наблюдения имеют разных владельцев и разные причины отказа. Именно эту разницу показывают CLI и технические досье.

04 / HARDWARE-ANCHORED PLACEMENT

TPM, TEE и seL4 — разные части одной конструкции.

Аппаратный корень доверия, защищённый сервис и микроядерная изоляция решают разные задачи. Архитектура PANDORA BOX соединяет их через профили размещения и явные требования к каждому узлу.

TPM 2.0 / MEASURE & ATTEST

Связь ключа и состояния

TPM участвует в измеряемой загрузке, хранении связанных с устройством ключей и подготовке аттестационного материала. PCR и event log дают основания рассуждать о последовательности измерений. Политика определяет, какие значения допустимы.

TPM не изолирует произвольную память приложения и не наблюдает все его действия. Сервису по-прежнему нужны корректный жизненный цикл, защита от повторного использования ответа и независимая проверка полученного материала.

TEE / PROTECT THE SERVICE

Минимальная защищённая функция

В TEE размещается узкий сервис: работа с корневым материалом, подпись утверждения, проверка критичного перехода или выдача ограниченного разрешения. Большой интерфейс и непроверенные зависимости увеличивают доверенную базу.

Внешняя ОС доставляет запросы и данные. TEE проверяет длины, идентичность, версии, freshness и контекст самостоятельно. Недоступность устройства, rollback и потеря долговечного состояния получают отдельные исходы.

seL4 / ISOLATE AUTHORITY

Разделение компонентов

seL4 задаёт архитектуру изолированных компонентов и capability-based доступа. Состав системы, граф полномочий, IPC, драйверы и размещение памяти определяют, кто может воздействовать на критичный сервис.

Свойства микроядра не переносятся автоматически на все драйверы и прикладной код. Поэтому профиль узла фиксирует свою доверенную базу, аппаратные предпосылки и ограничения устройства ввода-вывода.

HYBRID / KEEP BOUNDARIES EXPLICIT

Композиция узла

Гибридный узел соединяет ОС для широкой совместимости, изолированную функцию для критичных решений и аппаратные основания для проверки состояния. Локальный enforce-контур связан с верификатором и журналом, но не заменяет их.

Такой профиль явно отвечает на четыре вопроса: где живёт секрет, кто наблюдает среду, кто разрешает операцию и кто обеспечивает запрет. Потеря одного звена изменяет заявленное качество всего разрешённого пути.

ПрофильГраница размещенияРоль в PANDORA BOXРешающая проверка
portableПользовательский процессПереносимая консоль, исследование, обработка материаловКонтракты процесса и доступность требуемых функций
os-enforcedМеханизмы ОС и провайдер защитыРабота с системным контролем доступа и наблюдениемФактический locus, активная политика, качество перехвата
tee-serviceИзолированный доверенный сервисКритичные операции и ограниченный интерфейс к ключамAttestation, входной контракт и состояние долговечного материала
sel4-nodeГраф компонентов на seL4Узел с явной архитектурой изоляции и полномочийКонфигурация системы, устройства, IPC и состав доверенной базы
hybridНесколько согласованных границСовместимость ОС и отдельный доверенный контурСвязность attest → policy → grant → resource gate
Выпуск 2027 описывается через целевые профили. Справочник CLI ниже сохраняет реальные статусы и ограничения исходного интерфейса: наличие команды или заголовка само по себе не является сертификатом защищённости всей сборки.
05 / RESEARCH WORKSPACE

Исследование как воспроизводимая система.

PANDORA BOX собирает рабочий цикл: сформулировать гипотезу, подготовить среду, увидеть происходящее, проверить причинность, зафиксировать результат и вернуть среду в определённое состояние.

SENSE / HADES / OBSERVE

Наблюдение с происхождением

Источники ядра и пользовательского пространства сходятся в SENSE. Сенсор сохраняет собственную идентичность, схему и качество. В графе можно отличить непосредственно увиденный факт от связанной гипотезы.

Потери, слепой интервал, несовместимость схемы и отказ источника остаются видимыми. Аналитический вывод учитывает эти ограничения вместе с самой находкой.

DBG / ELF / XIO / XIM

Низкоуровневый рабочий стол

Отладка процессов, анализ ELF, память, асинхронный I/O и посредничество исполнения доступны как подсистемы с собственными границами. Контекст процесса и владение ресурсами связывают отдельные инструменты в сеанс.

Исследователь видит состояние операции, привязанный объект и поколение. Это особенно важно при перезапуске процесса, смене провайдера или завершении изолированной нагрузки.

NDR / PXSIG / FORENSIC

Находка, контекст, проверка

NDR связывает поток, протокол, имена и историю наблюдения. PXSIG добавляет происхождение сигнатурных данных, доверие к поставке и контекст совпадения. Forensic оформляет материалы для независимого разбора.

Положительное совпадение, недоступный контейнер и полностью просмотренный объект имеют разную семантику. Автоматическая реакция требует отдельного решения политики.

PXSANDBOX и ModHost

Sandbox сравнивает требуемую изоляцию с механизмами хоста. Probe, readiness, запуск нагрузки, полнота наблюдения и очистка — отдельные стадии. ModHost связывает артефакт, проверку поставки, профиль и рабочее поколение модуля.

После завершения проверяется остаточное состояние: процессы, дескрипторы, объекты, привязки и ресурсы провайдера. Неизвестная чистота среды не скрывается за успешным кодом завершения полезной нагрузки.

DSL, MSX и автоматизация

DSL выражает желаемую конфигурацию и размещение, MSX связывает операции в сценарий. Автоматизация работает с именованными возможностями, событиями и состояниями. Контракт допуска сохраняется независимо от способа вызова.

Исследовательская миссия задаёт область, бюджет, критерий остановки и результат. Сценарий может собрать данные и подготовить вывод, а доказательства сохраняют происхождение и связь с исходной задачей.

06 / CYBER CONFLICT AS A LIVING SYSTEM

ARENA: город, который отвечает на твои решения.

ARENA — командная киберстратегия в 2.5D-городе. Сети, люди, службы и инфраструктура образуют одну зависимую систему. Действие интересно своим последствием: нарушенная связь меняет работу службы, потеря наблюдения меняет решения команды, восстановление требует ресурсов и времени.

Контролировать машину — мало.
Нужно понимать систему, в которой она живёт.
THE CITY

Инфраструктура как граф последствий

Энергетика, вода, больницы, удостоверяющие службы, центры обработки данных и связь связаны зависимостями. У объекта есть собственное состояние, сервисы, ограничения и последствия недоступности. Город продолжает жить во время конфликта.

Отключение одного элемента не обязано немедленно завершить матч. Оно меняет доступность других систем, нагрузку на резервы и информационную картину. Игрок работает с каскадом эффектов и может влиять на его развитие.

THE TEAMS

Противоборство разных решений

Команда защиты удерживает работоспособность города, наблюдает признаки, распределяет силы и восстанавливает нарушенные связи. Противостоящая команда ищет пути воздействия в пределах сценария. Каждая сторона получает свою видимость и свои ограничения.

Победа связана с целью миссии и условиями города. Доступ к отдельному узлу, незаметность действия и стратегический результат — разные достижения, которые Judge оценивает отдельно.

THE ECONOMY

Пять ограниченных ресурсов

Модель соединяет CBC, персонал, вычисления, разведданные и время. Расход каждого ресурса означает отказ от другой возможности. Быстрая реакция может быть дорогой; глубокое исследование может запоздать; резервирование сокращает эффект отказа, но заранее занимает бюджет.

Решения имеют темп. Длительность и цена операции, задержка наблюдения и время восстановления создают пространство для командной координации.

THE DEBRIEF

Разбор причинной цепочки

После сессии игрок видит связь между намерением, доступным знанием, действием и исходом. Ground truth позволяет восстановить, что произошло в модели, а журнал наблюдения — что сторона могла знать в конкретный момент.

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

Tesseract — пространственная консоль платформы

Tesseract организует узлы, потоки, артефакты и отношения в пространстве. В исследовательском режиме это рабочее представление PLATX; в ARENA — интерфейс города и командной ситуации. Переход от обзора к объекту сохраняет связь с источником состояния и доступными операциями.

Карта не подменяет телеметрию декоративной анимацией. У визуального объекта есть идентичность, состояние, время обновления и происхождение. Неизвестность также отображается: отсутствие видимого события ещё не означает отсутствие воздействия.

BriefingГород, роли, допустимые действия, бюджет и критерии результата.
Observe & decideСбор признаков, распределение сил, гипотеза и командное решение.
Act & recoverИсполнение, каскад последствий, локализация и восстановление.
Debrief & replayПричинная история, ground truth и повтор сценария.
MIRAGE / SYNTHETIC REALITY

Миры с контролируемой правдоподобностью

MIRAGE моделирует файловые, процессные, сетевые и системные взаимодействия. Consistency Oracle следит за согласованностью мира. Seed, watermark, generation и ledger связывают воспроизведение с конкретной конфигурацией.

Видимая участнику история отделена от ground truth. Синтетический эффект остаётся внутри модельной границы; полнота и качество мира описываются явно. Это даёт ARENA управляемую среду для сценариев информационного противоборства.

MIRAGE DUEL / ADVANCED MODES

Борьба за представление

В расширенной линии TESSERACT / MIRAGE DUEL стороны работают не только с сервисами, но и с интерпретацией наблюдаемого мира: проекцией, сокрытием, стимуляцией реакции, распознаванием модели и привязкой наблюдений к устойчивому якорю.

Такие режимы развивают базовую командную стратегию. ARENA II и расширенный человеческий слой образуют отдельный горизонт развития; их не следует смешивать с составом первоначального выпуска 2027.

PANDORA BOX — самостоятельная системная платформа. ARENA использует её возможности для игры и обучения; Tesseract связывает оператора с моделью. Эти три роли сохраняются и за пределами игрового матча.
07 / COMPLETE DOMAIN ATLAS

Каждая подсистема — отдельное досье.

Карта охватывает все 64 текущих каталога src/<domain>/. В каждом досье: назначение, архитектурная граница, поток работы, обработка отказов, состав файлов, контракты и связь с CLI. PLATXBoot и HADES показаны внутри доменов, которым принадлежат их реализации.

Досье: 64 / 64. Поиск учитывает описание, интерфейсы и состав домена.

01

attach

Транзакционное присоединение возможностей и ресурсов
src/attach/Исполнение и расширения26 файлов10 API headers

Attachment превращает намерение связать provider с execution context в атомарный, наблюдаемый и обратимый объект. Он не реализует provider и не владеет context; его задача — проверить совместимость, зафиксировать связь, управлять generation и гарантировать cleanup при любом промежуточном отказе.

Граница ответственности

  • Вход: typed context handle, provider capability/descriptor, attachment policy и correlation id. Выход: attachment handle со state, owner и health.
  • Attachment не вызывает произвольные private callbacks. Provider предоставляет узкий prepare/commit/rollback/detach contract либо operation capability.
  • Multi-attach, resume и child-wrapper являются режимами одной transaction model; отдельные таблицы связей запрещены.

Устройство подсистемы

  • Registry хранит attachment identity, context/provider generations, state PREPARING/ACTIVE/DEGRADED/DETACHING/DEAD и acquisition scope.
  • Prepare выполняет read-only validation: context жив, provider version подходит, duplicate policy допускает операцию, required lease есть.
  • Context/provider loss приходит событием. State machine revoke-ит активную связь; automatic resume разрешён только policy и всегда проверяет новую generation.

Поток работы

  • CLI/DSL request → authz → context resolve.
  • Provider selection → prepare plan → scoped acquisitions.
  • Commit → ACTIVE event/health row.
  • Detach/loss → revoke → provider detach → resource release → DEAD.

Отказ и восстановление

  • Duplicate attach не создаёт вторую связь: возвращает existing/idempotent либо explicit DUPLICATE.
  • Provider умер между prepare и commit: generation mismatch отменяет transaction.
  • Rollback callback упал: Core всё равно освобождает owned resources и фиксирует rollback_incomplete audit.
  • Context loss во время detach coalesced в одну terminal operation.

Основные возможности

  • Prepare/commit/rollback transaction with duplicate-attachment rejection.
  • Generation-aware stale-handle rejection and child wrapper.
  • Health, explain, resume and multi-attachment policy paths emit Core events.

Управление и диагностика

Корневые команды: attachment. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / attach →
Состав подсистемы / 26 файлов
Файл / компонентНазначение и граница
src/attach/attach_child.cATT5: attach.child wraps plat_child_host. The reference attachment is a live CHILD. This provider spawns nothing itself: it drives plat_child_host_spawn/ready/shutdown. A duplicate launcher stack would give two READY and a half-state. FQ-06: wrap the existing host, do not reimplement it.
src/attach/attach_commit.cATT2: ACTIVE only after proof. commit(id): PREPARING → ATTACHING → VERIFYING → ACTIVE. Proof = provider.verify returning 0 (mock: proof_set must be 1). Without proof → NOT ACTIVE. Without prior prepare → NOT ACTIVE. generation taken from the monotonic source (attach_gen.c) on success:
src/attach/attach_desc.cATT0: descriptor string helpers. State and scope to string. zeroed desc → DETACHED. Unknown value → "UNKNOWN", not "ACTIVE". O15-1. GREEN: make test-attach-desc
src/attach/attach_dup.cO16-6: same target twice ACTIVE is banned. PID reuse is the trap: two slots on one pid, one detached, and the survivor is now ACTIVE on somebody else's process. The comparison is target_same over pid+birth+exe+ns_inum — a reused PID (same number, later birth) is a
src/attach/attach_events.cATT: rare facts, not heartbeat. Emit only on rare transitions (PREPARE / ACTIVE / DEGRADED / core 1..8 range is refused: publishing there would collide with module-lifecycle events and break Goal A event.on. Health ticks never reach this file — 64 heartbeats would overrun the ring.
src/attach/attach_explain.cATT6: explain candidates + reason. Lists the v1 providers (attach.mock, attach.child) with usability, plus the state/generation/reason of one slot when asked. Never executes an attach. Empty catalog → 0 candidates + reason, not a silent OK. Unknown id → reason, returns -1 — never an ACTIVE/OK lie.
src/attach/attach_gen.cgeneration++ and stale result rejection. DoD ATT5: target death → gen++ (state leaves ACTIVE) → a result carrying the old generation is rejected → reattach carries a strictly higher generation. PID reuse and late replies from a dead attachment must never
src/attach/attach_health.cATT6: health is a metric, not an event. plat_attach_health(id, out): OK / DEGRADED / LOST + reason. DETACHED → LOST, never OK. A dead target pid makes ACTIVE report LOST — ACTIVE must not lie. This file never publishes an event: a heartbeat that emitted on every tick would drown the 64-slot ring.
src/attach/attach_int.hinternal slot API (src/attach/ sources only). Do not include from outside src/attach/. Slot table lives in attach_prepare.c; other TUs link against it.
src/attach/attach_mock.cATT1: attach.mock provider. Scope TEST. Nine slots; all succeed by default; no fd/pid/network. mock_set_proof(id, on): controls whether verify() accepts. Duplicate register → EBUSY, not silent replacement. O15-4. GREEN: make test-attach-mock
src/attach/attach_multi.cO16-4: degrading one slot spares the rest. With two attachments live, a health LOST on A must degrade A alone: B keeps its state and its generation. Zeroing the table or dropping B "as well" is a half-state where a live CHILD dies for a neighbour's fault.
src/attach/attach_owner.cATT4: detach releases ownership. plat_attach_detach(id): ACTIVE/DEGRADED → DRAINING → DETACHED. Calls provider.drain, then provider.detach. Releases all xio-owned fds for this slot's owner. If a Core ABI has been registered via plat_attach_set_abi(), also sweeps the resource table (abi->res->release_owner).
src/attach/attach_plan.cO16-2: dry-run plan without external effect. Calling prepare "just to look" leaves a slot in PREPARING. A plan must leave the table exactly as it found it: it allocates nothing, moves no state, opens no fd, spawns nothing. It lists the steps a real attach would
src/attach/attach_policy.cO16-1: gate before prepare, not ACL in gadget. An empty gate that returns allow is a half-state: a TEST mock aimed at a closed — unknown provider, scope mismatch, empty target, or a TEST provider pointed at a spawnable exe all deny. It never prepares or commits; it is a
src/attach/attach_prepare.cATT2: two-phase prepare. Phase 1 of 2. No external effect: finds provider, allocates slot, sets state PREPARING. Does NOT call attach/verify/commit. Does NOT set generation. Does NOT reach ACTIVE. Slot table lives here; attach_int.h declares the internal API.
src/attach/attach_provider.cATT1: provider registry. Static table, not plat_capreg. Capacity is small (PLAT_ATTACH_PROVIDER_MAX=4). Empty id → refused. Duplicate id → refused. find(unknown) → NULL. Empty catalog + commit → refused, not silent OK. O15-3. GREEN: make test-attach-provider
src/attach/attach_reason.cO16-8: deny codes, unknown reason ≠ ALLOW. A refusing gate returns a code, not a bare -1, so the CLII does not print "ok" or "ACTIVE" over a denial. The zero code (NONE) is the default of a zeroed field and must never read as permission; garbage must not either.
src/attach/attach_replace.cO16-7: replace ACTIVE without drain is banned. A second prepare/commit on a live id would overwrite ACTIVE and let an inflight result carry the old generation into the new attachment — a half-state. Replacement is legal only once the old attachment has left
src/attach/attach_resume.cO16-3: resume protocol, not reattach. O15-25 marks a session DISCONNECTED. Resume re-establishes the channel without a second provider.attach: re-attaching would spawn and would burn the generation, turning a survivable disconnect into a target loss. The
src/attach/attach_rollback.cATT2: откат идемпотентен и не лжёт об исходе. Любой отказ attach/verify/commit → provider.rollback → DETACHED. Повторный вызов на уже откаченной привязке → 0, не паника. После отката слот можно готовить заново. Поколение никогда не публикуется как живое после отката.
src/attach/attach_scope.cO16-5: scope is bound to the provider. attach.mock is a TEST-scope provider and attach.child is CHILD-scope; that is fixed by what each one is, not by what a caller asks for. A mock claimed as CHILD would be "almost an attachment" with no host path; a child claimed
src/attach/attach_state.cATT0: legal state transitions only. plat_attach_transit: returns 0 on a legal arc, -1 otherwise. Illegal arc does NOT modify *state. No I/O, no spawn, no events. DETACHED → PREPARING PREPARING → ATTACHING ATTACHING → VERIFYING ACTIVE → DEGRADED DEGRADED → VERIFYING (must re-verify before returning ACTIVE)
src/attach/attach_target.cATT3: target identity != PID. PID reuse is real. birth + exe + ns_inum bind the identity. pid <= 0 → error. No /proc → error. "pid alone is enough" → refused. O15-9. GREEN: make test-attach-target
src/attach/attach_txn.cA2-P08: журнал отката, срок подготовки, владелец. Здесь НЕТ второго движка отката. Откат по-прежнему исполняют attach_commit.c / attach_rollback.c / attach_verify.c через provider.rollback. Этот файл только записывает намерение до первого разрушающего шага и исход после последнего — и переживает освобождение
src/attach/attach_verify.cATT2: VERIFYING is a separate step. plat_attach_verify(id): ATTACHING → VERIFYING only. Does NOT set ACTIVE (that is commit's job). Does NOT call provider.attach again. O15-8. GREEN: make test-attach-verify
src/attach/cmd_attach.clifecycle + inspection CLI for capability attachments. Selection and health live in the attach subsystem; this file only drives plat_attach_* and renders their results. A second chooser in the CLI would eventually disagree with plat_attach_explain — so there is none here.
Контракты API / 10 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/attach.h
/* platx/attach.h — how a capability runtime binds to a target.
 *
 * ATTACHED is not a bool. The state machine is the proof. A zeroed
 * descriptor means DETACHED, not ACTIVE. Generation 0 is always refused
 * on commit. Only "attach.mock" and "attach.child" are v1 providers.
 *
 * Owned by O15-1 (ATT0). Provider bodies live in their own .c files.
 * Do not add perl/web/browser providers here.
 *
 * GREEN: make test-attach-desc
 */

/* ── Sizing ──────────────────────────────────────────────────────────── */

#define PLAT_ATTACH_ID_MAX       64
#define PLAT_ATTACH_EXE_MAX     256
#define PLAT_ATTACH_SLOTS_MAX     8

/* ── Provider identifiers (v1 only) ──────────────────────────────────── */

#define PLAT_ATTACH_PROVIDER_MOCK   "attach.mock"
#define PLAT_ATTACH_PROVIDER_CHILD  "attach.child"

/* ── State machine ───────────────────────────────────────────────────── */

/*
 * DETACHED = 0  so that a zeroed descriptor is DETACHED, not ACTIVE.
 *
 * Legal arcs (→ = allowed by plat_attach_transit):
 *   DETACHED  → PREPARING
 *   PREPARING → ATTACHING
 *   ATTACHING → VERIFYING
 *   VERIFYING → ACTIVE
 *   ACTIVE    → DEGRADED
 *   DEGRADED  → VERIFYING   (re-verify before returning ACTIVE)
 *   ACTIVE    → DRAINING
 *   DRAINING  → DETACHED
 *   any non-DETACHED → FAILED
 *   FAILED    → ROLLBACK
 *   ROLLBACK  → DETACHED
 */
typedef enum plat_attach_state {
    PLAT_ATTACH_DETACHED  = 0,  /* MUST be 0: zeroed desc is DETACHED */
    PLAT_ATTACH_PREPARING = 1,
    PLAT_ATTACH_ATTACHING = 2,
    PLAT_ATTACH_VERIFYING = 3,
    PLAT_ATTACH_ACTIVE    = 4,
    PLAT_ATTACH_DEGRADED  = 5,
    PLAT_ATTACH_DRAINING  = 6,
    PLAT_ATTACH_FAILED    = 7,
    PLAT_ATTACH_ROLLBACK  = 8
} plat_attach_state_t;

/* ── Scope ───────────────────────────────────────────────────────────── */

typedef enum plat_attach_scope {
    PLAT_ATTACH_SCOPE_TEST    = 0, /* attach.mock */
    PLAT_ATTACH_SCOPE_CHILD   = 1, /* attach.child */
    PLAT_ATTACH_SCOPE_RUNTIME = 2, /* attach.runtime.perl (future) */
    PLAT_ATTACH_SCOPE_SERVICE = 3, /* attach.web.module (future) */
    PLAT_ATTACH_SCOPE_BROWSER = 4, /* attach.browser (future) */
    PLAT_ATTACH_SCOPE_LAB     = 5  /* attach.process.lab (future) */
} plat_attach_scope_t;

/* ── Target identity: PID alone is not enough ────────────────────────── */

/*
 * PID reuse is real. birth + exe + ns_inum distinguish the live process
 * from a recycled PID. plat_attach_target_same() returns 0 if any of
 * birth, exe, or ns_inum differs, even when pid matches.
 */
typedef struct plat_attach_target {
    pid_t   pid;
    uint64_t birth;           /* /proc/pid/stat field 22, clock ticks since boot */
    char     exe[PLAT_ATTACH_EXE_MAX]; /* /proc/pid/exe resolved path */
    uint64_t pid_ns_inum;     /* inode of /proc/pid/ns/pid */
} plat_attach_target_t;

/* ── Descriptor ──────────────────────────────────────────────────────── */

typedef uint32_t plat_attach_id_t;  /* slot handle; 0 = invalid */

typedef struct plat_attach_desc {
    plat_attach_id_t     id;         /* 0 = not allocated */
    char                 provider_id[PLAT_ATTACH_ID_MAX];
    plat_attach_scope_t  scope;
    plat_attach_state_t  state;      /* zeroed → DETACHED */
    uint32_t             generation; /* 0 until successful commit; commit refuse if 0 */
    plat_attach_target_t target;
    plat_owner_t         owner;      /* generation==0 → no owner yet */
    int                  proof_set;  /* commit requires proof_set != 0 */
} plat_attach_desc_t;

/* ── Provider ABI ────────────────────────────────────────────────────── */

/*
 * All nine slots. Bodies live in their respective .c (O15-2 … O15-16).
 * Return 0 for success, -1 for error.
 *
 * None of these may set desc->state = ACTIVE directly. State transitions
 * go through plat_attach_transit().
 */
typedef struct plat_attach_provider {
    char                id[PLAT_ATTACH_ID_MAX];
    plat_attach_scope_t scope;

    int (*probe)    (void);
    int (*prepare)  (plat_attach_desc_t *desc);
    int (*attach)   (plat_attach_desc_t *desc);
    int (*verify)   (plat_attach_desc_t *desc);
    int (*health)   (const plat_attach_desc_t *desc);
    int (*drain)    (plat_attach_desc_t *desc);
    int (*detach)   (plat_attach_desc_t *desc);
    int (*rollback) (plat_attach_desc_t *desc);
    int (*describe) (const plat_attach_desc_t *desc, char *buf, size_t n);
} plat_attach_provider_t;

/* ── String helpers ──────────────────────────────────────────────────── */

/* Unknown/out-of-range value → "UNKNOWN", not "ACTIVE". */
const char *plat_attach_state_str(plat_attach_state_t s);
const char *plat_attach_scope_str(plat_attach_scope_t sc);

/* Describe a zeroed (DETACHED) descriptor without crashing. */
int plat_attach_desc_describe(const plat_attach_desc_t *desc,
                              char *buf, size_t n);

/* ── State machine (O15-2: attach_state.c) ───────────────────────────── */

/*
 * Attempt state transition from → to.
 * Returns 0 on a legal arc; returns -1 and leaves *from unchanged on
 * an illegal arc (e.g. DETACHED→ACTIVE, FAILED→ACTIVE).
 */
int plat_attach_transit(plat_attach_state_t *state,
                        plat_attach_state_t to);

/* ACTIVE or DEGRADED (but not VERIFYING, not PREPARING). */
int plat_attach_is_active(plat_attach_state_t s);

/* ── Provider registry (O15-3: attach_provider.c) ────────────────────── */

/* Capacity is small (PLAT_ATTACH_PROVIDER_MAX). Not plat_capreg. */
#define PLAT_ATTACH_PROVIDER_MAX 4

int plat_attach_register(const plat_attach_provider_t *p);
const plat_attach_provider_t *plat_attach_find(const char *id);

/* ── Mock provider (O15-4: attach_mock.c) ────────────────────────────── */

/* Register "attach.mock" (scope TEST). Returns EBUSY on duplicate. */
int plat_attach_mock_register(void);

/*
 * Next call to the named slot fails; state is NOT set to ACTIVE.
 * Names: "prepare" "attach" "verify" "commit"
 */
void plat_attach_mock_fail_on(const char *slot);

/* Set/clear the proof flag so commit can succeed. */
void plat_attach_mock_set_proof(plat_attach_id_t id, int on);

/* ── Two-phase lifecycle (O15-5..O15-7: attach_prepare/commit/rollback) */

/*
 * plat_attach_prepare: no external effect. Sets state PREPARING.
 * Fails if provider_id unknown, or slot already active.
 */
int plat_attach_prepare(const char *provider_id,
                        const plat_attach_target_t *target,
                        plat_attach_id_t *out_id);

/*
 * plat_attach_commit: attach → verify → ACTIVE if proof present.
 * Fails if not preceded by prepare, or proof missing.
 * On failure: triggers rollback, state → DETACHED.
 */
int plat_attach_commit(plat_attach_id_t id);

/*
 * plat_attach_rollback: provider.rollback → state DETACHED.
 * Idempotent: called on already-DETACHED slot returns 0.
 */
int plat_attach_rollback(plat_attach_id_t id);

/* ── Verify step (O15-8: attach_verify.c) ────────────────────────────── */

/*
 * plat_attach_verify: from ATTACHING only → VERIFYING.
 * Does NOT set ACTIVE (that is commit's job).
 * Fails if proof missing, empty manifest, or generation == 0.
 */
int plat_attach_verify(plat_attach_id_t id);

/* ── Target identity (O15-9: attach_target.c) ───────────────────────── */

/*
 * Fill target from /proc. pid <= 0 → error; no /proc → error.
 * Does not accept "pid alone is enough" — birth/exe/ns are required.
 */
int plat_attach_target_fill(pid_t pid, plat_attach_target_t *out);

/*
 * Returns 1 only if pid + birth + exe + ns_inum all match.
 * pid match alone is not sufficient.
 */
int plat_attach_target_same(const plat_attach_target_t *a,
                            const plat_attach_target_t *b);

/* ── Owner cleanup (O15-10: attach_owner.c) ─────────────────────────── */

/*
 * plat_attach_detach: drain + detach provider, close owned fds,
 * then plat_res release_owner + plat_event_unsub_owner.
 * After return: xio_owner_leaks(owner) == 0, state DETACHED.
 * Generation-0 owner → -1.
 */
int plat_attach_detach(plat_attach_id_t id);

/* Look up a live descriptor by slot id. NULL if not found. */
plat_attach_desc_t *plat_attach_lookup(plat_attach_id_t id);

/* ── Replace guard (O16-7: attach_replace.c) ─────────────────────────── */

/*
 * May replacing (re-preparing) this id begin now? A pure predicate — it
 * never drains, detaches, or mutates the slot. Returns 0 only when the old
 * attachment has left the live states (DRAINING or DETACHED, or the slot
 * is already gone); returns -1 for ACTIVE / VERIFYING / any mid-transaction
 * state, leaving the slot unchanged. Replacing a live ACTIVE without drain
 * would overwrite it and let a stale generation be accepted as current.
 */
int plat_attach_replace_begin(plat_attach_id_t id);

/* ── attach.child provider (O15-11: attach_child.c) ──────────────────── */

/*
 * Register the "attach.child" provider (scope CHILD). It wraps
 * plat_child_host_spawn/ready/shutdown — it never fork()s itself.
 * verify() returns 0 only when plat_child_host_ready() is true: spawn
 * rc==0 is not proof. Duplicate registration → EBUSY.
 */
int plat_attach_child_register(void);

/*
 * Prepare a child attachment from a launch spec in one call: it binds the
 * spec, then runs plat_attach_prepare(). The slot ends in PREPARING; no
 * fork happens until commit. launch is void* so this header need not
 * include child_host.h; it must be a plat_child_launch_t with
 * isolate == PLAT_ISO_CHILD and a non-empty exe. The caller owns the spec
 * and it must outlive commit. Returns -1 on a bad spec or a full table.
 */
int plat_attach_child_prepare(const void *launch, plat_attach_id_t *out_id);

/*
 * Bind a launch spec to an already-prepared slot (alternative to the
 * wrapper above when the generic prepare path is used). isolate must be
 * PLAT_ISO_CHILD. Unknown slot or non-CHILD isolate → -1.
 */
int plat_attach_child_bind(plat_attach_id_t id, const void *launch);

/* ── Health: metric, not an event (O15-12: attach_health.c) ──────────── */

typedef enum plat_attach_health {
    PLAT_ATTACH_HEALTH_OK       = 0,
    PLAT_ATTACH_HEALTH_DEGRADED = 1,
    PLAT_ATTACH_HEALTH_LOST     = 2
} plat_attach_health_t;

typedef struct plat_attach_health_report {
    plat_attach_health_t code;
    char                 reason[96];
} plat_attach_health_report_t;

/*
 * Report health of a slot. Derived from state + target liveness.
 *   DETACHED / FAILED / ROLLBACK      → LOST (never OK)
 *   ACTIVE with a dead target pid     → LOST (ACTIVE must not lie)
 *   ACTIVE                            → OK
 *   DEGRADED                          → DEGRADED
 *   transitional (PREPARING/…/VERIFYING) → DEGRADED
 * Never calls plat_event_emit — heartbeat is a metric, not a fact.
 * Unknown slot → LOST + reason, returns -1.
 */
int plat_attach_health(plat_attach_id_t id, plat_attach_health_report_t *out);
const char *plat_attach_health_str(plat_attach_health_t h);

/* ── Explain: candidates + reason, no side effect (O15-13) ───────────── */

typedef struct plat_attach_explain_row {
    char                provider_id[PLAT_ATTACH_ID_MAX];
    plat_attach_scope_t scope;
    int                 usable;     /* probe() ok (or no probe) */
} plat_attach_explain_row_t;

typedef struct plat_attach_explain {
    plat_attach_id_t          id;          /* 0 = catalog-only view */
    plat_attach_state_t       state;       /* slot state if id != 0 */
    uint32_t                  generation;
    unsigned                  n_providers;
    plat_attach_explain_row_t providers[PLAT_ATTACH_PROVIDER_MAX];
    char                      reason[128];
} plat_attach_explain_t;

/*
 * Fill the explain view. id==0 → catalog only. Unknown id → reason set,
 * returns -1 (never a silent ACTIVE/OK). Empty catalog → n_providers==0
 * plus a reason, returns -1. Never executes attach/commit.
 */
int plat_attach_explain(plat_attach_id_t id, plat_attach_explain_t *out);
int plat_attach_explain_render(const plat_attach_explain_t *e,
                               char *buf, size_t n);

/* ── Rare facts, not heartbeat (O15-15: attach_events.c) ─────────────── */

/* Attach lifecycle facts. 0x1400 block, like Hook 0x1200 — never 1..8. */
#define PLAT_EV_ATTACH_PREPARE      0x1400u
#define PLAT_EV_ATTACH_ACTIVE       0x1401u
#define PLAT_EV_ATTACH_DEGRADED     0x1402u
#define PLAT_EV_ATTACH_TARGET_LOST  0x1403u
#define PLAT_EV_ATTACH_FAILED       0x1404u
#define PLAT_EV_ATTACH_DETACH       0x1405u

/* Payload of an attach fact: ids and numbers, no pointers, no command. */
typedef struct plat_attach_fact {
    uint32_t            generation;
    char                provider_id[PLAT_ATTACH_ID_MAX];
    plat_attach_state_t state;
} plat_attach_fact_t;

/*
 * Emit one attach fact. Refuses any id outside the 0x1400 block (so a
 * core event 1..8 can never be published from here). Emit belongs on rare
 * transitions only — never call it from a health tick.
 */
int plat_attach_emit_fact(plat_event_id_t id, const plat_attach_desc_t *desc);

/* ── Generation / stale result (O15-16: attach_gen.c) ────────────────── */

/*
 * A strictly increasing, never-zero generation source. Each call returns a
 * value greater than every previous one — reattach after a lost target is
 * always a higher generation than the attachment it replaces.
 */
uint32_t plat_attach_gen_next(void);

/*
 * The target died: bump the slot generation past any prior value and move
 * the slot out of ACTIVE (→ FAILED). The old generation is now stale.
 * Unknown or already-DETACHED slot → -1.
 */
int plat_attach_note_target_lost(plat_attach_id_t id);

/*
 * Accept a result carrying result_gen only if it equals the slot's current
 * generation and is non-zero. A stale generation (from before a target
 * loss) or generation 0 is refused.
 */
int plat_attach_accept_result(plat_attach_id_t id, uint32_t result_gen);
include/platx/attach_dup.h
/* platx/attach_dup.h — O16-6: one live target, one attachment.
 *
 * Two ACTIVE slots on one live target is a half-state: detaching one leaves
 * the other ACTIVE on a PID that, after reuse, is a different process. The
 * key is target_same (pid+birth+exe+ns_inum), never pid alone — a reused PID
 * (same pid, different birth) is a different target, not a duplicate. This
 * predicate reads the slot table; it allocates nothing. attach.h and
 * attach_target.c are not touched.
 */

/*
 * Return the id of a live (ACTIVE or DEGRADED) attachment whose target is the
 * same as `target` by full identity, or 0 if none. An empty (all-zero)
 * target is never a key and always returns 0.
 */
plat_attach_id_t plat_attach_find_active_target(
    const plat_attach_target_t *target);

/*
 * 0 if attaching this target is allowed (no live attachment on it); -1 if a
 * live attachment already holds the same target. A reused PID (same pid,
 * different birth/exe/ns) is not a duplicate.
 */
int plat_attach_deny_dup(const plat_attach_target_t *target);
include/platx/attach_multi.h
/* platx/attach_multi.h — O16-4: DEGRADED is local to one slot.
 *
 * Two attachments live in the table at once. A health LOST on A must not
 * take B down or zero the table — a live CHILD dying "as well" is a
 * half-state. Degrading one slot touches only that slot; counting reports
 * ACTIVE and DEGRADED separately. attach.h is not touched.
 */

/*
 * Move exactly one slot ACTIVE→DEGRADED. Neighbours are never touched. An
 * already-DEGRADED slot is a no-op success. Missing slot, or a slot in a
 * non-ACTIVE/DEGRADED state, → -1. Never evicts, never detaches.
 */
int plat_attach_note_degraded(plat_attach_id_t id);

/* Count slots currently ACTIVE (not DEGRADED). */
int plat_attach_count_active(void);

/* Count slots currently DEGRADED (not ACTIVE). */
int plat_attach_count_degraded(void);
include/platx/attach_plan.h
/* platx/attach_plan.h — O16-2: dry-run attach plan, no external effect.
 *
 * An operator wants to see what an attach would do without doing it. A plan
 * lists the steps and the policy verdict and touches nothing: no slot moves,
 * no fd or pid appears. "plan" is never "attached". attach.h is not touched.
 */

#define PLAT_ATTACH_PLAN_STEPS_MAX  256
#define PLAT_ATTACH_PLAN_REASON_MAX 128

typedef struct plat_attach_plan_result {
    int  allowed;                              /* 1 = plan would proceed */
    char reason[PLAT_ATTACH_PLAN_REASON_MAX];  /* why allowed / why not */
    char steps[PLAT_ATTACH_PLAN_STEPS_MAX];    /* probe/prepare/attach/verify */
} plat_attach_plan_result_t;

/*
 * Fill a dry-run plan for attaching provider_id to target. Never allocates a
 * slot, never calls provider.attach, never spawns. Unknown provider → reason
 * set, allowed=0, returns -1. Policy deny → reason set, allowed=0, returns -1.
 * A viable plan → allowed=1, steps listed, returns 0. Either way the slot
 * table is exactly as it was before the call.
 */
int plat_attach_plan(const char *provider_id,
                     const plat_attach_target_t *target,
                     plat_attach_plan_result_t *out);
include/platx/attach_policy.h
/* platx/attach_policy.h — O16-1: the gate that runs before prepare.
 *
 * Not an ACL inside a gadget, not a second registry. A pure predicate:
 * given a provider id, the scope the caller claims, and a target, it says
 * whether an attachment may even be prepared. An empty gate is allow-all —
 * a half-state — so the default answer is DENY, and every allow is earned.
 *
 * attach.h is not touched by O16-1; these symbols live here.
 */

typedef enum plat_attach_policy_result {
    PLAT_ATTACH_POLICY_ALLOW           = 0,
    PLAT_ATTACH_POLICY_DENY_NO_PROVIDER = 1, /* empty/unknown provider id */
    PLAT_ATTACH_POLICY_DENY_SCOPE       = 2, /* scope ≠ the catalog's scope */
    PLAT_ATTACH_POLICY_DENY_EMPTY_TARGET = 3, /* target NULL or all-zero */
    PLAT_ATTACH_POLICY_DENY_TEST_ON_CHILD = 4 /* TEST provider aimed at a real exe */
} plat_attach_policy_result_t;

/*
 * The classified decision. ALLOW only when: the provider is registered, the
 * claimed scope equals the provider's catalog scope, the target is non-empty,
 * and — for a TEST-scope provider — the target is not child-shaped (a real
 * exe path). Anything else is a specific DENY, never a silent allow.
 */
plat_attach_policy_result_t plat_attach_policy_check(
    const char *provider_id,
    plat_attach_scope_t scope,
    const plat_attach_target_t *target);

/* 0 only for ALLOW; -1 for every deny. */
int plat_attach_policy_allow(const char *provider_id,
                             plat_attach_scope_t scope,
                             const plat_attach_target_t *target);

/* Name of a result. Out-of-range → "UNKNOWN", never "ALLOW". */
const char *plat_attach_policy_str(plat_attach_policy_result_t r);
include/platx/attach_reason.h
/* platx/attach_reason.h — O16-8: attach deny codes.
 *
 * A gate that refuses must say why with a code, and an unknown or zero code
 * must never read as permission. This is a separate header on purpose:
 * attach.h is not touched by O16-8.
 *
 * Rule: absence of a deny reason (NONE / 0) is NOT allow. Allow is an
 * explicit, distinct value — see plat_attach_deny_is_allow().
 */

/*
 * Deny reasons. NONE = 0 means "no reason recorded", which is the default of
 * a zeroed field — and precisely because it is the zero default it must not
 * be mistaken for permission. A gate that means to allow returns
 * PLAT_ATTACH_ALLOW, never deny code 0.
 */
typedef enum plat_attach_deny {
    PLAT_ATTACH_DENY_NONE        = 0,  /* no reason set — NOT allow */
    PLAT_ATTACH_DENY_NO_PROVIDER = 1,  /* provider id unknown / catalog empty */
    PLAT_ATTACH_DENY_SCOPE       = 2,  /* provider scope forbids this target */
    PLAT_ATTACH_DENY_POLICY      = 3,  /* policy refused (risk / quality) */
    PLAT_ATTACH_DENY_DUP_TARGET  = 4,  /* target already has a live attachment */
    PLAT_ATTACH_DENY_NEED_DRAIN  = 5,  /* live ACTIVE must drain before replace */
    PLAT_ATTACH_DENY_STALE_GEN   = 6,  /* result carried a stale generation */
    PLAT_ATTACH_DENY_NO_PROOF    = 7,  /* verify/proof missing — not ACTIVE */
    PLAT_ATTACH_DENY_UNKNOWN     = 8   /* named catch-all */
} plat_attach_deny_t;

/*
 * Explicit allow sentinel. Deliberately outside the deny enum range so a
 * zero or garbage integer can never equal it. A gate returns this only when
 * it truly means allow.
 */
#define PLAT_ATTACH_ALLOW  0x7A11  /* ~"ALLOW"; never 0, never a deny code */

/* Name of a deny code. Out-of-range / 99 → "UNKNOWN" — never "ok"/"ACTIVE". */
const char *plat_attach_deny_str(plat_attach_deny_t code);

/* S527/S517: последняя причина отказа исполнения, пережившая слот.
 *
 * plat_attach_explain() отвечает по номеру привязки, а неудачный commit
 * слот освобождает — и объяснять становится нечего. Эта строка живёт
 * отдельно: этап + системная причина последней неудачи. Пустая строка
 * означает «неудач не было или причина не записана», а НЕ «всё хорошо».
 *
 * Отличать от plat_attach_deny_t: там РЕШЕНИЕ гейта (политика, область,
 * дубликат), здесь — поломка исполнения (не хватило дескрипторов, не
 * прочиталась личность цели). Разные ответы, разные каналы. */
void        plat_attach_fail_note(const char *stage, const char *why);
void        plat_attach_fail_clear(void);
const char *plat_attach_last_error(void);

/*
 * True only for the explicit PLAT_ATTACH_ALLOW value. Zero (NONE) and any
 * garbage integer → 0. Takes int so a raw/uninitialised value cannot be
 * laundered into allow by the enum type.
 */
int plat_attach_deny_is_allow(int code);
include/platx/attach_resume.h
/* platx/attach_resume.h — O16-3: resume protocol, not reattach.
 *
 * A session disconnect is not a target death. An ACTIVE (or DEGRADED)
 * attachment whose session dropped is RESUMED, never re-attached: calling
 * provider.attach again would spawn and would burn the generation. Resume is
 * a token bound to (attachment id, generation, target fingerprint). Applying
 * it proves "same attachment, same target, same epoch" without touching the
 * provider. attach.h and attach_session.c are not touched.
 */

typedef struct plat_attach_resume_token {
    plat_attach_id_t     attachment_id;
    uint32_t             generation;
    plat_attach_target_t target;   /* fingerprint: pid+birth+exe+ns_inum */
    uint64_t             token;    /* one-time, never 0 */
} plat_attach_resume_token_t;

/*
 * Issue a resume token for a live attachment. Only ACTIVE or DEGRADED (a
 * disconnected session is still one of those). Never calls attach/spawn,
 * never bumps the generation. Missing slot or a non-live state → -1.
 */
int plat_attach_resume_issue(plat_attach_id_t id,
                             plat_attach_resume_token_t *out);

/*
 * Apply a resume token. Succeeds (0) only when the slot exists, the token is
 * the one that was issued (one-time, non-zero), the generation still matches
 * (no stale epoch), and the target fingerprint still matches (no PID reuse).
 * Never calls provider.attach and never prepares. On any failure it returns
 * -1 and leaves the slot state untouched — a failed resume is not a detach.
 */
int plat_attach_resume_apply(plat_attach_id_t id,
                             const plat_attach_resume_token_t *token);

/* Forget issued tokens (tests). */
void plat_attach_resume_reset(void);
include/platx/attach_scope.h
/* platx/attach_scope.h — O16-5: a TEST provider never becomes a CHILD.
 *
 * v1 has two providers and each owns exactly one scope: attach.mock is TEST,
 * attach.child is CHILD. Registering the mock as CHILD, or preparing the
 * child as TEST, lies to the operator — an attachment "as CHILD" with no host
 * path. This predicate binds provider id to its one legal scope. attach.h and
 * the provider catalog are not touched.
 */

/*
 * 0 only when provider_id is a known v1 provider and want is its one legal
 * scope: attach.mock → TEST, attach.child → CHILD. Anything else — an unknown
 * id, a scope that is not the provider's, or a RUNTIME/SERVICE/BROWSER/LAB
 * scope (not this slice) — returns -1. Never picks "the first" provider.
 */
int plat_attach_scope_ok(const char *provider_id, plat_attach_scope_t want);
include/platx/attach_txn.h
/* platx/attach_txn.h — A2-P08: attach.child как транзакция.
 *
 * ПОЧЕМУ ЭТО ОТДЕЛЬНЫЙ ЗАГОЛОВОК
 *
 * attach.h описывает состояния и провайдерский ABI. Он не описывает
 * ТРАНЗАКЦИЮ: у подготовки нет срока, у commit нет владельца, а у отката нет
 * ни собственного наблюдаемого состояния, ни исхода. Дописывать это в
 * attach.h значило бы менять публичную структуру дескриптора, которую уже
 * читают чужие полосы. Поэтому — отдельный заголовок, как это уже сделано
 * для attach_reason.h (O16-8), с той же оговоркой: attach.h не трогается.
 *
 * ЧТО ЗДЕСЬ ЗАКРЫВАЕТСЯ
 *
 * Канон C24–C28 требует обязательного undo. В attach undo был, но был
 * неотличим от своего отсутствия:
 *
 *   1. Состояние PLAT_ATTACH_ROLLBACK объявлено в attach.h и в переходах
 *      attach_state.c, но НИ В ОДНОМ пути не наблюдаемо: оно записывается и
 *      тут же перезаписывается в DETACHED внутри одного прямолинейного
 *      блока, между записями нет ни точки публикации, ни возврата
 *      управления.
 *   2. Порядки записи расходились и лгали оба. attach_commit.c и
 *      attach_rollback.c звали provider.rollback ДО перевода в ROLLBACK —
 *      во время самой очистки состояние говорило ATTACHING/VERIFYING, то
 *      есть «привязка идёт». attach_verify.c звал его ПОСЛЕ перевода в
 *      DETACHED — во время очистки состояние говорило «очистка завершена».
 *   3. Слот в конце стирался (attach_slot_free). Обнулённый слот по
 *      построению читается как DETACHED — то же самое, что «чисто, ничего
 *      не было». Смерть процесса посередине отката оставляла картину,
 *      неотличимую от успешно завершённого отката, при живом ребёнке.
 *   4. Код возврата provider.rollback отбрасывался, а plat_attach_rollback
 *      возвращал 0 всегда. Провалившаяся очистка сообщала об успехе.
 *
 * Это тот же дефект, что A4 нашёл в своём ABI (ACT-GAP-24): нет состояния
 * «откат идёт», и оба порядка записи лгут при смерти процесса между шагами.
 *
 * Журнал ниже — не второй движок отката. Он ничего не откатывает: он
 * записывает НАМЕРЕНИЕ до первого разрушающего шага и ИСХОД после
 * последнего, и переживает освобождение слота. Различить «откат завершён» и
 * «откат оборван» можно только так: запись, начатая и не закрытая, есть
 * прерванный откат, и её остаток назван.
 *
 * SPDX-License-Identifier: GPL-2.0
 */

/* ── Журнал отката ───────────────────────────────────────────────────── */

/*
 * NONE = 0 — «записи нет», и именно потому, что это значение по умолчанию у
 * обнулённой памяти, оно НЕ означает «откат завершён». Завершение — это
 * DONE или FAILED, оба ненулевые и оба выставляются явно.
 *
 * BEGUN/PROVIDER/RESOURCES — откат начат и не закрыт. Запись в любой из этих
 * фаз означает прерванный откат: процесс умер между шагами, и остаток
 * (ребёнок, владения) не подтверждён освобождённым.
 */
typedef enum plat_attach_undo_phase {
    PLAT_ATTACH_UNDO_NONE      = 0, /* записи нет — НЕ «откат завершён» */
    PLAT_ATTACH_UNDO_BEGUN     = 1, /* намерение записано, шагов нет */
    PLAT_ATTACH_UNDO_PROVIDER  = 2, /* провайдер отработал (ребёнок снят) */
    PLAT_ATTACH_UNDO_RESOURCES = 3, /* владения освобождены */
    PLAT_ATTACH_UNDO_DONE      = 4, /* откат завершён, остатка нет */
    PLAT_ATTACH_UNDO_FAILED    = 5  /* откат завершён, остаток назван */
} plat_attach_undo_phase_t;

#define PLAT_ATTACH_UNDO_STAGE_MAX 32
#define PLAT_ATTACH_UNDO_WHY_MAX   96
/* История переживает слоты: место под каждый слот плюс столько же на
 * недавно закрытые записи. Иначе запись о прерванном откате вытеснялась бы
 * следующей же подготовкой — то есть исчезала бы ровно тогда, когда нужна. */
#define PLAT_ATTACH_UNDO_MAX (PLAT_ATTACH_SLOTS_MAX * 2)

typedef struct plat_attach_undo_rec {
    plat_attach_id_t         id;
    uint32_t                 generation;   /* поколение на момент начала */
    plat_attach_undo_phase_t phase;
    plat_attach_state_t      state_at_begin;
    int                      leftover;     /* !=0 — что-то не освобождено */
    char                     stage[PLAT_ATTACH_UNDO_STAGE_MAX];
    char                     why[PLAT_ATTACH_UNDO_WHY_MAX];
} plat_attach_undo_rec_t;

/*
 * Записать НАМЕРЕНИЕ откатить. Обязан вызываться ДО первого разрушающего
 * шага — до provider.rollback, до shutdown ребёнка, до освобождения слота.
 * Повторный begin по тому же id, пока запись не закрыта, не создаёт вторую
 * запись и не теряет первую: откат уже идёт.
 * desc == NULL → -1. Возвращает 0, когда намерение записано.
 */
int plat_attach_undo_begin(const plat_attach_desc_t *desc, const char *why);

/*
 * Отметить пройденный шаг. rc != 0 означает, что шаг НЕ удался: фаза не
 * продвигается, остаток запоминается, и итог такой записи может быть только
 * FAILED. Неизвестный id игнорируется (возврата нет — шаг не решение).
 */
void plat_attach_undo_step(plat_attach_id_t id,
                           plat_attach_undo_phase_t reached,
                           int rc, const char *stage);

/*
 * Закрыть запись. leftover != 0 → FAILED, иначе DONE.
 * Возвращает 0, если откат завершён без остатка, и -1, если остаток есть
 * или запись не открывалась. Это единственный источник ответа «очистка
 * удалась», и он отделён от причины исходного отказа (attach_reason.h).
 */
int plat_attach_undo_end(plat_attach_id_t id, int leftover, const char *stage);

/* Прочитать запись по id. Нет записи → -1, *out обнуляется. */
int plat_attach_undo_get(plat_attach_id_t id, plat_attach_undo_rec_t *out);

/*
 * Сколько откатов начато и не закрыто. Это и есть различитель, которого не
 * было: > 0 означает «есть прерванный или идущий откат», и при этом слоты
 * могут быть уже освобождены и выглядеть чистыми.
 */
int plat_attach_undo_unfinished(void);

/* Идёт ли (или оборван) откат именно этой привязки. */
int plat_attach_undo_in_progress(plat_attach_id_t id);

/* Прочитать i-ю незакрытую запись (0..unfinished-1). Нет — -1. */
int plat_attach_undo_unfinished_at(int i, plat_attach_undo_rec_t *out);

/* Имя фазы. Неизвестное значение → "UNKNOWN", никогда не "DONE". */
const char *plat_attach_undo_phase_str(plat_attach_undo_phase_t p);

/* Забыть весь журнал. Точка сброса для тестов, не для продукта. */
void plat_attach_undo_reset(void);

/* ── Срок подготовки ─────────────────────────────────────────────────── */

/*
 * Подготовленная транзакция удерживает слот и связанный launch spec. Без
 * срока «подготовил и забыл» держит слот навсегда, и таблица из восьми
 * слотов исчерпывается тем, что никто не отменял.
 *
 * 0 — срок отключён (поведение до P08). По умолчанию — 30 с.
 */
#define PLAT_ATTACH_PREPARE_MS_DEFAULT 30000u

void     plat_attach_txn_set_prepare_ms(unsigned ms);
unsigned plat_attach_txn_prepare_ms(void);

/*
 * Истёк ли срок подготовки. 1 — истёк, 0 — нет либо срок отключён,
 * -1 — нет такой привязки.
 */
int plat_attach_txn_expired(plat_attach_id_t id);

/*
 * Источник монотонного времени в миллисекундах. Подменяется только тестом,
 * чтобы срок можно было проверить, не ожидая его наяву. NULL возвращает
 * штатный CLOCK_MONOTONIC.
 */
void plat_attach_txn_set_clock(uint64_t (*now_ms)(void));
uint64_t plat_attach_txn_now_ms(void);

/* ── Владелец транзакции ─────────────────────────────────────────────── */

/*
 * Кто подготовил — тот и завершает. plat_attach_commit(id) владельца не
 * знает вовсе, поэтому подготовленную транзакцию мог активировать любой
 * вызывающий. Здесь владелец связывается с подготовкой явно.
 *
 * generation 0 запрещено: поколение не выдумывается (канон), а нулевое
 * означает «владельца ещё нет».
 */
int plat_attach_txn_bind_owner(plat_attach_id_t id, plat_owner_t owner);

/* Прочитать связанного владельца. Не связан → generation == 0. */
plat_owner_t plat_attach_txn_owner(plat_attach_id_t id);

/*
 * commit с проверкой владельца. Если с подготовкой связан владелец и
 * caller ему не равен (module, instance, generation) — отказ, и
 * подготовленная транзакция ОСТАЁТСЯ подготовленной: чужой вызов не
 * активирует её и не разрушает.
 * Если владелец не связывался, ведёт себя как plat_attach_commit.
 */
int plat_attach_commit_as(plat_attach_id_t id, plat_owner_t caller);
include/platx/child_host.h
/* platx/child_host.h — Core launcher for CHILD isolation.
 * Stub never calls this. Isolation is a deploy check, not a module type.
 */
#define PLAT_CHILD_HANDSHAKE_MS 500
/* First ACK cannot arrive before unshare + landlock + exec. handshake_ms is
 * the per-message protocol budget after the child is on the wire. Starting
 * that 500ms clock at fork fail-closes a live child that simply has not
 * exec'd yet. */
#define PLAT_CHILD_STARTUP_MS   3000

typedef struct plat_child_inst plat_child_inst_t;

typedef struct plat_child_launch {
    const plat_module_descriptor_t *desc; /* isolation_allowed checked here */
    const char                     *exe;
    const char *const              *extra_argv; /* NULL-terminated or NULL */
    plat_isolate_t                  isolate;    /* must be PLAT_ISO_CHILD */
    plat_lifecycle_t               *lm;         /* optional; death → recovery */
    const plat_recovery_opts_t     *rec_opts;   /* NULL = ON_FAILURE defaults */
    int                             handshake_ms; /* 0 = default */
    int                             xim;          /* !=0: arm XIM mediation before exec (O-B1b) */
} plat_child_launch_t;

/* READY only after the IPC proxy is usable and provides are in the registry.
 * Spawn path: socketpair → fork → sandbox → exec. Sandbox/exec/handshake
 * failure: no PID left, no capability, *out == NULL. EMBEDDED is refused.
 *
 * With lm: watched once on the live plat_recovery_t inside the instance.
 * Death: revoke, then plat_recovery_apply on the snapshot row — never a
 * stack policy object. RESTART respawns this instance (new PID, new gen). */
int  plat_child_host_spawn(const plat_child_launch_t *req,
                           plat_child_inst_t **out);
/* S510: почему последнее порождение не дошло до READY: этап и причина.
 * Пустая строка — последняя попытка удалась. Никогда не NULL.
 * errno один и тот же у трёх разных отказов; этап их различает. */
const char *plat_child_host_last_error(void);
/* 0 = reaped (provides revoked, recovery decides if it was live). 1 = still up. */
int  plat_child_host_reap(plat_child_inst_t *ch, int timeout_ms);
void plat_child_host_shutdown(plat_child_inst_t *ch);
pid_t plat_child_host_pid(const plat_child_inst_t *ch);
int   plat_child_host_ready(const plat_child_inst_t *ch);
/* Trusted parent owner. generation 0 if ch is NULL. */
plat_owner_t plat_child_host_owner(const plat_child_inst_t *ch);
02

audit

Журнал действий, событий безопасности и происхождения
src/audit/Наблюдение и исследование11 файлов3 API headers

Audit создаёт доказуемый журнал security- и operator-relevant действий. Он отвечает не за отладочные сообщения, а за причинную запись: actor, operation, target owner/generation, policy decision, result, correlation и минимально необходимый контекст.

Граница ответственности

  • Append API принимает уже типизированную запись и не выполняет lifecycle/security policy.
  • Секреты редактируются до сериализации; audit sink никогда не получает raw key/token.
  • Tail/search/rotate — operator operations с authz; отсутствие внешнего MBus не блокирует локальный append.

Устройство подсистемы

  • Schema envelope содержит schema version, monotonic/wall time, sequence, correlation, actor, component, action, target и outcome.
  • Локальный JSONL writer использует append discipline, bounded record и crash-aware framing; повреждённый последний record распознаётся reader-ом.
  • Rotation атомарно меняет active file, fsync policy задаётся profile. Heavy indexing остаётся внешним optional consumer.
  • Audit adapters для lifecycle, recovery, command, plugin, XIM/Hook и RA2C формируют одинаковые поля вместо свободного текста.

Поток работы

  • Operation получает correlation/actor.
  • Policy/handler формирует typed audit record.
  • Redaction + schema validation.
  • Append/sequence → optional event/MBus export.

Отказ и восстановление

  • Partial write: reader игнорирует незавершённый tail, sequence gap диагностируется.
  • Rotation crash: ровно один active target выбирается по durable marker.
  • Flood: bounded queue/backpressure; drop security records запрещён без fatal/degraded signal.

Основные возможности

  • JSONL append path designed for crash-tolerant records.
  • Audit schema and MBus bridge connect actions to correlation context.
  • Used by appliances and server for operator evidence.
Архитектурные детали и инварианты

Технический срез и открытый layout gap

Публичный platx_audit_record_t содержит поля общей длиной 80 байт, но

static_assert требует 88. Это открытое противоречие; готовый interoperable

88-байтный codec или успешно измеренный ring не заявляются.

8192 × 80 = 655360 байт; 8192 × 88 = 720896 байт. Это расчёт, не BSS measurement.

Декларации и source excerpts — AUDIT_API.

Обязательное поведение

1. Sequence монотонен в своей epoch; export сохраняет boot/owner identity.

2. Wrap и отставший consumer явно отражают потерю в GAP/counters. Старые

записи не могут быть молча перезаписаны с заявлением полного журнала.

3. MAC проверяется криптографически по нужным bytes/key; non-zero недостаточно.

4. Persistence требует sink, подтверждения записи, retention и проверки

restart/crash. RAM ring теряется при аварии питания.

5. Потеря обязательного аудита блокирует новые зависимые effects; cleanup

имеет собственный deadline и не ждёт недоступный sink бесконечно.

6. Lock-free cursor не доказывает корректность concurrent payload writers

или предел 1 µs. Нужны memory-ordering review и измерение на target.

Управление и диагностика

Корневые команды: audit, flow. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / audit →
Состав подсистемы / 11 файлов
Файл / компонентНазначение и граница
src/audit/audit.cappend-only JSON Lines audit trail implementation. Writes one JSON object per line to a log file. Rotates at 10 MB. All writes are serialised through a mutex; the flush is forced after each event (O_SYNC / fdatasync) so that the log survives a crash.
src/audit/audit.happend-only JSON Lines audit trail. Each event is one JSON object on its own line (NDJSON / JSON Lines): "action":"harden","pid":1234,"uid":0,"details":"flags=0x7"} Fields are always in this order (deterministic for grep / SIEM ingest): ts — ISO-8601 UTC with milliseconds
src/audit/audit_append.ccrash-tolerant audit log append (STAB-131). See audit_frame.h for why this unit takes a file descriptor and why the scan runs forward. The previous contents of this file used a frame header type, a struct field and a scan function that were declared nowhere; it was listed in PLATX_UNCLASSIFIED_OPTIONAL_SRCS as a buildable
src/audit/audit_export.cexport audit ring records to a file. The previous implementation opened the file, wrote a 72-byte header with record_count left at zero and exported_at never assigned, closed it and returned success. Not one record reached the file. It also declared `extern int audit_export_raw(...)`, a function that exists nowhere in the
src/audit/audit_frame.hcrash-tolerant framing for the audit log. STAB-131 asked for partial-record detection, fsync, magic frames and tail truncation. The file that was supposed to implement it, audit_append.c, referred to a frame header type, a field of an opaque struct and a scan function — none of which existed anywhere in the tree.
src/audit/audit_query.cquery the audit ring by type or corr_id. Both queries used to walk raw slots and copy whatever was there. With no publication step in the ring that meant they returned slots that were still being written, and — because the HMAC was never filled in — by the
src/audit/audit_ring.cPublication protocol Before this file had one, a producer claimed a slot with a relaxed fetch_add and then wrote the fields with plain stores. Nothing told a reader whether a slot it was looking at had been finished, and nothing ordered the field writes against the cursor increment. A reader could
src/audit/audit_ring.hThe record layout lives in include/platx/platx_audit.h and is included from there. This file used to carry a second copy of it; keeping two declarations of one type in a tree is how the eighty-eight-byte record came to be eighty bytes in three places at once.
src/audit/audit_sign.cHMAC-SHA256 per-record signing in MIL mode. What was here before was an XOR fold of the record with the key, marked "Stub", with a private audit_sig_t that no other file could name and a signature no caller used. Its own consumer, tests/audit/t_audit_sign.c,
src/audit/cmd_audit.cCLI namespace "audit" for the audit logging module. audit test [--message=TEXT] write a test event, verify it appears audit log --level=L --comp=C --action=A [--details=TEXT] write event audit tail [--count=N] [--level=L] show last N audit events
src/audit/cmd_flow.cnamespace `audit flow`: пассивный анализ соединений. Связывает три части, описанные в docs/OBSERVABILITY.md: источник кадров анализатор приёмник ─────────────── ────────── ──────── наблюдатель txpstack → nl_flow_tab_t → журнал audit (JSONL)
Контракты API / 3 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/audit_mbus.h
/* platx/audit_mbus.h — Critical Audit events via MBus/RA2C with backpressure (INT-117). */
#define PLAT_AUDIT_MBUS_TOPIC      "audit.critical"
#define PLAT_AUDIT_MBUS_MIN_LEVEL  AUDIT_WARNING   /* only WARNING+ forwarded */
#define PLAT_AUDIT_MBUS_QUEUE_MAX  64

typedef enum {
    PLAT_AUDIT_MBUS_OK       = 0,
    PLAT_AUDIT_MBUS_DROPPED  = 1,   /* backpressure: queue full */
    PLAT_AUDIT_MBUS_NOLINK   = 2,
} plat_audit_mbus_status_t;

/* Forward an audit event to MBus if level >= MIN_LEVEL.
 * Applies backpressure: drops with counter if queue full.  0/-1. */
int plat_audit_mbus_forward(audit_level_t level, const char *comp,
                            const char *action, const char *detail);

/* Flush queue to MBus (call from background tick).  Returns n flushed. */
int plat_audit_mbus_flush(void);

/* Dropped event counter since init. */
uint64_t plat_audit_mbus_dropped(void);
include/platx/audit_schema.h
/* platx/audit_schema.h — Structured audit schema (INT-112). */
#define PLAT_AUDIT_COMP_MAX   32
#define PLAT_AUDIT_ACT_MAX    64
#define PLAT_AUDIT_DETAIL_MAX 256

typedef struct {
    audit_level_t   level;
    char            comp[PLAT_AUDIT_COMP_MAX];
    char            action[PLAT_AUDIT_ACT_MAX];
    plat_corr_id_t  corr_id;
    uint64_t        ctx_id;
    uint32_t        ctx_gen;
    char            detail[PLAT_AUDIT_DETAIL_MAX];
} plat_audit_event_t;

/* Write a structured audit event (includes corr_id in detail).  0/-1. */
int plat_audit_write(const plat_audit_event_t *ev);

/* Convenience: write with current thread corr_id.  0/-1. */
int plat_audit_write_corr(audit_level_t level, const char *comp,
                          const char *action, uint64_t ctx_id, uint32_t ctx_gen,
                          const char *detail);

/* Шов для подсистем, которые собираются БЕЗ audit-зоны (integrity, seccomp,
 * LSM, SelfProtect). Они форматируют одну строку вида "COMP|action|detail"
 * и зовут этот символ через weak-переобъявление; если audit-зоны в сборке
 * нет, символ не разрешается и вызов не делается.
 *
 * Имя отдельное НЕ для красоты. До 09.09.2026 те же подсистемы объявляли
 * у себя `extern void plat_audit_write(const char *)` — при том, что
 * настоящий plat_audit_write принимает `const plat_audit_event_t *` и
 * возвращает int. Пока audit_schema.c и эти модули не попадали в один
 * бинарь, расхождение было невидимо; в первой же общей сборке строка
 * ушла бы туда, где ждут структуру. Это класс A-F001 (TE-SRC-12), и
 * лечится он тем, что у шва СВОЁ имя и свой прототип в заголовке. */
void plat_audit_note(const char *msg);
include/platx/audit_types.h
/* Public audit severity type shared by audit-facing subsystem APIs. */
typedef enum {
    AUDIT_DEBUG   = 0,
    AUDIT_INFO    = 1,
    AUDIT_NOTICE  = 2,
    AUDIT_WARNING = 3,
    AUDIT_ERROR   = 4,
    AUDIT_LEVEL_COUNT
} audit_level_t;

/* W1-40: unified audit_log_t public API (opaque handle). */
typedef struct audit_log audit_log_t;

/* Core audit write — severity-tagged, UTF-8, null-terminated message. */
int audit_log(audit_log_t *al, audit_level_t level, const char *msg);
03

central

Управление группой узлов, миссиями и распределённым состоянием
src/central/Управление и контракты11 файлов0 API headers

Central связывает идентичности узлов, операторские запросы, миссии, результаты аттестации и события. Он предоставляет представление распределённой системы и координирует действия через публичные контракты. Локальная политика узла сохраняет собственную ответственность за доступ к ресурсам.

Граница ответственности

  • Central связывает идентичности узлов, операторские запросы, миссии, результаты аттестации и события. Он предоставляет представление распределённой системы и координирует действия через публичные контракты. Локальная политика узла сохраняет собственную ответственность за доступ к ресурсам.

Устройство подсистемы

  • реализация единого слоя операций Central Node. Контракт и обоснование — в central_api.h. Здесь держится главный инвариант: ни CLI, ни HTTP-плагин не содержат бизнес-логики. Всё, что умеет Central, перечислено в g_ops[] ровно один раз.
  • единственная реализация HTTP-поверхности Central Node. Контракт и обоснование — в central_http.h. Здесь важны три решения. 1. МАРШРУТОВ ЗДЕСЬ НЕТ. Ни одного strcmp по "/api/central/". Пара (метод, путь) ищется в таблице central_api_ops(). Поэтому маршрут,
  • автономный Central Node с встроенным веб-интерфейсом. ЗАЧЕМ ОТДЕЛЬНЫЙ БИНАРЬ Полный platx — это десятки подсистем; тащить в него sqlite3 и HTTP-сервер ради узла доверия значило бы расширить поверхность атаки того самого процесса, который решает, кому во флоте доверять. Здесь собрано ровно
  • «объявлено, но не исполняется, и это неотличимо от исправной работы»: D1. Транспорт. mesh_publish_raw / mesh_subscribe_raw объявлялись здесь локальным weak extern и НЕ БЫЛИ ОПРЕДЕЛЕНЫ НИГДЕ в дереве. Символы резолвились в NULL, `if (mesh_publish_raw)` не выполнялся, rc

Управление и диагностика

Корневые команды: central. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / central →
Состав подсистемы / 11 файлов
Файл / компонентНазначение и граница
src/central/central_api.cреализация единого слоя операций Central Node. Контракт и обоснование — в central_api.h. Здесь держится главный инвариант: ни CLI, ни HTTP-плагин не содержат бизнес-логики. Всё, что умеет Central, перечислено в g_ops[] ровно один раз.
src/central/central_api.hединый слой операций Central Node. ЗАЧЕМ ЭТОТ ФАЙЛ СУЩЕСТВУЕТ Требование: «через CLI / API должна быть возможность выполнить любую задачу ничуть не хуже, чем через визуальный интерфейс». Такое требование нельзя выполнить обещанием — только конструкцией. Если CLI
src/central/central_http.cединственная реализация HTTP-поверхности Central Node. Контракт и обоснование — в central_http.h. Здесь важны три решения. 1. МАРШРУТОВ ЗДЕСЬ НЕТ. Ни одного strcmp по "/api/central/". Пара (метод, путь) ищется в таблице central_api_ops(). Поэтому маршрут,
src/central/central_http.hтранспортно-независимая HTTP-поверхность Central Node. ЗАЧЕМ ЭТОТ ФАЙЛ ПОЯВИЛСЯ До него разбор HTTP жил внутри плагина external/ra2c_webserver/plugins/ src/central_api.c, и вместе с разбором там жила РУЧНАЯ КОПИЯ структур central_req_t / central_op_t. Копия нужна была потому, что плагин не
src/central/central_main.cавтономный Central Node с встроенным веб-интерфейсом. ЗАЧЕМ ОТДЕЛЬНЫЙ БИНАРЬ Полный platx — это десятки подсистем; тащить в него sqlite3 и HTTP-сервер ради узла доверия значило бы расширить поверхность атаки того самого процесса, который решает, кому во флоте доверять. Здесь собрано ровно
src/central/central_mod.hрегистрация Central Node как подсистемы платформы. Отделено от central_api.h сознательно: API-слой не должен зависеть от console/subsys, потому что тот же слой линкуется в плагин webserver, где платформенной консоли нет.
src/central/central_node.c«объявлено, но не исполняется, и это неотличимо от исправной работы»: D1. Транспорт. mesh_publish_raw / mesh_subscribe_raw объявлялись здесь локальным weak extern и НЕ БЫЛИ ОПРЕДЕЛЕНЫ НИГДЕ в дереве. Символы резолвились в NULL, `if (mesh_publish_raw)` не выполнялся, rc
src/central/central_node.hRA2C Central Node: реестр доверия + политики + команды Central Node — стратегический центр триединой системы доверия. Принимает аттестации через Mesh, хранит Trust Registry, рассылает команды и политики через транспорт mesh_raw (PXTRUST envelope). mesh_subscribe_raw локальным weak extern, который НЕ БЫЛ ОПРЕДЕЛЁН нигде
src/central/central_web.cвстраивание ra2c_webserver в процесс Central Node. Тот же исполняемый файл, который держит реестр доверия, отдаёт HTTP. Не отдельный процесс с копией состояния, не мост, не IPC: реестр, который видит оператор в браузере, — это те же байты, по которым узел
src/central/central_web.hвстроенный HTTP-сервер Central Node. ПОЧЕМУ РЕГИСТРАЦИЯ, А НЕ WEAK-СИМВОЛ Слой операций central_api.c обязан уметь поднять веб-интерфейс, но не может зависеть от external/ra2c_webserver: тот тянет sqlite3, потоки и сокеты, а Central линкуется и в тесты, и в сборки без сети.
src/central/cmd_central.cnamespace `central`: CLI + интеграция в платформу. КОНСТРУКЦИЯ, А НЕ ОБЕЩАНИЕ Требование «через CLI можно всё то же, что через UI» здесь выполнено тем, что этот файл НЕ СОДЕРЖИТ бизнес-логики. Каждая подкоманда: разобрать --ключ=значение → central_req_t → central_api_dispatch()
Контракты API / 0 заголовков

Отдельный заголовок из include/platx/ не закреплён за этим доменом в справочнике. Внутренние границы перечислены в составе файлов; управление и публичные связи описаны в соседних подсистемах.

04

context

Идентичность контекста исполнения и контроль его жизни
src/context/Управление и контракты4 файлов3 API headers

Context моделирует объект среды, к которому применяются attachments, leases, observations и VFS tags: process, child, session, namespace или иной execution unit. Он даёт стабильную identity поверх нестабильных OS pid/fd.

Граница ответственности

  • Context не владеет lifecycle модуля, но имеет собственные alive/lost/generation facts.
  • OS identifiers являются attributes, не primary key.

Устройство подсистемы

  • Context table индексируется internal id и generation; attributes включают kind, owner, parent, OS ids, labels и health.
  • Creation привязывается к resource/child/session event. Loss идемпотентно закрывает новые leases и уведомляет attachment graph.
  • Descriptor-like providers могут расширять attributes versioned namespaces, не меняя core struct.

Поток работы

  • Provider/child/session creates context.
  • Policy attaches labels/leases.
  • Attachment/observe/VFS resolve handle.
  • Loss → revoke leases → degrade attachments → tombstone.

Отказ и восстановление

  • PID reuse не оживляет старый context: generation/start token различаются.
  • Table full возвращает bounded error и rollback creator resources.
  • Parent loss policy явно cascade либо orphan, без случайного поведения.

Основные возможности

  • Maintains host-local execution contexts and owner association.
  • Context loss degrades dependent attachment instead of silently reusing stale identity.
  • Provides explainability for attachment and policy decisions.

Управление и диагностика

Корневые команды: context. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / context →
Состав подсистемы / 4 файлов
Файл / компонентНазначение и граница
src/context/cmd_context.cprint one context. Does not create or choose. Selection and lifecycle live in context.c. A second table here would disagree with explain. Verb is `context`, not `gadget`.
src/context/context.chost-local execution-context table. One bounded array. A row is a context. Create fills a free or destroyed slot; a live row is never reused to make space. Destroy keeps the generation so the old handle stays STALE. Recreate is the same slot with generation++ — PID reuse is a new identity on a new
src/context/ctx_desc.cO16-9 / CTX0: context descriptor, not a bool. A zeroed descriptor is EMPTY, never a LIVE PROCESS. The string helpers never a type name. v1 admits PROCESS/SERVICE/CHILD/RUNTIME; BROWSER and CONTAINER are refused (not this slice). Generation 0 is never a live epoch.
src/context/ctx_watch.cO16-21: context lost → attachment DEGRADED. Context death ≠ session disconnect ≠ target death. When the context a runtime occupies dies, the attachment bound to it must not keep claiming ACTIVE — a request would land in a dead runtime. It goes DEGRADED via the
Контракты API / 3 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/context.h
/* platx/context.h — WHERE the runtime lives.
 *
 * NODE ≠ ATTACHMENT ≠ TARGET ≠ SESSION ≠ CAPABILITY. Context is none of
 * those. It is the host-local place a runtime occupies: kind, identity,
 * generation, optional parent. Capability later binds to (context, gen),
 * not to "the process" or "the binary".
 *
 * PID is a hint. birth + ns_inum make identity. A recycled PID is a new
 * generation, never the same context. Destroyed or stale-generation
 * handles fail closed. A full table is UNAVAIL — live rows are never
 * evicted to make space. This table is not plat_event and not a registry.
 */

#define PLAT_CONTEXT_SLOTS_MAX  16
#define PLAT_CONTEXT_REASON_MAX 160

typedef uint32_t plat_context_id_t; /* 0 = invalid */

typedef enum plat_context_kind {
    PLAT_CONTEXT_KIND_PROCESS = 1,
    PLAT_CONTEXT_KIND_CHILD   = 2,
    PLAT_CONTEXT_KIND_RUNTIME = 3
} plat_context_kind_t;

typedef enum plat_context_status {
    PLAT_CONTEXT_OK         = 0,
    PLAT_CONTEXT_ERR_ARGS   = 1,
    PLAT_CONTEXT_ERR_STALE  = 2, /* destroyed or generation mismatch */
    PLAT_CONTEXT_ERR_NOTFOUND = 3,
    PLAT_CONTEXT_ERR_UNAVAIL  = 4, /* table full; no silent evict */
    PLAT_CONTEXT_ERR_REFUSED  = 5  /* duplicate live identity, live children */
} plat_context_status_t;

/*
 * Process identity. All three fields must match for "the same" context.
 * pid alone is never enough: a new process that reused the PID is a
 * different identity (different birth and/or ns_inum).
 */
typedef struct plat_context_identity {
    pid_t    pid;
    uint64_t birth;    /* starttime / clock ticks since boot */
    uint64_t ns_inum;  /* inode of the pid namespace */
} plat_context_identity_t;

/* Handle: id + generation. Generation 0 is never a live handle. */
typedef struct plat_context_handle {
    plat_context_id_t id;
    uint32_t          generation;
} plat_context_handle_t;

typedef struct plat_context_desc {
    plat_context_handle_t   handle;
    plat_context_kind_t     kind;
    plat_context_identity_t identity;
    plat_context_handle_t   parent; /* id=0 → none */
    int                     live;   /* 1 only while created and not destroyed */
} plat_context_desc_t;

int  plat_context_init(void);
void plat_context_fini(void);

/*
 * Create a live context. parent NULL or {0,0} means none. Parent, if
 * named, must be a live matching handle — stale parent is STALE, not a
 * hanging child. Duplicate live identity is REFUSED. Full live table is
 * UNAVAIL. Returns plat_context_status_t; *out is filled only on OK.
 */
int plat_context_create(plat_context_kind_t kind,
                        const plat_context_identity_t *ident,
                        const plat_context_handle_t *parent,
                        plat_context_handle_t *out);

/* Live + matching gen required. Live children keep the parent alive. */
int plat_context_destroy(plat_context_handle_t h);

/*
 * Same id, generation++. Old handle fails closed. Identity may change
 * (PID reuse). Live children refuse — their parent_gen would go stale
 * under them. *out is the new handle, only on OK.
 */
int plat_context_recreate(plat_context_handle_t old,
                          const plat_context_identity_t *ident,
                          plat_context_handle_t *out);

/*
 * generation==0 resolves the live slot by id (CLI). A destroyed slot
 * is STALE, not a live desc. generation!=0 is an exact handle check.
 */
int plat_context_explain(plat_context_handle_t h, plat_context_desc_t *out);

int plat_context_explain_render(const plat_context_desc_t *d,
                                char *buf, size_t n);

/* 1 only when pid, birth, and ns_inum all match. All-zero is not a key. */
int plat_context_identity_same(const plat_context_identity_t *a,
                               const plat_context_identity_t *b);

const char *plat_context_kind_str(plat_context_kind_t k);
const char *plat_context_status_str(int s);
include/platx/context_life.h
/* platx/context_life.h — O16-9 / CTX0: the context lifecycle descriptor.
 *
 * WHERE a runtime lives, as a lifecycle + attachment binding: state, type,
 * generation, parent, the attachment it hosts, its owner and budget epoch.
 * A zeroed descriptor is EMPTY, never a LIVE PROCESS — a context is not a
 * bool, exactly as an attachment is not.
 *
 * This is a SEPARATE header on purpose. include/platx/context.h already owns
 * the context identity table (plat_context_* handles: pid/birth/ns_inum,
 * create/destroy/recreate/explain) and is depended on by context.c,
 * cmd_context.c, the lease tree and others. The two are complementary layers —
 * identity vs lifecycle — and the project will want to reconcile them into
 * one context module later. Until then this header adds the lifecycle view
 * without touching the identity one: distinct plat_ctx_* names, no collision.
 *
 * Bodies of the create/bind/drain lifecycle ABI live in O16-10…O16-21; this
 * header declares the descriptor and the pure helpers CTX0 implements.
 */

typedef uint32_t plat_ctx_id_t;   /* 0 = invalid */

/*
 * Lifecycle state. EMPTY = 0 so a zeroed descriptor is EMPTY, not LIVE.
 *   EMPTY    → the slot holds nothing
 *   BINDING  → identity + attachment being bound (no live runtime yet)
 *   LIVE     → bound, generation ≥ 1, capability may target it
 *   DEGRADED → live but impaired (health), still not EMPTY
 *   DRAINING → winding down; no new work
 *   DEAD     → gone; a stale handle fails closed
 */
typedef enum plat_ctx_state {
    PLAT_CTX_EMPTY    = 0,
    PLAT_CTX_BINDING  = 1,
    PLAT_CTX_LIVE     = 2,
    PLAT_CTX_DEGRADED = 3,
    PLAT_CTX_DRAINING = 4,
    PLAT_CTX_DEAD     = 5
} plat_ctx_state_t;

/*
 * Context type. v1 admits PROCESS / SERVICE / CHILD / RUNTIME. BROWSER and
 * CONTAINER are named so later slices have a number, but are NOT this slice —
 * plat_ctx_type_ok() rejects them.
 */
typedef enum plat_ctx_type {
    PLAT_CTX_TYPE_PROCESS   = 1,
    PLAT_CTX_TYPE_SERVICE   = 2,
    PLAT_CTX_TYPE_CHILD     = 3,
    PLAT_CTX_TYPE_RUNTIME   = 4,
    PLAT_CTX_TYPE_BROWSER   = 5,  /* not this slice */
    PLAT_CTX_TYPE_CONTAINER = 6   /* not this slice */
} plat_ctx_type_t;

/*
 * The lifecycle descriptor. generation is 0 until a successful bind (a live
 * context always has generation ≥ 1, like an XIO owner or an attachment).
 * attach_id mirrors plat_attach_id_t (kept as a plain uint32_t so this header
 * need not pull in all of attach.h); 0 = no attachment bound. parent_id 0 =
 * no parent. A zeroed struct is a valid EMPTY descriptor.
 */
typedef struct plat_ctx {
    plat_ctx_id_t    id;
    plat_ctx_type_t  type;
    uint32_t         generation;  /* 0 until bind */
    plat_ctx_id_t    parent_id;   /* 0 = none */
    uint32_t         attach_id;   /* bound attachment id; 0 = none */
    plat_owner_t     owner;
    uint32_t         budget_gen;  /* budget epoch */
    plat_ctx_state_t state;       /* zeroed → EMPTY */
} plat_ctx_t;

/* ── CTX0 helpers (bodies in ctx_desc.c) ─────────────────────────────── */

/* Out-of-range → "UNKNOWN", never "LIVE". */
const char *plat_ctx_state_str(plat_ctx_state_t s);

/* Out-of-range → "UNKNOWN", never a type name. */
const char *plat_ctx_type_str(plat_ctx_type_t t);

/* 1 for PROCESS/SERVICE/CHILD/RUNTIME; 0 for BROWSER/CONTAINER and any
 * unknown value. */
int plat_ctx_type_ok(plat_ctx_type_t t);

/* A generation of 0 is never valid for a bind — fail closed, like XIO/ATT. */
int plat_ctx_gen_ok(uint32_t generation);

/*
 * Descriptor-level bind precondition (no side effect, no table). A context
 * may be bound only when its type is admissible, its generation is non-zero,
 * and it is not already live/draining/dead. Returns 1 if a bind could
 * proceed, 0 otherwise. The actual bind lives in a later CTX package.
 */
int plat_ctx_can_bind(const plat_ctx_t *c);

/* ── Lifecycle ABI implemented by O16-10…O16-21 (declared here) ────────
 * Bodies are NOT in ctx_desc.c. Declared so later packages share one ABI
 * instead of each inventing its own names.
 */
int plat_ctx_bind(plat_ctx_t *c, plat_ctx_type_t type,
                  uint32_t attach_id, plat_owner_t owner);
int plat_ctx_note_degraded(plat_ctx_t *c);
int plat_ctx_drain(plat_ctx_t *c);
int plat_ctx_kill(plat_ctx_t *c);
include/platx/context_watch.h
/* platx/context_watch.h — O16-21: context loss degrades its attachment.
 *
 * A context death is not a session disconnect and not a target death.
 * When the host-local context a runtime lives in goes away, any attachment
 * bound to it must stop claiming ACTIVE — a capability request into a dead
 * context would otherwise land in a dead runtime. The attachment goes
 * DEGRADED (via the public attach API), never a silent DETACHED and never
 * left ACTIVE. The generation is NOT bumped: the target may still be alive;
 * only the context is gone. That is target-loss's job, not this file's.
 *
 * Separate header on purpose: context.h (the context table) is not touched.
 */

/*
 * Bind a context id to the attachment id that lives in it, and mark the
 * context live in the watcher. Re-binding refreshes the pair. This is the
 * watcher's own view of the binding; when ctx_bind (O16-15) lands it can
 * feed this or replace it. ctx==0 or attach==0 → -1.
 */
int plat_ctx_watch_bind(plat_context_id_t ctx, plat_attach_id_t attach);

/*
 * The context is gone. Mark it not-live and, if a bound attachment is
 * ACTIVE, move it ACTIVE→DEGRADED through plat_attach_transit (public API).
 * Never DETACHED, never left ACTIVE, never gen++. With no bound attachment,
 * only the context is marked lost; no attachment is touched. Returns 0.
 */
int plat_ctx_note_lost(plat_context_id_t ctx);

/* 1 while the watcher considers this context live; 0 after note_lost or if
 * the watcher never saw it. */
int plat_ctx_is_live(plat_context_id_t ctx);

/* Forget all watcher state (tests). */
void plat_ctx_watch_reset(void);
05

core

Ядро платформы: запуск, ABI, жизненный цикл, владение и консоль
src/core/Управление и контракты145 файлов82 API headers

Core — минимальная доверенная машина PLATX. Он компилирует descriptor graph, управляет instances, сериализует lifecycle/recovery, владеет resources/tasks/events/capabilities, предоставляет XIO/console/diagnostics и гарантирует упорядоченный boot/shutdown.

Граница ответственности

  • Core не содержит protocol, storage business logic, plugin payload или sensor rules.
  • Optional subsystems подключаются public contracts; отсутствие их source не меняет Core ABI.
  • Глобальные registries малы, bounded и имеют отдельные locks/lifetime rules.

Устройство подсистемы

  • Boot kernel проверяет config/manifest/profile, строит graph и выполняет stages до READY.
  • Instance kernel связывает descriptor, owner generation, lifecycle mailbox, recovery state, resource/task scopes и published services.
  • Operation kernel объединяет command authz, typed ops, correlation, audit и event delivery.
  • Shutdown сначала закрывает admission, quiesce dependents, revoke services, join tasks, release resources и destroy reverse graph.

Поток работы

  • Config/profile → descriptor discovery/validation.
  • Graph plan → create/start batches.
  • Runtime intents/events → serialized instance executor.
  • Shutdown intent → reverse plan → evidence summary.

Отказ и восстановление

  • Hook timeout не освобождает instance до подтверждённого termination.
  • Boot failure запускает deterministic rollback только созданных/запущенных nodes.

Основные возможности

  • Staged boot, descriptor validation, capability/resource/task/event registries.
  • Lifecycle and persistent recovery watches implement the one-brain model.
  • Child host/sandbox/IPC proxy enforce generation-aware out-of-process modules.
  • Command authz/dispatch, diagnostics, shutdown/quiesce and profile checks live here.

Управление и диагностика

Корневые команды: help, ping, mode, echo, section, status, version, ops, subsys, platform. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / core →
Состав подсистемы / 145 файлов
Файл / компонентНазначение и граница
src/core/agent_inventory.cINT-143: central agent inventory.
src/core/args.cВспомогательные функции для разбора аргументов Реализация утилит для разбора опций командной строки.
src/core/args.hВспомогательные функции для разбора аргументов командной строки Набор утилит для упрощённого разбора опций командной строки. Поддерживаются два формата: --option=value (опция со значением через '=') --option value (опция со значением как отдельный аргумент)
src/core/audit_mbus.cINT-117: Audit → MBus backpressure forwarder.
src/core/audit_schema.cStructured audit schema (INT-112).
src/core/batch.csingle-shot and script batch front-end. ./memfd --batch --command "cmd arg1 arg2" ./memfd --batch --script - Lines starting with '#' are printed as section comments (not executed). - Blank lines separate sections visually (printed as empty lines). - Every executed command is echoed as "[memfd]$ " before running,
src/core/builtin_commands.cВстроенные команды платформы Реализация встроенных команд, доступных во всех режимах работы.
src/core/cdata2.cCDATA v2 self-describing container Wire format (single line, no whitespace): [encoding:compression:encryption:key:hash_algo:hash:size_enc;size_raw]data encoding = hex | base64 | 0 compression = gzip | 0 encryption = rc4 | chacha20 | aes256gcm | 0 key = default | krh | 0 |
src/core/cli.ccommand line front-end for the memfd manager. Also contains cdata v1 helpers (simple binary container used only by the legacy CLI client — not to be confused with cdata2 which powers the "memfd cdata" command namespace).
src/core/cmd_authz.cSeen-nonce ring: a fixed-size circular buffer of (nonce, scope_hash) pairs. On ALLOW the pair is appended; the oldest entry is evicted when the ring is full. The ring is not persisted: after a restart, replay within the previous ring window is theoretically possible; callers
src/core/cmd_autoreg.hавто-регистрация подсистем по наличию их исходников. Для каждой подсистемы: если её .c существует в дереве (__has_include по относительному пути от src/core/), объявляем регистратор и REG_x() его вызывает; если файла нет — REG_x() превращается в no-op. Ручной
src/core/cmd_dispatch.cINT-148: typed request dispatcher.
src/core/cmd_plat_ops.cCLI for the operator listen. Session verbs stay on ra2c.
src/core/cmd_subsys.cCLI namespace "subsys" для реестра подсистем платформы. subsys list список всех зарегистрированных подсистем subsys count количество подсистем в реестре subsys health проверка здоровья всех подсистем (exit-код != 0 если есть проблемы)
src/core/compat.hsmall portability shims (non-POSIX helpers).
src/core/compat_handshake.cINT-142: ABI compatibility handshake. The server owns a capability table (its own ABI version + what it can serve). plat_compat_check walks the agent's offer, negotiates each capability to min(server_version, agent_version), and rejects if the negotiated version would be below the agent's stated minimum.
src/core/console.ccommand registry, line tokeniser, and dispatch core. This file is mode-agnostic: it knows nothing about REPL, IPC, or batch. It only knows how to store commands, parse a text line, and call handlers.
src/core/console.huniversal command-dispatch interface for prometheus. Three front-ends share a single command table and a single handler signature; only the I/O path differs between modes. REPL — interactive readline loop on stdin/stdout IPC — line-oriented Unix-socket server (one client per connection)
src/core/corr_id.cCorrelation ID (INT-111).
src/core/data_encoding.inc--------------------------- hex ---------------------------
src/core/diag_endpoint.cAuthenticated read-only diag endpoints (INT-116).
src/core/dsl_fs_action.cDSL typed FS actions with rollback (INT-098).
src/core/dsl_route_txn.cDSL config transaction for routes/limits (INT-107).
src/core/dsl_sensor_txn.cINT-129: DSL sensor profile transaction + rollback.
src/core/ebpf_cap_loader.cINT-123: eBPF loader behind capability lease.
src/core/elf_provenance.cELF provenance via XIO claim (INT-092).
src/core/fabric_delivery.cINT-139: Fabric artifacts → RA2C/MBus via weak hook.
src/core/fabric_snap.cINT-135: process.snapshot:v1 typed result + vault storage. vault_artifact_write is weak; absent = refuse, no side-effects.
src/core/flow_hades_enrich.cINT-126: Flow metadata enrichment for Hades events.
src/core/fsx_events.cFSX events → Audit/MBus (INT-097).
src/core/fuse_lifecycle.cFUSE lifecycle with Core resources (INT-096).
src/core/hades_alert_msx.cINT-128: Hades alerts → MSX queue, quota+dedup.
src/core/hades_bpf_owner.cINT-122: BPF fd ownership table for Hades.
src/core/hades_context_corr.cINT-125: Hades + Fabric Context correlation.
src/core/hades_event_schema.cINT-124: Versioned Hades event → Audit/MBus.
src/core/hades_sensor_profile.cINT-121: Single active Hades sensor profile.
src/core/hook_hades_ctx.cINT-127: Hook async context via Hades/Flow observer.
src/core/incident_env.cINT-145: incident result envelope.
src/core/lock_rank.cW8-02 / S352: the dynamic half of the lock-order check. Per-thread, because lock order is a per-thread property: what matters is the sequence one thread acquires locks in, not what the process holds in aggregate. The held set therefore lives in thread-local storage and needs
src/core/log_severity.cLog severity + Audit bridge (INT-113).
src/core/main.cТочка входа в исследовательскую платформу Данный модуль реализует точку входа в программный комплекс для исследования системных механизмов операционной системы Linux. Платформа предназначена для образовательных и научно-исследовательских целей в области системного программирования.
src/core/mbus_ra2c_adapter.cMBus actors via public protocol.ra2c:v1. Does not include src/transport/ra2c_*. Weak emit/push must not return success: absence is NOLINK. Queue is bounded; overflow is backpressure. A2-P04 invariants held here: I1 route() admits, it does not deliver. The two facts are counted
src/core/memfd.hshared declarations for the extended memfd utility. The utility is split into a manager daemon that owns the memfd objects and a thin CLI front-end that talks to it over a Unix domain socket. File descriptors are handed between the two processes with SCM_RIGHTS
src/core/memfd_cap.cMemfd manager as capability provider (INT-091).
src/core/memfd_seal_policy.cMemfd seal policy (INT-093).
src/core/mesh_health.cMesh peer health + transport availability (INT-105).
src/core/msx_lease_shim.cINT-138: MSX/DSL through Lease only. Does not call msx_engine.c or msx_mod.c; all refs are weak.
src/core/msx_net_metrics.cMSX read network/flow metrics (INT-108).
src/core/plat_abi.cCore primitive pointers for EMBEDDED modules.
src/core/plat_abi_proxy.cchild-side plat_abi_t. Only public headers. require/provide/revoke + res + event->emit serialize over one CALL frame. task/diag stay NULL — see abi_proxy.h. xio is get() only; own_fd / release_owner / stats_text stay NULL.
src/core/plat_alert.cреализация. Договор — в include/platx/alert.h.
src/core/plat_atomic_file.cреализация. Договор — в include/platx/atomic_file.h.
src/core/plat_bundle.cреализация. Договор — в include/platx/bundle.h.
src/core/plat_capreg.cworker capability table. Pointer identity owns a slot. Revoke removes the name; a dead child proxy must not stay findable.
src/core/plat_capreq.cреализация. Договор — в include/platx/capreq.h.
src/core/plat_cfgkey.cреализация. Договор — в include/platx/cfgkey.h.
src/core/plat_chain.cреализация. Договор — в include/platx/chain.h.
src/core/plat_child_host.cChild death is a process event: emit the fact, revoke provides, then plat_recovery_apply on the live watch row. Recovery decides; lifecycle alone runs hooks (host start respawns the process). Never a stack plat_recovery_t — budget lives on the watched instance.
src/core/plat_child_ipc.cframing only. send/recv go through XIO.
src/core/plat_child_landlock.cLandlock FS allowlist for PLAT_ISO_CHILD. Invariant: restrict after NEWUSER/NEWNS and NNP, before seccomp/exec. Namespaces first so uid_map and the mount view are already the child's. NNP is required by restrict_self (or userns CAP_SYS_ADMIN); we set it before this call so exec cannot regain host privileges and then escape.
src/core/plat_child_landlock.hCHILD FS allowlist. Core-internal. After NEWUSER/NEWNS and NNP, before seccomp/exec. Missing API → -1.
src/core/plat_child_sandbox.cnamespaces + landlock + seccomp-bpf for PLAT_ISO_CHILD. Self-apply only. Not a live-worker injector, not a second supervisor. Order: unshare → PR_SET_NO_NEW_PRIVS → landlock → seccomp. Namespaces first: NEWUSER maps and unshare need /proc writes and the pre-NNP cap set. NNP next so exec cannot regain host privileges, and
src/core/plat_child_sandbox.hCHILD namespaces + landlock + allowlist. Core-internal. Call only in the child after fork, before exec. Not a supervisor. Order: unshare → NO_NEW_PRIVS → landlock → seccomp. Missing any → -1.
src/core/plat_childrec.cреализация. Договор — в include/platx/childrec.h.
src/core/plat_compstat.cреализация. Договор — в include/platx/compstat.h.
src/core/plat_crashpoint.cреализация. Договор — в include/platx/crashpoint.h.
src/core/plat_cryptopol.cреализация. Договор — в include/platx/cryptopol.h.
src/core/plat_ctlmsg.cреализация. Договор — в include/platx/ctlmsg.h.
src/core/plat_ctx.hCore-only. Modules see an incomplete plat_module_ctx_t.
src/core/plat_dep_graph.hCore-private. Bindings with a lifetime, and the view over them. Not a fifth registry: everything here is stored in, or derived from, the capability registry that already exists. The problem this answers. plat_cap_require() hands back the provider's vtable pointer and forgets the
src/core/plat_desc.cреализация. Договор — в include/platx/desc.h.
src/core/plat_epoch.cреализация. Договор — в include/platx/epoch.h.
src/core/plat_event.cin-process fan-out. Not a bus, not JSON, not durable. Bounded ring. emit copies and returns; overflow drops oldest + counter. Волна 5 (A1-P05). Изменения относительно baseline 9139 B — каждое закрывает дефект, воспроизведённый на неизменённом файле (A1-P05-202):
src/core/plat_fleet_plan.cCommit only after every validation above. A refusal leaves all state, including idempotency history, byte-for-byte unchanged.
src/core/plat_gencommit.cреализация. Договор — в include/platx/gencommit.h.
src/core/plat_jrec.cреализация. Договор — в include/platx/jrec.h.
src/core/plat_keyrot.cреализация. Договор — в include/platx/keyrot.h.
src/core/plat_keystate.cреализация. Договор — в include/platx/keystate.h.
src/core/plat_layer.clayer names and name→layer guess table.
src/core/plat_layer.hinit/start waves of already-registered subsys. Why this axis exists: a later wave must not run after a mandatory on a dead wave). Not platx_boot_stage (process gates) and not requires[] (edges inside one wave). layer — which wave; on_layer. runs once after that wave's
src/core/plat_lifecycle.cLifecycle transitions call module hooks and therefore must be serialized as one transaction, not merely publish the final state atomically. A global lock deliberately avoids changing the public lifecycle struct/ABI.
src/core/plat_lifecycle_serial.cA1-P03: identity, decisions, admission, deadline. Everything here is a fixed table under one leaf mutex. No allocation, no descriptor hook, no callback, and nothing that blocks while the mutex is held — plat_lc_admit_drain() is the only entrypoint that waits at all, and
src/core/plat_logrot.cреализация. Договор — в include/platx/logrot.h.
src/core/plat_ops.cthin operator listen over the process RA2C session. No keyed session → send/cmd/switch/recv refuse with the REPL text. GET /gadget/explain?cap= — C-vertical candidates via plat_gadget_explain. Weak symbols: gadget.o missing → 501, not 200 with a fake catalog.
src/core/plat_ops.hoperator API over the live RA2C session in this process. Calls the existing ra2c CLI. Does not own a session and does not decide.
src/core/plat_ops_cap.cplat_ops via capability APIs (INT-115).
src/core/plat_pluginset.cреализация. Договор — в include/platx/pluginset.h.
src/core/plat_preflight.cinstalled, read-only host suitability probe.
src/core/plat_preflight.hRead-only install preflight. This entry point must run before platform initialization, audit creation, worker filters, threads, or plugins.
src/core/plat_privcheck.cреализация. Договор — в include/platx/privcheck.h.
src/core/plat_provgen.cреализация. Договор — в include/platx/provgen.h.
src/core/plat_reason.cреализация. Договор — в include/platx/reason.h.
src/core/plat_reclog.cреализация. Договор — в include/platx/reclog.h.
src/core/plat_recovery.cdecides. Lifecycle alone runs hooks.
src/core/plat_replay.cреализация. Договор — в include/platx/replay.h.
src/core/plat_resilience.cреализация. Договор — в include/platx/resilience.h.
src/core/plat_resource.cопубликованный plat_resource_api_t поверх настоящего ядра. ЧТО ЗДЕСЬ ИЗМЕНИЛОСЬ И ПОЧЕМУ Раньше это был весь plat_resource: статическая таблица, четыре функции и `release`, который делал memset строки. Ни одного close(), ни одного callback — дескриптор, ради учёта которого строка заводилась, оставался
src/core/plat_resource_owner.cA1-P04: ядро владения ресурсом, которое действительно освобождает ресурс. До этого файла plat_res_api.release() обнулял строку таблицы и возвращал 0. Дескриптор при этом оставался открытым. Отчёт «остатков нет» был верен ровно в том смысле, что в таблице действительно ничего не осталось; в системе
src/core/plat_span.cреализация. Договор — в include/platx/span.h.
src/core/plat_subsys_adapt.cHost-side wrapper so legacy subsys_ops go through the platx descriptor. Supervisor must not call these hooks itself.
src/core/plat_subsys_adapt.hhost adapter: legacy subsys_ops as a platx descriptor. Only lifecycle calls the hooks. Supervisor requests transitions.
src/core/plat_task.cEMBEDDED pthreads belong to (module, instance, generation). ЧТО ИСПРАВЛЕНО В ЭТОЙ ВЕРСИИ (A1-P04) 167 лимит задач из профиля применяется ДО pthread_create, отдельным кодом отказа: «политика запрещает» и «мест нет» лечатся по-разному. 168 размер стека и guard page проверяются по политике, а не берутся
src/core/plat_task.sync-conflict-20260912-171405-APFVAKO.cEMBEDDED pthreads belong to (module, instance, generation). ЧТО ИСПРАВЛЕНО В ЭТОЙ ВЕРСИИ (A1-P04) 167 лимит задач из профиля применяется ДО pthread_create, отдельным кодом отказа: «политика запрещает» и «мест нет» лечатся по-разному. 168 размер стека и guard page проверяются по политике, а не берутся
src/core/plat_txn.cреализация. Договор — в include/platx/txn.h.
src/core/plat_watchdog.cнезависимый сторож (A1-P09-421..430). Разбор того, ЧТО именно здесь чинится, — в include/platx/watchdog.h. Здесь только про устройство. Три свойства, ради которых модуль написан, и как каждое достигается: 1. Независимость от наблюдаемого (421, 423). Свой pthread. Ни одного
src/core/plat_watchdog_bind.cпривязка независимого сторожа к платформе (A1-P09-421..425, 448). ЧТО ЭТОТ ФАЙЛ ДЕЛАЕТ И ЧТО НАМЕРЕННО НЕ ДЕЛАЕТ Делает: заводит сторожа на собственном потоке, даёт ему НЕБЛОКИРУЮЩЕГО наблюдателя поверх plat_task и переносит опубликованные факты во ВХОД СУЩЕСТВУЮЩЕГО восстановления.
src/core/plat_wire.cреализация. Договор — в include/platx/wire.h.
src/core/plat_worker_sandbox.cseccomp-bpf allowlist for the WORKER process. continue boot. Self-apply only. Not CHILD, not ptrace inject. Worker is the Hades parent: it loads sensors (bpf_prog_load / map create / perf_event_open + ioctl SET_BPF, see src/ebpf/hades/core/ebpf_loader.c).
src/core/plat_worker_sandbox.hWORKER allowlist. Core-internal. Call only in the worker after XIO is up, before domain modules run. Self-apply. Not CHILD, not inject, not a second supervisor.
src/core/plat_xio_door.hshared ABI-gate helper for "one door" XIO dispatch. Include this in a translation unit that implements typed XIO dispatch, AFTER platx/abi.h and the unit's own weak redeclarations of plat_abi_core and xio_submit. The helper is header-only (static inline)
src/core/plat_zeroize.cограниченная очистка Core-owned контекстов Разбор обещаний и границ — в include/platx/zeroize.h. Здесь устройство.
src/core/platform.cконтекст платформы, резолюция режимов, владение планировщиком.
src/core/platform.hконтекст платформы: единая точка владения жизненным циклом, режимами подсистем и планировщиком. Первый шаг ухода от глобальных синглтонов к явному контексту (platform_ctx). Режим (role) — свойство КАЖДОЙ подсистемы по отдельности, не процесса: EMBEDDED — состояние живёт в этом процессе (in-process);
src/core/platform_init.cboot orchestrator of the platx kernel (see platx/kernel.h). Actual order (platx_boot_stage_t, walk 0→5, no jump): 0 KERNEL register + Core ABI; calls linked xio/log/taskmgr 2 MANIFEST on_before_core (deploy bootstrap.dsl) then modules.stage then KERNEL layer init+start via subsys_start_all
src/core/platform_init.hпубличный интерфейс инициализации платформы.
src/core/platform_io.hplatform.c's one door for typed I/O. Private to src/core. In a header rather than in the .c for one reason: the door is static, so the only way to examine it is to include the translation unit that holds it, and platform.c does not stand alone -- it reaches the
src/core/platform_subsys.cрегистрация всех платформенных подсистем в реестре. Модули, реализующие полные subsys_ops_t (xio, log, plugin, vault, platform, keyring, ...), вносят себя в реестр сами — через subsys_register() из своих register_*_commands() или init-функций. Этот файл регистрирует все ОСТАЛЬНЫЕ (известные, но ещё не
src/core/platx_config.cSearch (first existing file wins; a present-but-broken file does not fall through — that would be a silent half-config): 1. explicit path / $PLATX_CONF 2. vfs:/cfg/platx.conf (packed archive, after mount) 3. embed/platx.conf (source tree / cwd) 4. embed/config/platx.conf
src/core/platx_config.hCall platx_config_load() from platform_initialize() at CONFIG (after plugin_mount_embedded + vfs_cfg_apply, before DSL / core boot). Missing or broken mandatory keys → return != 0; do not set ready.
src/core/platx_safe.cWave 7 helpers. One file, one contract: refuse, don't clip.
src/core/platx_stage_table.cparse embed/modules.stage, then require names. Parse rules (one row = one module; this is not a supervisor): - UTF-8 text. '#' to EOL and blank lines are ignored. - Fields: stage | module | mandatory (exactly two '|'). - stage ∈ {kernel, system, net, protect, optional}.
src/core/platx_stage_table.hpolicy table from vfs:/modules.stage. Presence check only. Does not register, start, or promote stubs. Call from platform_initialize at MANIFEST after DSL deploy (vfs_hooks). Missing table or missing mandatory name → return != 0; platx_stage_table_ready stays 0 (that is not platform_ready()).
src/core/policy/policy_dist.cINT-144: signed policy bundle distribution.
src/core/policy_dist.cINT-144: signed policy bundle distribution.
src/core/profile_check.cINT-141: profile minimum capability contract. Walk the descriptor's required[] list. For each entry call plat_abi_require(capability, min_version). A NULL return means the capability is absent or version-incompatible → profile not READY. plat_abi_require is weak: a test that does not link the full ABI
src/core/profile_wire.cA1-P02: portable load-time profile decoder. Contract notes that are easy to lose in review: P02-061 The only length accepted for the base image is exactly PLAT_PROFW_SIZE. Every shorter prefix is refused before a single field is read, so a truncated blob cannot overread.
src/core/proto.cРеализация proto
src/core/repl.cinteractive REPL front-end for prometheus. Invoked by: ./prometheus --console=REPL_MODE Uses GNU readline (persistent history, tab completion) when built with -DHAVE_READLINE -lreadline. Falls back to plain fgets otherwise. The write callback sends output to stdout.
src/core/sched.cреализация планировщика (фоновый поток + таймеры).
src/core/sched.hреальный планировщик задач платформы. Фоновый поток с очередью таймеров (CLOCK_MONOTONIC). Задачи бывают периодические (period_ms > 0) и одноразовые (period_ms == 0). Callback вызывается ВНЕ внутреннего мьютекса планировщика, поэтому может сам дёргать API платформы; для сериализации с главным потоком используйте
src/core/server.cthe memfd manager. Owns all live memfd objects and serves requests from the CLI over a Unix domain socket.
src/core/shutdown_quiesce.cShutdown quiesce order (INT-109). Order: Flow → MBus drain → Transport stop → FSX unmount → FUSE → VFS.
src/core/startup_barrier.cA1-P02: trusted startup readiness barrier. All state is file-static and single-threaded by construction: startup runs on one thread before any task exists. Nothing here takes a lock, and nothing here is safe to call after READY except the read-only The invariant every function below preserves:
src/core/subsys.clightweight subsystem registry implementation. Thread safety: subsys_register/revoke are protected by a mutex. subsys_find/list acquire the same mutex (short critical section). ops pointers are stable — callers may cache them.
src/core/subsys.hunified subsystem/plugin interface for the memfd platform. DESIGN (see docs/ARCH.md for rationale) Every subsystem — built-in (mbus, mesh, http, ...) or dynamically loaded plugin — implements subsys_ops_t. A thin registry (subsys_register / subsys_find / subsys_list) replaces the dual platform_provide + plugin_manager
src/core/supervisor_graph.cSupervisor module graph (INT-114).
src/core/trace_xio.cINT-118: Thin XIO wrappers over trace subsystem.
src/core/transport_cap.cTransport capability ABI (INT-101).
src/core/uipc_transport.cUIPC as fd-transfer transport (INT-103).
src/core/uring_xio_future.cUring completions → XIO futures (INT-104).
src/core/util.csmall shared helpers: logging, full read/write, codecs.
src/core/vault_vfs.cVault secret as read-only VFS node (INT-095).
src/core/vfs_context_tag.cContext identity → VFS/FSX artifacts (INT-099).
src/core/vfs_health_view.cINT-119: VFS health/audit_tail/supervisor snapshots.
src/core/vfs_stream.cVFS streaming API for RA2C (INT-094).
Контракты API / 82 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/abi.h
/* platx/abi.h — Core primitives only. Domain APIs come from require().
 *
 * EMBEDDED: this pointer is the real Core.
 * CHILD:    the same layout, implemented by a serializing proxy.
 */
typedef struct plat_module_ctx plat_module_ctx_t;

typedef struct plat_abi {
    uint32_t abi_version;   /* PLATX_CORE_ABI */
    uint32_t struct_size;   /* sizeof(plat_abi_t) */

    void *(*require)(plat_module_ctx_t *m, const char *capability,
                     uint32_t version);
    int   (*provide)(plat_module_ctx_t *m, const char *capability,
                     uint32_t version, const void *vtable);
    int   (*revoke)(plat_module_ctx_t *m, const char *capability);

    const plat_resource_api_t *res;
    const plat_task_api_t     *task;
    const plat_event_api_t    *event;
    const plat_xio_api_t      *xio;
    const plat_diag_api_t     *diag;
} plat_abi_t;

/* Fill Core primitive pointers. Domain vtables stay on require(). */
int plat_abi_init(plat_abi_t *abi);
/* Worker boot: res + task tables, then a process-wide EMBEDDED ABI. */
int plat_abi_core_init(void);
const plat_abi_t *plat_abi_core(void);

/* EMBEDDED modules without create_args: same table as abi->require.
 * CHILD must use the proxy on args->abi, not these. Fail closed if
 * Core ABI is down — no second registry. */
void *plat_abi_require(const char *capability, uint32_t version); /* inline interface */

int plat_abi_provide(const char *capability, uint32_t version,
                                   const void *vtable); /* inline interface */

int plat_abi_revoke(const char *capability); /* inline interface */

/* Worker-side registry behind abi->require/provide/revoke. */
void *plat_cap_require(plat_module_ctx_t *m, const char *capability,
                       uint32_t version);
int   plat_cap_provide(plat_module_ctx_t *m, const char *capability,
                       uint32_t version, const void *vtable);
/* Revoke the first named slot owned by this exact context; NULL is the trusted
 * Core override. Returns -1 if no eligible slot exists, without side effects. */
int   plat_cap_revoke(plat_module_ctx_t *m, const char *capability);
int   plat_cap_init(void);
void  plat_cap_fini(void);
int   plat_cap_has(const char *capability, uint32_t version);
int   plat_cap_revoke_ctx(plat_module_ctx_t *m);
include/platx/abi_contract.h
/* platx/abi_contract.h — A1-CONTRACT: early public compatibility contract.
 *
 * A1-P01-012/013/014/015. This header is the one place a consumer may look
 * BEFORE it dereferences anything a producer handed it.
 *
 * Three properties are deliberate and load-bearing:
 *
 *  1. It is header-only and pure. No Core symbol is referenced, nothing is
 *     linked, no allocation, no syscall. A consumer that only wants to say
 *     "is this table shaped the way I was compiled against" must not have to
 *     link Core to ask.
 *
 *  2. It is portable. Only <stdint.h> and <stddef.h>. It does NOT include
 *     platx/abi.h, platx/xio_api.h or anything that reaches <sys/socket.h>,
 *     because A3 (platx-windows) has to compile the same fixtures without a
 *     Linux tree and without a Windows provider being ready (A1-P01-014).
 *
 *  3. It carries no domain tables. Only the two header words every PLATX
 *     versioned table starts with, and the classification of an owner-gated
 *     result. Capabilities stay on require() (invariants.h #3, #4).
 *
 * THE HEADER-WORD CONTRACT
 * ------------------------
 * Every versioned PLATX table — plat_abi_t today, and any Core/XIO/profile
 * table that wants this negotiation — begins with exactly:
 *
 *     uint32_t <something>_version;   / * major<<16 | minor  * /
 *     uint32_t struct_size;           / * sizeof(that struct) * /
 *
 * plat_abi_t already does (include/platx/abi.h). That prefix is what makes a
 * safe peek possible: reading the first eight bytes of a table is defined
 * even when the rest of the layout is a version the reader has never seen.
 *
 * RULE. Compatibility is decided by three questions, in this order:
 *   - major equal?            no  -> PLAT_ABI_ERR_MAJOR   (never call)
 *   - producer minor >= mine? no  -> PLAT_ABI_ERR_MINOR   (never call)
 *   - producer size   >= mine? no -> PLAT_ABI_ERR_SIZE    (never call)
 * A larger producer struct is accepted: the consumer's fields are a prefix.
 * A smaller one is refused even at the same version, because the trailing
 * slots the consumer was compiled to read are not there.
 */

/* Contract revision of THIS header. Bumped when the rules below change,
 * independently of PLATX_CORE_ABI which versions the Core table itself. */
#define PLATX_ABI_CONTRACT_REV  1u

#define PLAT_ABI_MAJOR_OF(v)  ((uint32_t)(((uint32_t)(v)) >> 16))
#define PLAT_ABI_MINOR_OF(v)  ((uint32_t)(((uint32_t)(v)) & 0xFFFFu))
#define PLAT_ABI_MAKE(ma, mi) ((uint32_t)((((uint32_t)(ma)) << 16) | \
                                          (((uint32_t)(mi)) & 0xFFFFu)))

/* The safe-peek prefix. Layout-compatible with the first two members of
 * plat_abi_t and of every table that opts into this contract. */
typedef struct plat_abi_header {
    uint32_t abi_version;
    uint32_t struct_size;
} plat_abi_header_t;

/* Negotiation verdicts. Distinct values: a caller must be able to log which
 * of the three refusals happened without re-deriving it. */
typedef enum plat_abi_verdict {
    PLAT_ABI_OK        =  0,
    PLAT_ABI_ERR_NULL  = -1,  /* no table at all */
    PLAT_ABI_ERR_MAJOR = -2,  /* different major: layout is not comparable */
    PLAT_ABI_ERR_MINOR = -3,  /* producer older than the consumer needs */
    PLAT_ABI_ERR_SIZE  = -4   /* producer struct shorter than consumer's */
} plat_abi_verdict_t;

/* The whole negotiation, on the two header words only.
 *
 * want_version / want_size are what the CONSUMER was compiled against —
 * normally PLATX_CORE_ABI and sizeof(plat_abi_t) at the consumer's own
 * compile time. Nothing beyond the first eight bytes of `hdr` is touched, so
 * this is defined even when the producer is a future layout. */
plat_abi_verdict_t
plat_abi_check_header(const plat_abi_header_t *hdr,
                      uint32_t want_version, size_t want_size); /* inline interface */

/* Same check from an opaque pointer. A consumer holding only `void *` — a
 * CHILD proxy, a loader, a Windows fixture with no plat_abi_t in scope —
 * still reads a defined prefix. */
plat_abi_verdict_t
plat_abi_check_opaque(const void *table, uint32_t want_version, size_t want_size); /* inline interface */

const char *plat_abi_verdict_str(plat_abi_verdict_t v); /* inline interface */

/* ── A1-P01-013: one owner-result vocabulary ──────────────────────────────
 *
 * The tree already refuses in several dialects: XIO returns named negatives
 * (platx/xio_err.h), plat_task_api_t returns -1 and names the state in errno
 * (platx/task.h), plat_resource_api_t returns 0/-1. A consumer that has to
 * decide "retry, give up, or clean up" must not need three switch statements
 * that each know one dialect.
 *
 * These are the five outcomes that decision actually has. They are declared
 * here, once, and the classifiers below are the only mapping. Adding a sixth
 * outcome is an ABI change, not a local convenience.
 */
typedef enum plat_owner_result {
    /* The call did what it said. */
    PLAT_OWNER_OK       = 0,
    /* The request was malformed, or the epoch/handle is not a thing:
     * retrying with the same arguments changes nothing. */
    PLAT_OWNER_ARGS     = 1,
    /* The object exists but is owned by someone else, or by an older
     * generation of this owner. NOTHING was read, written or closed.
     * generation 0 is never an owner: it is refused here, not swept. */
    PLAT_OWNER_STALE    = 2,
    /* Someone else already holds the operation (a concurrent join, a
     * claimed slot). Retry may succeed once they are done. */
    PLAT_OWNER_BUSY     = 3,
    /* Requested, not finished. The slot is deliberately kept: a cancel that
     * was delivered, a bounded wait that expired, an inflight submit.
     * This is NOT a terminal state and must never be reported as success. */
    PLAT_OWNER_PENDING  = 4,
    /* The subject is gone for good — exited, released, closed. Its slot is
     * cleared. Cleanup is complete; there is nothing left to reap. */
    PLAT_OWNER_TERMINAL = 5
} plat_owner_result_t;

const char *plat_owner_result_str(plat_owner_result_t r); /* inline interface */

/* XIO dialect -> vocabulary. `rc` is what xio_submit() returned: >= 0 is a
 * byte count, negatives are platx/xio_err.h codes. The numeric codes are
 * repeated here rather than included, so that this header stays free of
 * <sys/socket.h> via xio_api.h; the contract test asserts they still agree
 * with platx/xio_err.h (tests/contract/platx_abi_contract_test.c). */
#define PLATX_CONTRACT_XIO_ERR_SUBMIT      (-1001)
#define PLATX_CONTRACT_XIO_ERR_COMPLETE    (-1002)
#define PLATX_CONTRACT_XIO_ERR_UNAVAIL     (-1003)
#define PLATX_CONTRACT_XIO_ERR_CANCEL      (-1004)
#define PLATX_CONTRACT_XIO_ERR_OWNER_STALE (-1005)
#define PLATX_CONTRACT_XIO_ERR_TIMEOUT     (-1006)
#define PLATX_CONTRACT_XIO_ERR_ARGS        (-1007)

plat_owner_result_t plat_owner_from_xio(long rc); /* inline interface */

/* Task dialect -> vocabulary, for plat_task_api_t::join and ::cancel.
 * `rc` is the return value, `err` the errno it named (platx/task.h).
 * errno numbers are passed in by the caller so this header needs no <errno.h>
 * dependency on any particular platform's numbering; the contract test pins
 * them against the host <errno.h>. */
plat_owner_result_t
plat_owner_from_task(int rc, int err,
                     int e_eagain, int e_esrch, int e_ebusy, int e_edeadlk); /* inline interface */

/* Generation 0 is not an owner. Every own/claim path must refuse it up front
 * and every sweep path must treat it as a no-op — platx/xio_api.h says so for
 * own_fd and release_owner, platx/resource.h for release_owner. One predicate
 * so the rule is not re-implemented per call site. */
int plat_owner_gen_valid(uint32_t generation); /* inline interface */
include/platx/abi_proxy.h
/* platx/abi_proxy.h — child-side plat_abi_t. Same layout, IPC behind it.
 * Compile with -I include only.
 *
 * Proxied this cut: require/provide/revoke, res (own/release/check/
 * release_owner), event->emit. Host applies, then ACK; short payload /
 * unknown method / dead fd → error, no half-applied resource.
 *
 * NULL and why (do not fake success):
 *   task — entry is a function pointer; it cannot cross the socket.
 *          A host trampoline CALL deadlocks if spawn runs before serve()
 *          and would serialize child work on the serve thread. Proxy
 *          must not invent pthread.
 *   event->subscribe / unsubscribe — fn pointer, same reason as task.
 *          The event slot is live because emit is real; these methods
 *          fail closed (id 0 / -1), they do not publish a host slot.
 *   xio  — plat_xio_api.get() would return a worker xio_t*. Methods
 *          live in src/xio/; CHILD compiles with -I include only.
 *          Handing out host pointers is a half-state. Framing already
 *          uses XIO on the socket; that is not abi->xio.
 *   diag — host-local bootstrap log. Child has no subscriber.
 */
int  plat_abi_proxy_init(plat_abi_t *abi, int ipc_fd);
int  plat_abi_proxy_handshake(plat_abi_t *abi, int timeout_ms);
int  plat_abi_proxy_ready(plat_abi_t *abi);
/* Serve worker CALLs until the channel closes. */
int  plat_abi_proxy_serve(plat_abi_t *abi);
void plat_abi_proxy_fini(plat_abi_t *abi);
include/platx/action_provider.h
/* platx/action_provider.h — action_provider_v1 (D054).
 *
 * The only ABI in the fabric that changes state. Everything here exists to
 * make one thing impossible: an action that happened but cannot be described,
 * undone or attributed.
 *
 * The transaction is six-legged, and the legs are not optional:
 *   preview   what would change, and how far it reaches — without changing it
 *   prepare   reserve, fence and stage; nothing is visible yet
 *   commit    make it real, exactly once for a given idempotency key
 *   verify    read the world back and confirm it matches what was promised
 *   rollback  undo a prepared or committed action to its recorded prior state
 *   reconcile after a restart, decide what a half-finished transaction was
 *
 * Fencing: an action carries a lease. A lease that has been superseded may not
 * commit, even if it prepared successfully — that is the whole point of a
 * lease, and it is the difference between "the old owner is slow" and "there
 * are two owners".
 *
 * AI seams AI016-AI033 are contract-only here: the descriptor carries
 * `descriptor_hash`, a dry-run enum and `no_self_trigger` so that an advisory
 * plane can be attached later without reopening this ABI. No model runtime,
 * no inference and no self-triggered action may be implemented in this wave.
 *
 * ABI freeze: ACTION_PROVIDER_ABI 1 (D054 sealed 2026-09-01)
 */

#define ACTION_PROVIDER_ABI   1u

#define ACTION_TARGET_MAX   256u
#define ACTION_KEY_MAX       64u   /* idempotency key, printable */
#define ACTION_BLAST_SLOTS   16u   /* enumerated affected classes */

/* ── What an action does ─────────────────────────────────────────────────── */
typedef enum action_verb {
    ACTION_VERB_NONE       = 0,
    ACTION_VERB_QUARANTINE = 1,   /* move a file out of reach, reversibly */
    ACTION_VERB_FREEZE     = 2,   /* stop a process without killing it */
    ACTION_VERB_ISOLATE    = 3,   /* cut network reachability */
    ACTION_VERB_REVOKE     = 4,   /* withdraw a credential or lease */
    ACTION_VERB_COLLECT    = 5,   /* seal evidence; changes only the store */
    ACTION_VERB_MAX
} action_verb_t;

/* ── Dry-run ladder (AI seam AI017) ──────────────────────────────────────
 * PLAN never touches the system. SHADOW performs every check and reservation
 * that COMMIT would, then releases them. LIVE is the only value that changes
 * anything, and it is the only one an audit record may call an action.
 */
typedef enum action_mode {
    ACTION_MODE_PLAN   = 0,
    ACTION_MODE_SHADOW = 1,
    ACTION_MODE_LIVE   = 2,
    ACTION_MODE_MAX
} action_mode_t;

/* ── Transaction state (D097) ────────────────────────────────────────────── */
typedef enum action_state {
    ACTION_ST_NONE      = 0,
    ACTION_ST_PREVIEWED = 1,
    ACTION_ST_PREPARED  = 2,
    ACTION_ST_COMMITTED = 3,
    ACTION_ST_VERIFIED  = 4,
    ACTION_ST_ROLLED    = 5,
    ACTION_ST_FAILED    = 6,
    ACTION_ST_MAX
} action_state_t;

/* ── Blast radius (D093) ─────────────────────────────────────────────────── */
typedef enum action_blast_class {
    ACTION_BLAST_NONE      = 0,
    ACTION_BLAST_FILE      = 1,
    ACTION_BLAST_DIRECTORY = 2,
    ACTION_BLAST_PROCESS   = 3,
    ACTION_BLAST_PROC_TREE = 4,
    ACTION_BLAST_SOCKET    = 5,
    ACTION_BLAST_NETNS     = 6,
    ACTION_BLAST_HOST      = 7,   /* host-wide effect; requires explicit grant */
    ACTION_BLAST_MAX
} action_blast_class_t;

typedef struct action_blast_entry {
    uint32_t klass;       /* action_blast_class_t */
    uint32_t count;       /* how many of that class are touched */
    uint32_t reversible;  /* 1 = rollback restores this class exactly */
    uint32_t _pad;
} action_blast_entry_t;

/* ── The action descriptor ───────────────────────────────────────────────
 * `descriptor_hash` pins the descriptor a decision was made against: commit
 * refuses a descriptor whose hash differs from the previewed one, so a plan
 * approved for one target cannot be executed against another (AI018).
 */
typedef struct action_descriptor {
    uint32_t abi_version;
    uint32_t struct_size;

    uint32_t verb;                    /* action_verb_t */
    uint32_t mode;                    /* action_mode_t */

    char     target[ACTION_TARGET_MAX];   /* canonical, provider-specific */
    char     idempotency_key[ACTION_KEY_MAX];

    uint64_t lease_id;                /* fencing token; 0 is refused */
    uint64_t lease_generation;        /* superseded generation cannot commit */
    uint64_t authority_id;            /* who authorised; 0 is refused */
    uint64_t evidence_ref;            /* the finding this answers; 0 refused */

    uint32_t no_self_trigger;         /* 1 = must not be raised by the plane
                                       * that would execute it (AI021) */
    uint32_t require_reversible;      /* 1 = refuse if any class is not */
    uint8_t  descriptor_hash[32];     /* SHA-256 over the fields above */
} action_descriptor_t;

/* ── Preview ─────────────────────────────────────────────────────────────── */
typedef struct action_preview {
    uint32_t abi_version;
    uint32_t struct_size;
    uint32_t n_blast;
    uint32_t reversible_all;          /* 1 = every entry is reversible */
    uint64_t estimated_ns;
    action_blast_entry_t blast[ACTION_BLAST_SLOTS];
    uint8_t  descriptor_hash[32];     /* what commit will be pinned to */
    char     refusal[PROV_REASON_MAX];/* empty when the action is admissible */
} action_preview_t;

/* ── Receipt ─────────────────────────────────────────────────────────────── */
typedef struct action_receipt {
    uint32_t abi_version;
    uint32_t struct_size;
    uint32_t state;                   /* action_state_t */
    uint32_t applied;                 /* 1 = the world changed */
    uint64_t txn_id;
    uint64_t committed_at_ns;
    uint64_t lease_id;
    uint64_t lease_generation;
    uint8_t  descriptor_hash[32];
    uint8_t  prior_digest[32];        /* state before, for rollback and verify */
    uint8_t  after_digest[32];        /* state after commit */
    char     detail[PROV_REASON_MAX];
} action_receipt_t;

/* ── The vtable ──────────────────────────────────────────────────────────── */
typedef struct action_provider_v1 {
    uint32_t abi_version;   /* ACTION_PROVIDER_ABI */
    uint32_t struct_size;
    void    *ctx;

    int (*describe)(void *ctx, prov_envelope_t *out, prov_status_t *st);
    int (*negotiate)(void *ctx, const prov_negotiate_req_t *req,
                     prov_negotiate_result_t *out);

    /* Never changes state, in any mode. */
    int (*preview)(void *ctx, const action_descriptor_t *d,
                   action_preview_t *out, prov_status_t *st);

    /* Reserve and fence. Fails REFUSAL when the lease is stale, the authority
     * is absent, the evidence reference is empty, or the descriptor hash does
     * not match the preview it claims to follow. */
    int (*prepare)(void *ctx, const action_descriptor_t *d,
                   uint64_t *txn_id_out, prov_status_t *st);

    /* Exactly once per idempotency key. A repeat returns the original receipt
     * with applied == 0 and PROV_OK — a second commit is not an error, but it
     * is also not a second change. */
    int (*commit)(void *ctx, uint64_t txn_id, action_receipt_t *out,
                  prov_status_t *st);

    /* Read the world back. A verify that only re-reads the provider's own
     * bookkeeping proves nothing and is a conformance failure (D098). */
    int (*verify)(void *ctx, uint64_t txn_id, action_receipt_t *out,
                  prov_status_t *st);

    /* Undo a prepared or committed transaction. */
    int (*rollback)(void *ctx, uint64_t txn_id, action_receipt_t *out,
                    prov_status_t *st);

    /* After a provider restart: report every transaction that was prepared or
     * committed but not verified, so the coordinator can finish or undo it
     * rather than forget it (D097). */
    int (*reconcile)(void *ctx, action_receipt_t *out, uint32_t max,
                     uint32_t *n_out, prov_status_t *st);

    int (*health)(void *ctx, prov_health_t *out, prov_status_t *st);

    void *reserved[8];
} action_provider_v1_t;

const char *action_verb_name(uint32_t verb);
const char *action_state_name(uint32_t state);
const char *action_blast_name(uint32_t klass);
include/platx/agent_inventory.h
/* platx/agent_inventory.h — INT-143: central agent inventory.
 *
 * The server maintains a bounded table of connected agents. Each agent
 * entry is keyed by its RA2C identity and populated from its capability
 * snapshot at handshake time. Entries expire when the RA2C session drops
 * or is explicitly evicted.
 */

#define PLAT_INV_AGENT_MAX     32
#define PLAT_INV_ID_MAX        64
#define PLAT_INV_CAP_MAX       16
#define PLAT_INV_REASON_MAX    128

typedef struct plat_inv_cap_snap {
    char     name[PLAT_INV_ID_MAX];
    uint32_t version;
} plat_inv_cap_snap_t;

typedef struct plat_inv_agent {
    char                 agent_id[PLAT_INV_ID_MAX];  /* RA2C identity */
    uint32_t             abi_version;
    uint64_t             connected_at_ms;   /* monotonic */
    plat_inv_cap_snap_t  caps[PLAT_INV_CAP_MAX];
    int                  n_caps;
    int                  live;              /* 0 after eviction */
} plat_inv_agent_t;

int  plat_inv_init(void);
void plat_inv_fini(void);

/* Add or update an agent entry. Returns 0 on success, -1 if table full. */
int plat_inv_upsert(const plat_inv_agent_t *agent);

/* Mark agent as disconnected (live=0). Returns 0 or -1 (not found). */
int plat_inv_evict(const char *agent_id);

/* Fill *out with the current entry. Returns 0 or -1 (not found / evicted). */
int plat_inv_get(const char *agent_id, plat_inv_agent_t *out);

/* Count live agents. */
int plat_inv_count(void);
include/platx/alert.h
/* alert.h — S542/S543: деградация, о которой можно что-то сделать.
 *
 * S542  У КАЖДОЙ деградации обязаны быть пять вещей: причинная цепочка,
 *       время первого появления, текущее поколение, на что это влияет и что
 *       делать. Сообщение «health: DEGRADED» не содержит ни одной из них и
 *       потому не является сообщением: прочитавший его знает ровно столько
 *       же, сколько до чтения.
 *
 *       Поэтому запись БЕЗ цепочки, влияния или действия отвергается на
 *       входе. Это неудобно тому, кто её заводит, — и в этом весь смысл:
 *       неудобство ложится на автора один раз, а не на дежурного каждый раз.
 *
 * S543  Повторы склеиваются, но НИЧЕГО не теряют. Склейка нужна затем, что
 *       одна и та же деградация, повторённая тысячу раз, вытесняет из поля
 *       зрения девятьсот других. Но склеить — не значит забыть: сохраняются
 *       число повторов, длительность, ХУДШАЯ виденная тяжесть и переход в
 *       восстановление.
 *
 *       Худшая тяжесть отдельно важна. Деградация, побывавшая КРИТИЧЕСКОЙ и
 *       осевшая на «предупреждении», в сводке по текущей тяжести выглядит
 *       безобидно — и именно так пропускают то, что уже один раз падало.
 *
 *       И вторая ловушка склейки: эпизоды. Деградация, восстановившаяся и
 *       случившаяся снова, — это ДВА эпизода, а не один длинный. Склеить их
 *       значит соврать о длительности: «шло восемь часов» вместо «дважды по
 *       пять минут с перерывом».
 */
#define PLAT_ALERT_MAX        32
#define PLAT_ALERT_KEY_MAX    64
#define PLAT_ALERT_CAUSE_MAX  4
#define PLAT_ALERT_TEXT_MAX   96
#define PLAT_ALERT_ACTION_MAX 128

typedef enum {
    PLAT_SEV_INFO     = 0,
    PLAT_SEV_DEGRADED = 1,
    PLAT_SEV_CRITICAL = 2
} plat_sev_t;

typedef struct {
    char       key[PLAT_ALERT_KEY_MAX];
    char       cause[PLAT_ALERT_CAUSE_MAX][PLAT_ALERT_TEXT_MAX];
    int        cause_n;
    char       impact[PLAT_ALERT_TEXT_MAX];
    char       action[PLAT_ALERT_ACTION_MAX];

    uint64_t   first_ms;       /* начало ТЕКУЩЕГО эпизода           */
    uint64_t   last_ms;
    uint32_t   generation;
    uint32_t   count;          /* повторов в текущем эпизоде        */
    uint32_t   episodes;       /* сколько раз начиналось заново     */

    plat_sev_t current;
    plat_sev_t worst;          /* худшая за ВСЮ жизнь записи        */
    plat_sev_t worst_episode;  /* худшая в текущем эпизоде          */

    int        recovered;
    uint64_t   recovered_ms;
    uint64_t   total_down_ms;  /* сумма по всем закрытым эпизодам   */
    int        used;
} plat_alert_t;

typedef struct {
    plat_alert_t a[PLAT_ALERT_MAX];
    int          n;
} plat_alert_book_t;

typedef enum {
    PLAT_ALERT_OK        = 0,
    PLAT_ALERT_NO_CAUSE  = 1,  /* нет причинной цепочки  */
    PLAT_ALERT_NO_IMPACT = 2,
    PLAT_ALERT_NO_ACTION = 3,
    PLAT_ALERT_FULL      = 4,
    PLAT_ALERT_BAD_ARG   = 5,
    PLAT_ALERT_UNKNOWN   = 6   /* нет такой записи        */
} plat_alert_verdict_t;

void plat_alert_init(plat_alert_book_t *b);

/* Поднять или продлить. cause — массив из cause_n звеньев цепочки.
 * Отвергает запись без цепочки, влияния или действия (S542). */
plat_alert_verdict_t plat_alert_raise(plat_alert_book_t *b,
                                      const char *key, plat_sev_t sev,
                                      const char *const *cause, int cause_n,
                                      const char *impact, const char *action,
                                      uint32_t generation, uint64_t now_ms);

/* Восстановление — ПЕРЕХОД, а не удаление: запись остаётся. */
plat_alert_verdict_t plat_alert_recover(plat_alert_book_t *b,
                                        const char *key, uint64_t now_ms);

const plat_alert_t *plat_alert_find(const plat_alert_book_t *b, const char *key);

/* Сколько сейчас НЕ восстановленных. */
int plat_alert_active(const plat_alert_book_t *b);

/* Человеку: по строке на запись, с цепочкой и действием. */
int plat_alert_render(const plat_alert_book_t *b, char *buf, size_t cap);

const char *plat_sev_str(plat_sev_t s);
const char *plat_alert_verdict_name(plat_alert_verdict_t v);
include/platx/atomic_file.h
/* atomic_file.h — S403: замена файла целиком или никак.
 *
 * ПРОТОКОЛ
 * ────────
 *   1. временный файл В ТОМ ЖЕ КАТАЛОГЕ, что и цель;
 *   2. запись целиком;
 *   3. fsync временного — данные на диске;
 *   4. close с проверкой кода — отложенная ошибка записи приходит именно сюда;
 *   5. rename — подмена атомарна для читателя;
 *   6. fsync КАТАЛОГА — запись каталога о подмене на диске.
 *
 * Шаг 1 не косметика: rename атомарен только внутри одной файловой системы, а
 * /tmp сплошь и рядом отдельная. Шаг 6 — тот, которого в дереве не было нигде:
 * fsync файла делает долговечными данные, но не запись каталога, указывающую
 * на них. Без него вызов возвращает успех, а после потери питания цель может
 * остаться прежней — и это ещё хороший исход.
 *
 * ОТКАЗ ВМЕСТО МОЛЧАНИЯ
 * ─────────────────────
 * Если у переданной таблицы I/O нет fsync, функция ОТКАЗЫВАЕТ (-ENOTSUP), а не
 * пропускает шаг. Обещание долговечности без механизма — это не долговечность,
 * а её вид; молчаливый пропуск здесь стоил бы ровно того, ради чего эта
 * функция написана.
 *
 * ЗАВИСИМОСТЬ
 * ───────────
 * На каждой границе объявлена точка краха (S408), поэтому линковка требует
 * src/core/plat_crashpoint.c. Невзведённая точка — сравнение строки с пустой,
 * то есть цена нулевая; отказаться от неё значило бы отказаться от единственного
 * способа проверить, что именно попадает на диск при смерти процесса.
 *
 * ЧЕСТНОСТЬ ПОСЛЕДНЕГО ШАГА
 * ─────────────────────────
 * Отказ на шаге 6 возвращается вызывающему, но rename к этому моменту УЖЕ
 * произошёл и отменён быть не может: цель подменена, долговечность подмены не
 * подтверждена. Возврат ошибки здесь означает «состояние на диске не
 * гарантировано», а не «ничего не изменилось».
 */
struct xio_t;

/* 0 — успех. Отрицательный -errno — отказ.
 *
 * До шага 5 (rename) любой отказ оставляет ЦЕЛЬ НЕТРОНУТОЙ: временный файл
 * удаляется, прежнее содержимое остаётся последним известным хорошим.
 * -ENOTSUP означает, что таблица I/O не умеет чего-то из протокола. */
int plat_atomic_write(const struct xio_t *io, const char *path,
                      const void *data, size_t len, int mode);

/* Долговечное УДАЛЕНИЕ (S404).
 *
 * unlink убирает запись каталога, но долговечной её отсутствие делает только
 * fsync каталога — ровно как и при подмене. Без него после потери питания файл
 * может вернуться, и «удалён» окажется временным состоянием. Асимметрия здесь
 * ничем не оправдана: удаление — такое же изменение каталога, как и создание.
 *
 * 0 / -errno. Отсутствие файла (-ENOENT) — успех: цель достигнута. */
int plat_atomic_remove(const struct xio_t *io, const char *path);

/* Убрать СВОИ временные файлы, оставшиеся от прежних падений (S431).
 *
 * Имя временного файла несёт pid создателя, и уборка идёт только по нему:
 * удаляется лишь то, чей владелец ДОКАЗАННО мёртв (kill(pid,0) → ESRCH).
 * Живой владелец, чужой формат имени, отказ в проверке — файл остаётся.
 *
 * Направление отказа выбрано намеренно: переиспользованный pid выглядит живым,
 * и мы не удалим мусор. Обратная ошибка — удалить файл работающего процесса —
 * стоит данных, а эта стоит места на диске.
 *
 * Возвращает число убранных файлов, либо -errno. */
int plat_atomic_sweep(const struct xio_t *io, const char *dir,
                      const char *base);
include/platx/bundle.h
/* bundle.h — S468/S485/S483: версия пакета настроек и её перевод.
 *
 * S468  У пакета CFG/MSX есть версия, и перевод между версиями обязан быть
 *       ДЕТЕРМИНИРОВАННЫМ, АВТОНОМНЫМ и ОБРАТИМЫМ.
 *
 *       детерминированным — один вход даёт один и тот же байт-в-байт выход,
 *       сколько бы раз его ни прогнали. Иначе «мигрировали» и «мигрировали
 *       ещё раз» — разные файлы, и сравнить их нельзя.
 *
 *       автономным — без сети, без часов, без случайности. Перевод, которому
 *       нужен внешний источник, нельзя выполнить там, где чинят упавший узел.
 *
 *       обратимым — и это ЖЁСТЧЕ, чем кажется. Обратим не тот перевод, после
 *       которого можно что-то собрать обратно, а тот, после которого обратно
 *       собирается ИСХОДНЫЙ ФАЙЛ. Если операция потеряла бы значение, которое
 *       поставил человек, обратный перевод обязан ОТКАЗАТЬСЯ, а не подставить
 *       умолчание: молча вернуть «почти то же самое» хуже, чем не вернуть.
 *
 * S485  Сухой прогон: те же диагностики, ноль побочных действий. Проверять
 *       переводом «на живую» — значит проверять на том, что ломаешь.
 *
 * S483  Матрица понижений строится из ЭТИХ вердиктов, а не из обещаний:
 *       см. tests/cli/compat_matrix.sh.
 *
 * ФОРМАТ (намеренно скучный)
 * ──────────────────────────
 *   bundle_version=<N>
 *   ключ=значение
 * Строки с '#' и пустые сохраняются дословно: комментарий человека — это
 * тоже содержимое.
 */
#define PLAT_BUNDLE_V_MIN     1u
#define PLAT_BUNDLE_V_CURRENT 2u
#define PLAT_BUNDLE_MAX_BYTES 8192u
#define PLAT_BUNDLE_MAX_NOTES 16

typedef enum {
    PLAT_BUNDLE_OK           = 0,
    PLAT_BUNDLE_MALFORMED    = 1,
    PLAT_BUNDLE_NO_VERSION   = 2,
    PLAT_BUNDLE_TOO_NEW      = 3,
    PLAT_BUNDLE_TOO_OLD      = 4,
    PLAT_BUNDLE_IRREVERSIBLE = 5,  /* обратный перевод потерял бы данные */
    PLAT_BUNDLE_NOOP         = 6,  /* уже нужной версии                 */
    PLAT_BUNDLE_OVERFLOW     = 7
} plat_bundle_verdict_t;

typedef struct {
    char key[64];
    char what[96];    /* что именно произойдёт с этим ключом */
} plat_bundle_note_t;

typedef struct {
    plat_bundle_note_t note[PLAT_BUNDLE_MAX_NOTES];
    int                n;
    uint32_t           from, to;
    plat_bundle_verdict_t verdict;
} plat_bundle_plan_t;

/* Версия пакета из текста. 0 и NO_VERSION, если заголовка нет. */
plat_bundle_verdict_t plat_bundle_version(const char *text, uint32_t *out);

/* S485: сухой прогон. Ничего не пишет — ни файла, ни глобального состояния.
 * Возвращает ПЛАН: по строке на каждый ключ, которого перевод коснётся. */
plat_bundle_verdict_t plat_bundle_plan(const char *text, uint32_t to,
                                       plat_bundle_plan_t *plan);

/* Перевод. Выход детерминирован: те же байты при каждом прогоне.
 * Порядок ключей исходника сохраняется; добавленные ключи идут в конец. */
plat_bundle_verdict_t plat_bundle_migrate(const char *text, uint32_t to,
                                          char *out, size_t cap);

const char *plat_bundle_verdict_name(plat_bundle_verdict_t v);
include/platx/capreq.h
/* capreq.h — S481/S482: незнакомая возможность как решение, а не как молчание.
 *
 * S481  Незнакомая НЕОБЯЗАТЕЛЬНАЯ возможность = недоступна. Незнакомая
 *       ОБЯЗАТЕЛЬНАЯ = отказ на старте.
 *
 *       Разница здесь не в строгости, а в том, кто отвечает за последствия.
 *       Необязательная возможность на то и необязательная, что её отсутствие
 *       предусмотрено: работаем без неё. Обязательную же кто-то потребовал,
 *       потому что без неё поведение узла НЕ ТО, за которое он отвечает.
 *       Запуститься, не поняв такого требования, значит начать работу под
 *       чужим именем: снаружи узел выглядит настроенным, а внутри — нет.
 *       Поэтому отказ на СТАРТЕ, а не первая ошибка через час.
 *
 * S482  Снятая возможность не исчезает молча: у неё есть состояние RETIRED,
 *       по которому её убирают из перечней команд и статуса. Возможность,
 *       которую сняли, но которая осталась в списке, — это обещание, за
 *       которым ничего нет.
 */
typedef enum {
    PLAT_CAP_ST_ABSENT  = 0,  /* нам не известна вовсе          */
    PLAT_CAP_ST_PRESENT = 1,
    PLAT_CAP_ST_RETIRED = 2   /* была, снята по пути устаревания */
} plat_cap_state_t;

typedef struct {
    const char *name;
    int         mandatory;
} plat_cap_req_t;

typedef enum {
    PLAT_CAP_OK      = 0,
    PLAT_CAP_REFUSE  = 1,   /* обязательная недоступна — старт отменён */
    PLAT_CAP_BAD_ARG = 2
} plat_cap_verdict_t;

plat_cap_state_t plat_cap_state(const char *name);

/* Разобрать требования. out_mask — биты доступных известных возможностей.
 * why — человекочитаемая причина отказа (имя возможности внутри).
 * При отказе out_mask НЕ выставляется: полумаска хуже отсутствия маски. */
plat_cap_verdict_t plat_cap_resolve(const plat_cap_req_t *reqs, size_t n,
                                    uint32_t *out_mask,
                                    char *why, size_t whycap);

/* Перечень для статуса/команд: снятые сюда не попадают (S482). */
size_t plat_cap_list_visible(const char **out, size_t cap);
include/platx/cdata2.h
/* Options for cdata2_save().  All pointer fields may be NULL for defaults. */
typedef struct {
    const char *encoding;    /* "hex" | "base64" | NULL → "base64"             */
    const char *compression; /* "gzip" | NULL → none                           */
    const char *encryption;  /* "rc4" | "chacha20" | "aes256gcm" | NULL → none */
    const char *key;         /* secret supplied via --key=SECRET               */
    const char *keyring;     /* keyring entry name (--keyring=NAME)            */
    int         embed_key;   /* 1: store literal key hex in container field    */
    const char *hash_algo;   /* "sha256" | "crc32" | NULL → "sha256"           */
} cdata2_opts_t;

/* Pack raw bytes into a CDATA v2 container string.
 * Returns malloc'd NUL-terminated string, or NULL on error.
 * opts may be NULL for all defaults.  Caller frees. */
char *cdata2_save(const unsigned char *raw, size_t rawlen,
                  const cdata2_opts_t *opts);

/* Unpack a CDATA v2 container string.
 * override_key: secret passed on command line when key_ref=="0".
 * Returns malloc'd raw bytes; *outlen = byte count.  NULL on error. */
unsigned char *cdata2_load(const char *container, const char *override_key,
                           size_t *outlen);

/* Print container metadata to stdout.
 * Returns 0 if the container is well-formed, -1 otherwise. */
int cdata2_info(const char *container);

/* Transparent dispatch helper.
 * If s begins with a valid CDATA v2 header "[...]", decode and return the
 * raw payload (malloc'd, *outlen set).  Returns NULL if s is not a CDATA
 * container or if decoding fails.  Never calls die(). */
unsigned char *mfp_maybe_cdata(const char *s, size_t *outlen);

unsigned char *hex_decode(const char *, size_t *);
char *hex_encode(const unsigned char *, size_t);
unsigned char *b64_decode(const char *, size_t *);
char *b64_encode(const unsigned char *, size_t);
unsigned char *cdata_literal_decode(const char *, size_t *);
include/platx/cfgkey.h
/* cfgkey.h — S462: четыре разных ответа вместо одного «unknown key».
 *
 * Сейчас конфигуратор на любой непринятый ключ отвечает одинаково: «unknown
 * key 'x'» и отказ. Под этим ответом лежат четыре РАЗНЫХ положения, и
 * оператору нужно разное действие в каждом:
 *
 *   ИЗВЕСТНЫЙ    — принимаем.
 *   УСТАРЕВШИЙ   — принимаем, но говорим, чем заменить. Отказ здесь ломал бы
 *                  работающие конфиги при обновлении.
 *   УДАЛЁННЫЙ    — отказ с указанием, ЧТО делать: ключ был, его убрали, и
 *                  молчаливое «unknown» заставляет искать опечатку там, где
 *                  её нет.
 *   ОПЕЧАТКА     — отказ, но с подсказкой. «protect.module» вместо
 *                  «protect.modules» — самая частая причина непринятого
 *                  конфига, и она решается одной строкой вывода.
 *   НЕИЗВЕСТНЫЙ  — отказ без подсказки: похожего нет.
 *
 * Расстояние правки считается до 2 включительно. Дальше подсказка становится
 * вредной: «вы имели в виду совсем другое?» хуже, чем её отсутствие.
 */
typedef enum {
    PLAT_CFGKEY_KNOWN      = 0,
    PLAT_CFGKEY_DEPRECATED = 1,
    PLAT_CFGKEY_REMOVED    = 2,
    PLAT_CFGKEY_TYPO       = 3,
    PLAT_CFGKEY_UNKNOWN    = 4
} plat_cfgkey_class_t;

typedef struct {
    const char *key;
    const char *note;   /* чем заменить / когда убран */
} plat_cfgkey_entry_t;

typedef struct {
    const plat_cfgkey_entry_t *known;      size_t n_known;
    const plat_cfgkey_entry_t *deprecated; size_t n_deprecated;
    const plat_cfgkey_entry_t *removed;    size_t n_removed;
} plat_cfgkey_table_t;

/* Классифицировать ключ. suggest получает подсказку (имя похожего ключа или
 * note), если она есть; иначе пустую строку. */
plat_cfgkey_class_t plat_cfgkey_classify(const plat_cfgkey_table_t *t,
                                         const char *key,
                                         char *suggest, size_t suggest_sz);

const char *plat_cfgkey_class_name(plat_cfgkey_class_t c);

/* Признак «расстояние неприменимо»: NULL-аргумент или ключ длиннее предела.
 * B-024: раньше в этих случаях возвращалось 64 — то же число, каким могло
 * оказаться настоящее расстояние между двумя 64-символьными ключами, так что
 * отказ был неотличим от ответа. Настоящее расстояние никогда не превышает
 * предела длины, поэтому SIZE_MAX не может быть спутан с ним. */
#define PLAT_CFGKEY_DISTANCE_NA ((size_t)-1)

/* Расстояние правки, вынесено ради проверок. Возвращает
 * PLAT_CFGKEY_DISTANCE_NA, если хоть один аргумент NULL или длиннее предела. */
size_t plat_cfgkey_distance(const char *a, const char *b);
include/platx/chain.h
/* chain.h — S446/S419: пропажа, перестановка и подмена члена обязаны быть видны.
 *
 * Манифест доказательств — это список членов с их хешами. Список сам по себе
 * защищает от подмены СОДЕРЖИМОГО и ни от чего больше: удалить строку или
 * поменять две местами он не мешает. Поэтому строки связаны в цепочку:
 *
 *     link[i] = SHA256( link[i-1] || имя || размер || хеш(содержимое) )
 *
 * Каждое звено зависит от всех предыдущих, поэтому пропажа, перестановка и
 * подмена одинаково рвут цепочку. Хеш берётся существующий (crypto_sha256);
 * ничего нового здесь не заводится.
 *
 * ЗАВЕРШЁН ИЛИ НЕТ (S419)
 * ───────────────────────
 * Собранный набор отличается от недособранного НАЛИЧИЕМ замыкающей записи.
 * Без неё набор — INCOMPLETE, и это не ошибка чтения: сборка могла оборваться
 * крахом, и выдать её за готовую значило бы подписать неизвестно что.
 */
typedef enum {
    PLAT_CHAIN_SEALED     = 0,  /* цепочка сошлась и замкнута            */
    PLAT_CHAIN_INCOMPLETE = 1,  /* сошлась, но не замкнута — сборка оборвана */
    PLAT_CHAIN_MISSING    = 2,  /* члена нет на диске                    */
    PLAT_CHAIN_REPLACED   = 3,  /* содержимое члена иное                 */
    PLAT_CHAIN_BROKEN     = 4   /* звено не сходится: порядок или сам
                                 * манифест изменены                     */
} plat_chain_state_t;

typedef struct {
    plat_chain_state_t state;
    size_t             members;     /* сколько записей в манифесте        */
    size_t             at;          /* индекс первой расходящейся записи  */
    char               name[128];   /* её имя                             */

    /* При BROKEN: целы ли САМИ ЧЛЕНЫ на диске.
     *
     * Цепочка доказывает, что манифест не тот, но НЕ различает перестановку
     * записей и правку манифеста — по одному разорванному звену это не
     * восстановить, и выдавать догадку за вывод здесь нельзя. Зато проверяемо
     * другое: сходится ли каждый файл со СВОИМ записанным хешем. Сходится —
     * значит тронут манифест, а не содержимое; не сходится — тронуто и оно.
     * Это оператору и нужно знать в первую очередь. */
    int                members_intact;
} plat_chain_report_t;

struct xio_t;

/* Создать пустой манифест. */
int plat_chain_create(const struct xio_t *io, const char *manifest);

/* Добавить члена: содержимое читается из dir/name. */
int plat_chain_add(const struct xio_t *io, const char *manifest,
                   const char *dir, const char *name);

/* Замкнуть набор. После замыкания добавлять нельзя. */
int plat_chain_seal(const struct xio_t *io, const char *manifest);

/* Проверить манифест против того, что реально лежит в dir. */
int plat_chain_verify(const struct xio_t *io, const char *manifest,
                      const char *dir, plat_chain_report_t *out);
include/platx/child_ipc.h
/* platx/child_ipc.h — wire ABI between worker child_host and child proxy.
 * Length-prefixed frames over a connected SOCK_STREAM. Not a domain API.
 */
#define PLAT_IPC_MAGIC     0x31584C50u /* 'PLX1' */
#define PLAT_IPC_MAX       4096u
#define PLAT_IPC_NAME      64u
#define PLAT_CHILD_IPC_FD  3

enum plat_ipc_type {
    PLAT_IPC_HELLO = 1,
    PLAT_IPC_HELLO_ACK,
    PLAT_IPC_REQUIRE,
    PLAT_IPC_REQUIRE_OK,
    PLAT_IPC_REQUIRE_ERR,
    PLAT_IPC_PROVIDE,
    PLAT_IPC_PROVIDE_OK,
    PLAT_IPC_PROVIDE_ERR,
    PLAT_IPC_REVOKE,
    PLAT_IPC_REVOKE_OK,
    PLAT_IPC_REVOKE_ERR,
    PLAT_IPC_CALL,
    PLAT_IPC_CALL_OK,
    PLAT_IPC_CALL_ERR,
    PLAT_IPC_READY,
    PLAT_IPC_BYE
};

/* Reserved CALL handles for Core primitives. Capability handles are 1..0x7fffffff.
 * Unknown handle / method / short payload → CALL_ERR, no half-apply. */
#define PLAT_IPC_H_RES    0x80000001u
#define PLAT_IPC_H_EVENT  0x80000002u

#define PLAT_IPC_RES_OWN            1u
#define PLAT_IPC_RES_RELEASE        2u
#define PLAT_IPC_RES_RELEASE_OWNER  3u
#define PLAT_IPC_RES_CHECK          4u
#define PLAT_IPC_EV_EMIT            1u

#define PLAT_IPC_OWNER_LEN   12u
#define PLAT_IPC_RES_NAME    48u
#define PLAT_IPC_RES_OWN_LEN (PLAT_IPC_OWNER_LEN + 4u + 8u + PLAT_IPC_RES_NAME)

typedef struct plat_ipc_hdr {
    uint32_t magic;
    uint32_t type;
    uint32_t seq;
    uint32_t len;
} plat_ipc_hdr_t;

typedef struct plat_ipc_msg {
    uint32_t type;
    uint32_t seq;
    uint32_t len;
    uint8_t  data[PLAT_IPC_MAX];
} plat_ipc_msg_t;

int plat_ipc_send(int fd, uint32_t type, uint32_t seq,
                  const void *payload, uint32_t len);
/* timeout_ms < 0 = block. 0 = poll once. >0 = bounded wait. */
int plat_ipc_recv(int fd, plat_ipc_msg_t *msg, int timeout_ms);
int plat_ipc_rpc(int fd, uint32_t *seq, uint32_t type,
                 const void *req, uint32_t reqlen,
                 uint32_t ok_type, uint32_t err_type,
                 void *okbuf, uint32_t okcap, uint32_t *oklen,
                 int timeout_ms);

uint32_t plat_ipc_put_cap(uint8_t *dst, const char *name, uint32_t version);
int      plat_ipc_get_cap(const uint8_t *src, uint32_t len,
                          char *name, uint32_t name_cap, uint32_t *version);
uint32_t plat_ipc_put_call(uint8_t *dst, uint32_t handle, uint32_t method,
                           const void *in, uint32_t in_len);
int      plat_ipc_get_call(const uint8_t *src, uint32_t len,
                           uint32_t *handle, uint32_t *method,
                           const uint8_t **data, uint32_t *data_len);
include/platx/childrec.h
/* childrec.h — S421/S422: запись о ребёнке не переживает переиспользование pid.
 *
 * PID НЕ ЯВЛЯЕТСЯ ИДЕНТИФИКАТОРОМ
 * ───────────────────────────────
 * Номер процесса переиспользуется, и после перезапуска (а на загруженной
 * машине и без него) под тем же номером живёт кто-то другой. Сохранённая
 * запись «мой ребёнок — pid 4242» после этого указывает на чужой процесс, и
 * всё, что по ней делают — сигнал, привязка, снятие ограничений, — делают с
 * ним. Поэтому запись хранит ещё и ВРЕМЯ РОЖДЕНИЯ процесса (поле 22
 * /proc/<pid>/stat) и поколение владельца. Совпасть должны все три.
 *
 * Поле 22 читается от ПОСЛЕДНЕЙ ')' в строке: имя процесса заключено в скобки
 * и может содержать и пробелы, и сами скобки, поэтому разбор по пробелам с
 * начала строки ломается на процессе с именем "x) 1 2 3 4 5".
 *
 * ТРИ СОСТОЯНИЯ (S421)
 * ────────────────────
 * PENDING — ребёнок запущен, о готовности не сообщал. Это НЕ «не запустился»:
 * крах родителя между запуском и READY оставляет именно такую запись, и
 * достроить её в любую сторону значит либо бросить работающего ребёнка, либо
 * ждать несуществующего.
 */
struct xio_t;

typedef enum {
    PLAT_CHILD_PENDING = 0,   /* запущен, готовности не подтверждал */
    PLAT_CHILD_READY   = 1,
    PLAT_CHILD_GONE    = 2
} plat_child_state_t;

typedef struct {
    uint32_t           pid;
    uint32_t           generation;
    uint64_t           birth;        /* starttime, поле 22 /proc/<pid>/stat */
    plat_child_state_t state;
} plat_childrec_t;

typedef enum {
    PLAT_CHILD_VALID     = 0,
    PLAT_CHILD_DEAD      = 1,  /* процесса нет                        */
    PLAT_CHILD_REUSED    = 2,  /* pid тот же, процесс другой          */
    PLAT_CHILD_STALE_GEN = 3   /* владелец пересоздан                 */
} plat_child_verdict_t;

/* Снять запись о живом процессе. -ESRCH, если его нет. */
int plat_child_capture(plat_childrec_t *out, uint32_t pid, uint32_t generation);

/* Сверить запись с тем, что живёт под этим pid ПРЯМО СЕЙЧАС. */
plat_child_verdict_t plat_child_verify(const plat_childrec_t *r,
                                       uint32_t current_generation);

int plat_child_save(const struct xio_t *io, const char *path,
                    const plat_childrec_t *r);
int plat_child_load(const struct xio_t *io, const char *path,
                    plat_childrec_t *out);
include/platx/cmd_authz.h
/* platx/cmd_authz.h — INT-146: central command authorization.
 *
 * Every command dispatched from the central node must carry:
 *   scope      — what it may affect (capability namespace)
 *   target     — which agent/context identity
 *   expiry     — absolute monotonic ms; 0 = no expiry (admin only)
 *   nonce      — replay protection: each nonce accepted exactly once
 *   audit_out  — filled on both ALLOW and DENY with the outcome
 *
 * A command missing any required field is DENIED immediately.
 * A replayed nonce is DENIED even if all other fields are valid.
 * An expired command is DENIED even if the nonce is fresh.
 *
 * This module is stateful: it maintains the seen-nonce ring.
 * Call plat_cmd_authz_init() once before use; plat_cmd_authz_fini() on
 * shutdown to release the ring.
 */

#define PLAT_AUTHZ_SCOPE_MAX   64
#define PLAT_AUTHZ_TARGET_MAX  64
#define PLAT_AUTHZ_REASON_MAX  128
#define PLAT_AUTHZ_NONCE_RING  256   /* seen-nonce ring capacity */

/* ── Command envelope ────────────────────────────────────────────────── */
typedef struct plat_cmd_envelope {
    char     scope[PLAT_AUTHZ_SCOPE_MAX];   /* capability being exercised */
    char     target[PLAT_AUTHZ_TARGET_MAX]; /* agent id or context id str  */
    uint64_t expiry_ms;   /* monotonic ms; 0 = no expiry (requires admin)  */
    uint64_t nonce;       /* replay token — must be unique per scope+target */
    int      admin;       /* non-zero: allows expiry_ms == 0               */
} plat_cmd_envelope_t;

/* ── Authorization result ────────────────────────────────────────────── */
typedef enum plat_authz_verdict {
    PLAT_AUTHZ_ALLOW  = 0,
    PLAT_AUTHZ_DENY   = 1
} plat_authz_verdict_t;

typedef struct plat_authz_result {
    plat_authz_verdict_t verdict;
    char                 reason[PLAT_AUTHZ_REASON_MAX];
} plat_authz_result_t;

/* ── API ─────────────────────────────────────────────────────────────── */

int  plat_cmd_authz_init(void);
void plat_cmd_authz_fini(void);

/* Authorize one command envelope.
 * Returns 0 (ALLOW) or -1 (DENY).
 * result is always populated.
 * Side effect on ALLOW: nonce is recorded as seen.
 */
int plat_cmd_authz(const plat_cmd_envelope_t *env,
                   plat_authz_result_t        *result);

/* Read the monotonic clock in ms (same basis as expiry_ms). */
int plat_authz_clock_ms(uint64_t *out);
include/platx/cmd_dispatch.h
/* platx/cmd_dispatch.h — INT-148: typed request dispatcher.
 * Authorization via cmd_authz before dispatch; result via incident_env. */

#define PLAT_DISPATCH_MAX_HANDLERS 32
#define PLAT_DISPATCH_CMD_MAX      64

typedef int (*plat_cmd_handler_fn)(const char *cmd_type,
                                   const void *payload, size_t payloadsz,
                                   plat_incident_env_t *env);

int  plat_dispatch_init(void);
void plat_dispatch_fini(void);

/* Register handler for cmd_type. -1 on duplicate or table full. */
int plat_dispatch_register(const char *cmd_type, plat_cmd_handler_fn fn);

/* Remove handler. Returns 0 if found, -1 if not. */
int plat_dispatch_deregister(const char *cmd_type);

/* Authorize via auth_env, find handler for cmd_type, invoke.
 * Returns 0 on success, -1 on authz failure or missing handler. */
int plat_dispatch(const plat_cmd_envelope_t *auth_env,
                  const char *cmd_type,
                  const void *payload, size_t payloadsz,
                  plat_incident_env_t *out_env);
include/platx/compat_handshake.h
/* platx/compat_handshake.h — INT-142: agent↔server compatibility handshake.
 *
 * Before any capability exchange the agent presents its ABI version,
 * capability versions, policy schema digest, and feature bits. The server
 * checks these against its own table and replies: accepted / downgraded /
 * rejected. A rejected handshake must not proceed to capability exchange.
 *
 * Invariant: a newer server MUST accept an older agent with a lower ABI
 * version, provided it can supply the downgraded interface. An agent that
 * demands features the server does not have is rejected, not silently
 * degraded.
 */

#define PLAT_COMPAT_CAP_MAX      16
#define PLAT_COMPAT_NAME_MAX     32
#define PLAT_COMPAT_REASON_MAX   128

/* ── Agent advertises ────────────────────────────────────────────────── */
typedef struct plat_compat_cap {
    char     name[PLAT_COMPAT_NAME_MAX];
    uint32_t version;      /* agent's own version of this capability */
    uint32_t min_version;  /* minimum it can tolerate from the server */
} plat_compat_cap_t;

typedef struct plat_compat_offer {
    uint32_t           abi_version;      /* PLATX_CORE_ABI of the agent */
    uint32_t           policy_digest;    /* CRC32 of current policy schema */
    uint64_t           feature_bits;     /* optional feature flags */
    plat_compat_cap_t  caps[PLAT_COMPAT_CAP_MAX];
    int                n_caps;
} plat_compat_offer_t;

/* ── Server replies ──────────────────────────────────────────────────── */
typedef enum plat_compat_verdict {
    PLAT_COMPAT_ACCEPTED   = 0,  /* full match or server provides downgrade */
    PLAT_COMPAT_DOWNGRADED = 1,  /* accepted with capability version floor lowered */
    PLAT_COMPAT_REJECTED   = 2   /* incompatible; must not proceed */
} plat_compat_verdict_t;

typedef struct plat_compat_reply {
    plat_compat_verdict_t verdict;
    uint32_t              server_abi;        /* server's own ABI version */
    uint64_t              accepted_features; /* server-supported subset */
    /* Per-capability negotiated versions (same order as offer.caps). */
    uint32_t              negotiated[PLAT_COMPAT_CAP_MAX];
    char                  reason[PLAT_COMPAT_REASON_MAX];
} plat_compat_reply_t;

/* ── Server-side check ───────────────────────────────────────────────── */

/* plat_compat_check: validate an agent's offer against this server's table.
 * Returns 0 (ACCEPTED or DOWNGRADED) or -1 (REJECTED).
 * reply is always populated regardless of verdict.
 */
int plat_compat_check(const plat_compat_offer_t *offer,
                      plat_compat_reply_t        *reply);

/* plat_compat_check_strict: like check, but treats DOWNGRADED as REJECTED.
 * Use when the server cannot tolerate version gaps.
 */
int plat_compat_check_strict(const plat_compat_offer_t *offer,
                              plat_compat_reply_t        *reply);

/* ── Agent-side validation ───────────────────────────────────────────── */

/* plat_compat_accept_reply: the agent inspects the server's reply and
 * decides whether to proceed. Returns 0 (proceed) or -1 (abort).
 * Aborts if verdict is REJECTED, or if any negotiated version is below
 * the agent's own min_version for that capability.
 */
int plat_compat_accept_reply(const plat_compat_offer_t *offer,
                              const plat_compat_reply_t *reply);
include/platx/compstat.h
/* compstat.h — S505/S537/S538/S539: состояние пути так, чтобы им можно было
 * пользоваться, не читая отладочный журнал.
 *
 * S505  У каждой составляющей пути шесть полей, и ни одно не лишнее:
 *         чего добивались, что есть, поколение, владелец, последний переход,
 *         причина расхождения.
 *       Пять первых отвечают на «что происходит», шестое — на «почему не то,
 *       что заказано». Строка, где желаемое не равно фактическому, а причина
 *       не названа, ОТВЕРГАЕТСЯ на входе: это и есть тот самый тихий отказ,
 *       ради которого статус вообще существует.
 *
 * S537  Человеку — коротко, по этапам, без секретов, с указанием следующего
 *       шага. Отказ без следующего шага — половина отказа.
 *
 * S538  Машине — JSON с номером схемы. И ГЛАВНОЕ: в человеческом выводе не
 *       должно быть ни одного смысла, которого нет в машинном. Иначе
 *       автоматизация читает не то же самое, что человек, и расхождение
 *       обнаружится в тот единственный раз, когда оно дорого.
 *
 * S539  Затирание секретов — ОДНА функция на оба вывода. Два места затирания
 *       расходятся: сначала чинят одно, потом полгода не замечают второго.
 *       Поэтому и консоль, и JSON, и журнал берут текст только отсюда.
 */
#define PLAT_CS_SCHEMA      1
#define PLAT_CS_NAME_MAX   48
#define PLAT_CS_STATE_MAX  24
#define PLAT_CS_TRANS_MAX  40
#define PLAT_CS_DETAIL_MAX 192
#define PLAT_CS_MAX        16

typedef struct {
    char          name[PLAT_CS_NAME_MAX];
    char          desired[PLAT_CS_STATE_MAX];
    char          actual[PLAT_CS_STATE_MAX];
    uint64_t      generation;
    char          owner[PLAT_CS_NAME_MAX];
    char          last_transition[PLAT_CS_TRANS_MAX];
    uint64_t      last_transition_ms;   /* монотонные мс от старта пути */
    plat_reason_t reason;
    char          detail[PLAT_CS_DETAIL_MAX];   /* хранится УЖЕ затёртым */
} plat_cs_row_t;

typedef struct {
    plat_cs_row_t row[PLAT_CS_MAX];
    int           n;
} plat_cs_t;

void plat_cs_init(plat_cs_t *s);

/* Добавить строку. detail затирается ЗДЕСЬ, один раз, до любого вывода.
 * Возвращает -1, если desired != actual, а причина PLAT_R_OK: состояние не
 * достигнуто и не объяснено — такую строку показывать нельзя. */
int plat_cs_add(plat_cs_t *s, const char *name,
                const char *desired, const char *actual,
                uint64_t generation, const char *owner,
                const char *last_transition, uint64_t transition_ms,
                plat_reason_t reason, const char *detail);

/* S539: единственное место затирания. Возвращает длину результата.
 * Затирает значение после ключевых слов (key/token/secret/password/psk/
 * authorization) и длинные шестнадцатеричные/base64-подобные строки. */
size_t plat_cs_redact(char *dst, size_t cap, const char *src);

/* S537: человеку. Одна строка на составляющую + следующий шаг у каждой,
 * которая не OK. */
int plat_cs_render_human(const plat_cs_t *s, char *buf, size_t cap);

/* S538: машине. Один объект со схемой и массивом составляющих. */
int plat_cs_render_json(const plat_cs_t *s, char *buf, size_t cap);

/* Всё ли достигнуто. 0 — да. Иначе индекс первой недостигнутой + 1. */
int plat_cs_first_bad(const plat_cs_t *s);
include/platx/core.h
/* platx/core.h — SDK umbrella. Modules compile with -I include only.
 * Static boot core that memfd links: platx/kernel.h + libplatx.a. */
include/platx/corr_id.h
/* platx/corr_id.h — Correlation ID across all subsystems (INT-111). */
#define PLAT_CORR_ID_BYTES  16
#define PLAT_CORR_ID_HEXLEN 33

typedef struct { uint8_t b[PLAT_CORR_ID_BYTES]; } plat_corr_id_t;
extern const plat_corr_id_t PLAT_CORR_ID_ZERO;

/* Generate a fresh correlation ID from entropy.  Returns 0/-1. */
int  plat_corr_id_new(plat_corr_id_t *out);
/* Пишет 32 hex-символа и терминатор. NULL в любом аргументе — тихий отказ
 * без записи: тип возврата не позволяет сообщить об этом иначе. */
void plat_corr_id_hex(const plat_corr_id_t *id, char out[PLAT_CORR_ID_HEXLEN]);
int  plat_corr_id_parse(const char *hex, plat_corr_id_t *out);
int  plat_corr_id_is_zero(const plat_corr_id_t *id);

/* Thread-local propagation — matches trace_ctx pattern. */
void plat_corr_id_set_current(const plat_corr_id_t *id);
int  plat_corr_id_get_current(plat_corr_id_t *out);   /* 0 if set, -1 if none */
void plat_corr_id_clear(void);
include/platx/crashpoint.h
/* crashpoint.h — S408: детерминированные точки краха на границах долговечности.
 *
 * ЗАЧЕМ
 * ─────
 * Пункты «крах во время X» (S409–S422) без этого механизма не тесты, а
 * пожелания: убить процесс ровно между fsync и rename нельзя ни таймером, ни
 * сигналом извне — окно измеряется микросекундами и не воспроизводится. Точка
 * краха переворачивает задачу: не «поймать момент», а «объявить его заранее и
 * прийти в него детерминированно».
 *
 * КАК
 * ───
 * Код расставляет именованные точки на границах долговечности. Прогон
 * вооружается переменными окружения — потому что взводить надо ДОЧЕРНИЙ
 * процесс, а окружение переживает fork и exec, в отличие от любого вызова API:
 *
 *   PLATX_CRASH_AT=<имя>       в какой точке умереть
 *   PLATX_CRASH_NTH=<k>        на k-м проходе через неё (по умолчанию 1)
 *   PLATX_CRASH_ID_FILE=<путь> куда записать имя точки ПЕРЕД смертью
 *   PLATX_CRASH_TRACE=<путь>   дописывать каждую пройденную точку (разведка)
 *
 * ПОЧЕМУ _exit, А НЕ abort И НЕ exit
 * ──────────────────────────────────
 * exit() выполняет atexit-обработчики и сбрасывает буферы stdio; abort() тоже
 * может дойти до сброса. Крах, который сбрасывает буферы, — не крах: он
 * доносит до диска ровно то, чего настоящая потеря питания не донесла бы, и
 * тест после него проверял бы состояние, которого в жизни не бывает. Здесь
 * только _exit(PLATX_CRASH_EXIT): ни обработчиков, ни сброса, ни закрытия
 * потоков.
 *
 * По той же причине идентификатор точки пишется через write+fsync напрямую, а
 * не через stdio: он обязан пережить смерть, которая ничего не сбрасывает.
 */
/* Код выхода «умер в объявленной точке». Отличается от 0/1/77, чтобы не
 * сливаться с PASS/FAIL/SKIP тестового договора. */
#define PLATX_CRASH_EXIT 99

/* Объявить границу. Если прогон вооружён на эту точку и это её k-й проход —
 * процесс умирает здесь и не возвращается. Иначе возврат немедленный. */
void plat_crash_point(const char *name);

/* Вооружить/разоружить программно (для случая, когда окружение недоступно).
 * name == NULL снимает взвод. */
void plat_crash_arm(const char *name, unsigned nth);

/* Сколько раз эта точка была пройдена в текущем процессе. Нужна разведке:
 * «сколько границ прошло до отказа» — это факт, а не догадка. */
unsigned plat_crash_hits(const char *name);
include/platx/cryptopol.h
/* cryptopol.h — S484: замороженные имена криптополитик, без псевдонимов.
 *
 * Имя алгоритма в конфиге и на проводе — это ИДЕНТИФИКАТОР, а не описание.
 * «sha256», «SHA-256» и «Sha256» для человека одно и то же; для системы,
 * которая по этому имени выбирает поведение и потом ссылается на него в
 * доказательствах, — три разных строки. Если принять все три и молча свести
 * к одной, то в журнале окажется имя, которого оператор не писал, а при
 * следующем разборе никто не докажет, что именно было согласовано.
 *
 * Отсюда два правила, оба неудобные и оба намеренные:
 *
 *   1. Точное совпадение или отказ. Псевдоним — это ОТДЕЛЬНЫЙ вердикт, а не
 *      «почти правильно»: человеку надо сказать, что мы поняли, чего он
 *      хотел, и всё равно не приняли.
 *
 *   2. Пустое пересечение при согласовании — отказ. Никогда не «возьмём
 *      что-нибудь по умолчанию»: подстановка при неудачном согласовании —
 *      это и есть тихая подмена, ради предотвращения которой всё остальное.
 */
#define PLAT_CP_SHA256    0x0001u
#define PLAT_CP_SHA512    0x0002u
#define PLAT_CP_ED25519   0x0004u
#define PLAT_CP_AES256GCM 0x0008u
#define PLAT_CP_KNOWN     0x000Fu

typedef enum {
    PLAT_CP_OK        = 0,
    PLAT_CP_UNKNOWN   = 1,  /* такого имени нет вовсе                  */
    PLAT_CP_ALIAS     = 2,  /* узнали намерение, но имя не то          */
    PLAT_CP_RETIRED   = 3,  /* имя было, политика снята                */
    PLAT_CP_NO_COMMON = 4   /* пересечение пусто — БЕЗ подстановки     */
} plat_cp_verdict_t;

/* Точный поиск. При OK кладёт бит в *out; иначе *out не трогает. */
plat_cp_verdict_t plat_cryptopol_lookup(const char *name, uint32_t *out);

/* Согласование: пересечение локальной и удалённой политик.
 * Пустое пересечение — отказ, *out не трогается. */
plat_cp_verdict_t plat_cryptopol_negotiate(uint32_t local, uint32_t peer,
                                           uint32_t *out);

const char *plat_cryptopol_name(uint32_t bit);
const char *plat_cp_verdict_name(plat_cp_verdict_t v);
include/platx/ctlmsg.h
/* ctlmsg.h — S472/S473: версия управляющего протокола и порядок сообщений.
 *
 * S472  У протокола присоединения есть версия, и он обязан внятно вести себя
 *       при: незнакомом типе сообщения, незнакомом поле, НАРУШЕННОМ ПОРЯДКЕ и
 *       смене поколения собеседника.
 *
 *       Порядок — самое недооценённое из этого. Управляющий протокол — это
 *       автомат, а не мешок сообщений. READY до HELLO означает, что говорящий
 *       либо не тот, за кого себя выдаёт, либо переподключился, а мы этого не
 *       заметили. Принять такое «потому что поле READY выглядит нормально» —
 *       значит начать работу с состоянием, которого не согласовывали.
 *
 *       Поколение (peer_gen) отличает «тот же собеседник продолжает» от «он
 *       перезапустился и начал заново». Сообщение от ПРЕДЫДУЩЕГО поколения,
 *       пришедшее после нового HELLO, — это эхо мёртвого собеседника. Оно
 *       обязано быть отвергнуто: иначе ответ уедет тому, кого уже нет, а
 *       состояние соединения окажется склеенным из двух жизней.
 *
 * S473  То же для XIM: CHILD READY несёт версию. Родитель принимает ребёнка
 *       версии не ниже объявленного минимума; иначе — отказ. И отказ значит
 *       ИМЕННО отказ: ребёнок, чей READY отвергнут, не считается готовым — ни
 *       в статусе, ни в учёте, ни как «наверное, поднялся».
 *
 * НЕЗНАКОМЫЕ ТИПЫ: две разные вещи
 * ────────────────────────────────
 * Тип из ДИАПАЗОНА РАСШИРЕНИЙ (>= PLAT_CTL_T_EXT_BASE) можно пропустить: он
 * объявлен необязательным заранее. Любой другой незнакомый тип — отказ:
 * молчаливое игнорирование обязательного сообщения выглядит на той стороне
 * как согласие.
 */
#define PLAT_CTL_PROTO_MIN     1u
#define PLAT_CTL_PROTO_CURRENT 2u
/* Ниже этой версии ребёнок не принимается (S473). */
#define PLAT_CTL_CHILD_MIN     1u

typedef enum {
    PLAT_CTL_T_HELLO       = 1,
    PLAT_CTL_T_CHILD_READY = 2,
    PLAT_CTL_T_CHILD_FAIL  = 3,
    PLAT_CTL_T_BYE         = 4,
    PLAT_CTL_T_EXT_BASE    = 0x8000  /* и выше — необязательные расширения */
} plat_ctl_type_t;

typedef struct {
    uint16_t proto;
    uint16_t type;
    uint32_t flags;      /* незнакомые биты — отказ */
    uint64_t peer_gen;   /* поколение собеседника; растёт при перезапуске */
    uint64_t seq;
} plat_ctl_msg_t;

#define PLAT_CTL_FLAG_KNOWN 0x00000003u

typedef enum {
    PLAT_CTL_ST_IDLE  = 0,   /* ничего не согласовано            */
    PLAT_CTL_ST_HELLO = 1,   /* HELLO принят, версия известна    */
    PLAT_CTL_ST_READY = 2,   /* ребёнок объявлен готовым         */
    PLAT_CTL_ST_DEAD  = 3    /* завершено или отвергнуто         */
} plat_ctl_state_t;

typedef struct {
    plat_ctl_state_t state;
    uint16_t         proto;      /* согласованная версия        */
    uint64_t         peer_gen;   /* поколение текущей жизни     */
    uint64_t         last_seq;
    int              child_ready;/* 1 только после ПРИНЯТОГО READY */
} plat_ctl_peer_t;

typedef enum {
    PLAT_CTL_OK           = 0,
    PLAT_CTL_UNKNOWN_MSG  = 1,  /* обязательный тип, которого мы не знаем */
    PLAT_CTL_IGNORED_EXT  = 2,  /* необязательное расширение — пропущено  */
    PLAT_CTL_BAD_PROTO    = 3,
    PLAT_CTL_OUT_OF_ORDER = 4,
    PLAT_CTL_STALE_GEN    = 5,  /* эхо предыдущей жизни собеседника       */
    PLAT_CTL_UNKNOWN_FLAG = 6,
    PLAT_CTL_REPLAY       = 7,  /* seq не растёт                          */
    PLAT_CTL_BAD_ARG      = 8
} plat_ctl_verdict_t;

void plat_ctl_peer_init(plat_ctl_peer_t *p);
plat_ctl_verdict_t plat_ctl_accept(plat_ctl_peer_t *p, const plat_ctl_msg_t *m);
const char *plat_ctl_verdict_name(plat_ctl_verdict_t v);
const char *plat_ctl_state_name(plat_ctl_state_t s);
include/platx/desc.h
/* desc.h — S453/S454/S465/S466: согласование размера у расширяемых договоров.
 *
 * ЗАЧЕМ ЗАГОЛОВОК У КАЖДОГО ПУБЛИЧНОГО ДЕСКРИПТОРА
 * ────────────────────────────────────────────────
 * Структура, которую заполняет ОДНА сторона, а читает ДРУГАЯ, собранная
 * другим заголовком, — это протокол, даже если она объявлена в C. Без размера
 * читатель берёт поля по своим смещениям: у более старой стороны за концом
 * структуры мусор, у более новой — поля, о которых он не знает.
 *
 * Поэтому каждый такой дескриптор начинается с plat_desc_hdr_t, и размер в нём
 * заполняет ТОТ, КТО ЗАПОЛНЯЕТ СТРУКТУРУ.
 *
 * ПРАВИЛА (и почему они не симметричны)
 * ─────────────────────────────────────
 *   size < нашего   → отказ. За концом чужой структуры наши смещения не
 *                     значат ничего.
 *   size > нашего   → ПРИНЯТЬ. Известные поля на местах, лишние не трогаем.
 *                     Иначе добавление поля в конец ломало бы всех, и
 *                     расширяемость была бы на словах.
 *   size == 0       → отказ: заголовок не заполнен вовсе.
 *   size невозможен → отказ: больше объявленного максимума или не кратен
 *                     выравниванию. Это не «новая версия», это мусор.
 *
 *   version         → отдельно от размера: версия про СМЫСЛ полей, размер про
 *                     их число. Старшая версия — отказ, а не попытка угадать.
 *
 *   reserved != 0   → отказ (S454). Незнакомое значение в зарезервированном
 *                     поле — это чужое намерение. Принять его молча значит
 *                     согласиться с тем, чего мы не поняли.
 *
 *   unknown flags   → отказ по той же причине.
 *
 * СОХРАНЕНИЕ РАСШИРЕНИЙ (S454)
 * ────────────────────────────
 * Если сторона получила структуру ДЛИННЕЕ своей и отдаёт её дальше, хвост
 * обязан уехать без изменений. Инструмент, переписывающий артефакт, не имеет
 * права терять поля, которых не понимает: потеря выглядит как решение автора.
 */
#define PLAT_DESC_MAX_SIZE  (64u * 1024u)

typedef struct {
    uint32_t size;      /* sizeof у ЗАПОЛНИВШЕГО                   */
    uint16_t version;   /* смысл полей                             */
    uint16_t flags;     /* известные биты; неизвестные — отказ     */
    uint64_t reserved;  /* обязан быть нулём                       */
} plat_desc_hdr_t;

typedef enum {
    PLAT_DESC_OK          = 0,
    PLAT_DESC_TOO_SMALL   = 1,
    PLAT_DESC_NO_HEADER   = 2,  /* size == 0                        */
    PLAT_DESC_IMPOSSIBLE  = 3,  /* размер невозможен                */
    PLAT_DESC_BAD_VERSION = 4,
    PLAT_DESC_RESERVED_SET= 5,
    PLAT_DESC_UNKNOWN_FLAG= 6
} plat_desc_verdict_t;

/* own_size — sizeof структуры У ЧИТАТЕЛЯ; own_version — его версия;
 * known_flags — маска битов, которые он понимает. */
plat_desc_verdict_t plat_desc_check(const plat_desc_hdr_t *h,
                                    size_t own_size, uint16_t own_version,
                                    uint16_t known_flags);

const char *plat_desc_verdict_name(plat_desc_verdict_t v);

/* Заполнить заголовок своей стороной. */
void plat_desc_init(plat_desc_hdr_t *h, size_t own_size, uint16_t version,
                    uint16_t flags);

/* Скопировать дескриптор, СОХРАНИВ хвост, которого мы не понимаем (S454).
 * dst_cap — сколько байт доступно в приёмнике. Возвращает число скопированных
 * байт или -errno. */
long plat_desc_copy_preserving(void *dst, size_t dst_cap,
                               const void *src, size_t own_size);
include/platx/diag.h
/* platx/diag.h — bootstrap log. Full log/ is a subscriber, not Core. */
typedef enum plat_diag_lvl {
    PLAT_DIAG_DEBUG = 0,
    PLAT_DIAG_INFO,
    PLAT_DIAG_WARN,
    PLAT_DIAG_ERROR
} plat_diag_lvl_t;

typedef struct plat_diag_api {
    void (*write)(plat_diag_lvl_t lvl, const char *comp,
                  _Printf_format_string_ const char *fmt, ...);
    void (*write)(plat_diag_lvl_t lvl, const char *comp, const char *fmt, ...)
        __attribute__((format(printf, 3, 4)));
} plat_diag_api_t;
include/platx/diag_endpoint.h
/* platx/diag_endpoint.h — Authenticated read-only diagnostic endpoints (INT-116). */
#define PLAT_DIAG_PATH_MAX   64
#define PLAT_DIAG_BUF_MAX    4096

typedef enum {
    PLAT_DIAG_OK     = 0,
    PLAT_DIAG_NOLINK = 1,
    PLAT_DIAG_DENIED = 2,
    PLAT_DIAG_ERROR  = 3,
} plat_diag_status_t;

/* Read-only diagnostic handler: fills buf, returns bytes written or -1. */
typedef int (*plat_diag_handler_fn)(char *buf, size_t bufsz, void *ud);

/* Register a read-only diagnostic endpoint at path.  0/-1. */
int plat_diag_register(const char *path,
                       plat_diag_handler_fn fn, void *ud);

/* Read a diagnostic endpoint with capability auth.  Returns bytes/-1. */
int plat_diag_read(const char *path, uint64_t ctx_id, uint32_t ctx_gen,
                   char *buf, size_t bufsz);
include/platx/enrich_provider.h
/* platx/enrich_provider.h — enrich_provider_v1 (D055): DEFERRED ABI.
 *
 * STATUS: contract only. There is no product implementation of this ABI in
 * Wave 2, and adding one is a gate failure, not a feature
 * (tests/cli/provider_enrich_deferred_gate.sh).
 *
 * Why it is written now and implemented later. Enrichment is the step where
 * an observation acquires context it did not carry: a hash becomes a
 * reputation, a path becomes a package, an address becomes an owner. Every one
 * of those lookups is slower than the fact, may fail, may be attacker-visible
 * and — this is the part that decides the design — is UNTRUSTED INPUT
 * (Constitution C27). Enrichment that writes into the observation record
 * silently promotes third-party text to evidence.
 *
 * So the contract is fixed here while it costs nothing:
 *   - enrichment never mutates an observation. It emits a separate record
 *     that references the observation by identity (sense_enrich_ref_t);
 *   - every enrichment carries `taint` and its source; a consumer that cannot
 *     handle tainted values must be able to drop it by looking at one field;
 *   - enrichment is asynchronous and droppable. Nothing in the decision path
 *     may block on it, and its absence is never an error — it is a field that
 *     is not there;
 *   - enrichment has its own budget and its own privacy lease. It is the one
 *     provider class that talks to things outside the host.
 *
 * ABI freeze: deferred. The declarations below may still change; anything
 * depending on them must be marked deferred as well.
 */

/* The marker the gate greps for. Removing it does not enable the ABI; it just
 * makes the gate red, which is the intended relationship. */
#define ENRICH_PROVIDER_ABI_DEFERRED  1
#define ENRICH_PROVIDER_ABI           0u   /* 0 = not frozen, not implemented */

/* Taint classes (AI seam AI019): where a value came from decides what may be
 * done with it, not how plausible it looks. */
typedef enum enrich_taint {
    ENRICH_TAINT_NONE     = 0,  /* computed locally from trusted input */
    ENRICH_TAINT_LOCAL    = 1,  /* local cache or database */
    ENRICH_TAINT_REMOTE   = 2,  /* fetched from outside the host */
    ENRICH_TAINT_ATTACKER = 3,  /* derived from data the subject controls */
    ENRICH_TAINT_MAX
} enrich_taint_t;

typedef struct enrich_request {
    uint32_t abi_version;
    uint32_t struct_size;
    uint64_t observation_id_hi;
    uint64_t observation_id_lo;
    uint32_t enrichment_type;
    uint32_t _pad;
    prov_budget_t budget;
} enrich_request_t;

typedef struct enrich_result {
    uint32_t abi_version;
    uint32_t struct_size;
    uint32_t taint;             /* enrich_taint_t; never NONE for remote */
    uint32_t data_len;
    uint64_t produced_at_ns;
    uint8_t  data[512];
    char     source[PROV_REASON_MAX];
} enrich_result_t;

/* Deferred vtable. Declared so that dependants can be written against a stable
 * shape; not registered anywhere, and no implementation exists. */
typedef struct enrich_provider_v1 {
    uint32_t abi_version;
    uint32_t struct_size;
    void    *ctx;

    int (*describe)(void *ctx, prov_envelope_t *out, prov_status_t *st);
    int (*enrich)(void *ctx, const enrich_request_t *req,
                  enrich_result_t *out, prov_status_t *st);
    int (*health)(void *ctx, prov_health_t *out, prov_status_t *st);

    void *reserved[8];
} enrich_provider_v1_t;
include/platx/epoch.h
/* epoch.h — S424/S425: сроки переживают перезагрузку честно.
 *
 * ЗАДАЧА
 * ──────
 * Срок аренды, одноразовый токен и любой дедлайн считаются по МОНОТОННЫМ
 * часам — они не скачут от NTP и `date -s`. Но монотонные часы обнуляются при
 * перезагрузке: сохранённый дедлайн «через 300 секунд от загрузки» после
 * перезагрузки означает совсем другой момент, а выглядит так же.
 *
 * Поэтому вместе с дедлайном сохраняется ЭПОХА ЗАГРУЗКИ. При чтении:
 *   эпоха совпала  → дедлайн осмыслен, сравниваем;
 *   эпоха другая   → дедлайн БЕССМЫСЛЕН, аренда считается истёкшей.
 *
 * Истёкшей, а не «ошибкой чтения» и не «ещё живой». Считать её живой значит
 * продлевать доступ перезагрузкой; считать ошибкой — терять различие между
 * «не знаю» и «сломано». Отказ выбран в сторону, которая не выдаёт прав.
 *
 * НАСТЕННЫЕ ЧАСЫ (S425)
 * ─────────────────────
 * Их сдвиг ФИКСИРУЕТСЯ как факт для оператора, но ни одно решение о сроке или
 * доступе на них не опирается. Это разные вещи: заметить скачок полезно,
 * принимать по нему решения — нет.
 */
#define PLAT_EPOCH_ID_MAX 64

typedef struct {
    char     id[PLAT_EPOCH_ID_MAX];  /* устойчивый идентификатор загрузки */
    uint64_t mono_ns;                /* CLOCK_MONOTONIC на момент снимка  */
    int64_t  wall_s;                 /* CLOCK_REALTIME, только как факт   */
} plat_epoch_t;

/* Снять текущую эпоху. 0 / -errno. */
int plat_epoch_now(plat_epoch_t *out);

typedef enum {
    PLAT_LEASE_LIVE     = 0,
    PLAT_LEASE_EXPIRED  = 1,  /* срок вышел по монотонным часам           */
    PLAT_LEASE_REBOOTED = 2,  /* другая эпоха: дедлайн бессмыслен         */
    PLAT_LEASE_MALFORMED= 3   /* запись нечитаема                          */
} plat_lease_state_t;

typedef struct {
    char     epoch_id[PLAT_EPOCH_ID_MAX];
    uint64_t deadline_mono_ns;
    uint32_t one_shot;        /* 1 — токен сгорает при первом употреблении */
    uint32_t used;
} plat_lease_t;

/* Выдать аренду на ttl_ms от текущего момента. */
int plat_lease_issue(plat_lease_t *out, uint64_t ttl_ms, int one_shot);

/* Состояние аренды ПРЯМО СЕЙЧАС. Не меняет её. */
plat_lease_state_t plat_lease_state(const plat_lease_t *l);

/* Употребить одноразовый токен. 0 — употреблён впервые; -EPERM — уже был или
 * недействителен. Второе употребление обязано отказать даже в ту же секунду. */
int plat_lease_consume(plat_lease_t *l);

/* Сдвиг настенных часов между двумя снимками эпохи, в секундах.
 * Возвращает 0, если эпохи разные (сравнивать нечего) — см. *comparable. */
int64_t plat_wall_drift(const plat_epoch_t *before, const plat_epoch_t *now,
                        int *comparable);
include/platx/event.h
/* platx/event.h — in-process fan-out. Not a bus, not JSON, not durable. */
#define PLAT_EV_RING_CAP 64u
#define PLAT_EV_PAY_MAX 256u
#define PLAT_EVENT_HAS_STATS 1

typedef uint32_t plat_event_id_t;
typedef uint64_t plat_sub_id_t;

typedef void (*plat_event_fn)(plat_event_id_t id, const void *payload,
                              size_t len, void *ud);

typedef struct plat_event_stats {
    uint64_t emitted;    /* accepted emit() calls, including overflow drops */
    uint64_t dropped;    /* overwrite of oldest + payload > PLAT_EV_PAY_MAX */
    uint32_t queued;     /* current ring occupancy */
    uint32_t capacity;   /* PLAT_EV_RING_CAP */
} plat_event_stats_t;

typedef struct plat_event_api {
    int          (*emit)(plat_event_id_t id, const void *payload, size_t len);
    plat_sub_id_t (*subscribe)(plat_owner_t owner, plat_event_id_t id,
                               plat_event_fn fn, void *ud);
    /* Returns after in-flight emit finishes (unless called from that emit). */
    int          (*unsubscribe)(plat_sub_id_t sub);
} plat_event_api_t;

/*
 * emit copies into a bounded ring (no malloc). It never waits for a
 * consumer. Full ring: drop oldest, increment dropped, keep the new fact.
 * Callback sees a copy valid only for that call. It must not free or retain it.
 */
int  plat_event_init(void);
void plat_event_fini(void);
int  plat_event_emit(plat_event_id_t id, const void *payload, size_t len);
int  plat_event_stats(plat_event_stats_t *out);
/* Snapshot last ids. Does not emit, pop, or change overflow. Newest last. */
int  plat_event_recent(uint32_t *ids, uint32_t max);
/* Drop leftover subs on DESTROY. generation 0 is a no-op.
 * Waits out a concurrent emit so ud can be freed after return. */
int  plat_event_unsub_owner(plat_owner_t owner);
extern const plat_event_api_t plat_event_api;

/* Well-known core events. Domain modules define their own IDs ≥ 0x1000. */
#define PLAT_EV_MODULE_LOAD     1u
#define PLAT_EV_MODULE_START    2u
#define PLAT_EV_MODULE_STOP     3u
#define PLAT_EV_MODULE_FAIL     4u
#define PLAT_EV_RECOVERY        5u
#define PLAT_EV_SERVICE_REG     6u
#define PLAT_EV_SERVICE_REV     7u
#define PLAT_EV_BOOT            8u  /* stage name after enter; "ready" only when platform_ready */

/* XIM: the parent answered for one mediated CHILD syscall. Payload is
 * plat_xim_fact_t (platx/xim.h): ids and numbers, no pointers. A fact, not a
 * command -- nothing downstream may turn it into start/stop/restart. */
/* Hook Engine. Payload is plat_hook_fact_t (platx/hook.h). These describe
 * attach, detach and refusal -- lifecycle of the interposition itself. Per
 * invocation facts deliberately do not live here: one event per read would
 * drown a ring of 64 and starve every other subscriber. Counters and, later,
 * a recorder carry those. */
#define PLAT_EV_HOOK_ATTACH     0x1200u
#define PLAT_EV_HOOK_DETACH     0x1201u
#define PLAT_EV_HOOK_ERROR      0x1202u
#define PLAT_EV_HOOK_POLICY     0x1203u  /* a decision the runtime applied */

#define PLAT_EV_XIM_DECISION    0x1300u

/* Hades / eBPF (platx/hades_capsule.h). RARE lifecycle facts only — a profile
 * committed, dropped, degraded. Payload <= 256 bytes, no pointers: capsule_id,
 * profile_id, generation, epoch, quality, lost, reason. The dense per-hit
 * observations (an exec, a connect, an open) never live here — they ride the
 * Hades DOMAIN ring, exactly as Hook per-invocation facts stay off this ring.
 * HADES_ATTACH fires when a profile is committed AFTER shadow verification, not
 * after a bare BPF load. */
#define PLAT_EV_HADES_ATTACH    0x1500u  /* profile committed after shadow */
#define PLAT_EV_HADES_DETACH    0x1501u  /* cleanly removed */
#define PLAT_EV_HADES_ERROR     0x1502u  /* load/attach/BTF/shadow refusal */
#define PLAT_EV_HADES_DEGRADED  0x1503u  /* loss, adaptive transition, integrity mismatch */
#define PLAT_EV_HADES_DROPPED   0x1504u  /* lost threshold crossed (not every drop) */
#define PLAT_EV_HADES_PROFILE   0x1505u  /* apply/reload: new profile/epoch generation */

/* 0x1600..0x1601 are reserved for a retired laboratory subsystem. Do not reuse. */
include/platx/event_bound.h
/* platx/event_bound.h — A1-P05. Bounded plat_event: наблюдаемые ёмкости,
 * ограниченный unsubscribe, барьер остановки, маркеры для аудита.
 *
 * Это расширение platx/event.h, а не второй event engine (C20). Кольцо,
 * таблица подписчиков и диспетчер — те же самые, один TU src/core/plat_event.c.
 * Здесь объявлено то, чего в старом контракте не было и из-за отсутствия чего
 * нельзя было ни доказать границу, ни отличить потерю от тишины.
 *
 * Контракт волны 5 (A1-EVENT-IF). Стабильные точки для A2/A3/A4:
 *   - счётчик на каждую ёмкость (кольцо, таблица подписчиков, payload, вложенность);
 *   - токен подписки, привязанный к epoch подсистемы и к слоту таблицы;
 *   - unsubscribe с внешним deadline, не зависающий на чужом callback;
 *   - неблокирующий снимок счётчиков (не берёт лок диспетчера вовсе);
 *   - маркер seq/drop/restart, по которому аудит отличает gap от тишины;
 *   - owner эмитента в факте (ответ на A2 H12).
 */

/* ── Ёмкости. Каждая имеет отдельный счётчик исчерпания (A1-P05-201) ───── */
#define PLAT_EV_MAX_SUB 64
/* Бюджет вложенного emit. Раньше глубина ограничивалась только числом слотов
 * (64 кадра ev_emit ≈ 80 KiB стека) и нигде не была объявлена. Теперь это
 * явное число, и превышение считается, а не молчит. */
#define PLAT_EV_MAX_NEST 4
/* Сколько owner'ов одновременно могут держать STOPPING-барьер. */
#define PLAT_EV_MAX_BARRIER 16
/* Порог, после которого доставка одному подписчику объявляется медленной.
 * Это ТОЛЬКО наблюдение: диспетчер не отписывает медленного, не перезапускает
 * его и не меняет маршрут. Факт задержки передаётся, решение остаётся за
 * Recovery (A1-P05-229). */
#define PLAT_EV_SLOW_NS (10ull * 1000000ull)   /* 10 мс */

#define PLAT_EVENT_BOUND_ABI 1

/* ── Коды отказа. Отрицательные, не пересекаются с 0/-1 старого API ────── */

#define PLAT_EV_OK               0
#define PLAT_EV_E_ARGS         (-2)   /* NULL/нулевой id/generation 0 */
#define PLAT_EV_E_TOOBIG       (-3)   /* payload > PLAT_EV_PAY_MAX (A1-P05-219) */
#define PLAT_EV_E_NOT_READY    (-4)   /* подсистема не инициализирована */
#define PLAT_EV_E_FULL         (-5)   /* таблица подписчиков исчерпана */
#define PLAT_EV_E_BARRIER      (-6)   /* owner в STOPPING (A1-P05-215) */
#define PLAT_EV_E_STALE        (-7)   /* токен не из этого epoch/слота */
#define PLAT_EV_E_TIMEOUT      (-8)   /* deadline истёк, detach сохранён */
#define PLAT_EV_E_BUSY         (-9)   /* снимок не удался без ожидания */
#define PLAT_EV_E_SEQ_EXHAUST (-10)   /* пространство токенов исчерпано */
#define PLAT_EV_E_NEST        (-11)   /* превышен бюджет вложенности */
#define PLAT_EV_E_VERSION     (-12)   /* неизвестная обязательная версия факта */

/* ── Раскладка токена подписки (A1-P05-208) ────────────────────────────────
 *
 * Старый токен был счётчиком, обнулявшимся в plat_event_init(). После
 * fini/init он выдавался заново с 1, и токен прошлого поколения успешно
 * отписывал нового подписчика другого owner. Теперь токен несёт epoch
 * подсистемы и индекс слота, поэтому ни перезапуск, ни переиспользование
 * слота не делают старый токен снова валидным.
 *
 *   биты 63..48  epoch  — инкремент на каждый plat_event_init(), не сбрасывается
 *   биты 47..40  slot   — индекс в таблице, прямой поиск без сканирования
 *   биты 39..0   seq    — монотонный внутри epoch, никогда не 0
 */
#define PLAT_EV_TOK_EPOCH_SHIFT 48
#define PLAT_EV_TOK_SLOT_SHIFT  40
#define PLAT_EV_TOK_SEQ_MASK    ((uint64_t)0xFFFFFFFFFFull)
#define PLAT_EV_TOK_SLOT_MASK   ((uint64_t)0xFFull)
#define PLAT_EV_TOK_EPOCH_MASK  ((uint64_t)0xFFFFull)

/* Предел выдачи токенов внутри epoch. По умолчанию — вся разрядность seq.
 * Отдельная константа, а не сама маска: тестовая сборка уменьшает предел,
 * не трогая раскладку токена, поэтому исчерпание проверяется на настоящей
 * реализации, а не на её изменённой копии (A1-P05-245). */
#define PLAT_EV_SUB_SEQ_LIMIT PLAT_EV_TOK_SEQ_MASK

uint32_t plat_ev_tok_epoch(plat_sub_id_t t); /* inline interface */
uint32_t plat_ev_tok_slot(plat_sub_id_t t); /* inline interface */
uint64_t plat_ev_tok_seq(plat_sub_id_t t); /* inline interface */

/* ── Состояние подписки: логический detach отделён от освобождения ─────────
 * (A1-P05-213). DETACHED слот больше не выбирается новыми emit, но его
 * callback storage жив, пока держится хоть одна inflight-ссылка. */
typedef enum plat_ev_sub_state {
    PLAT_EV_SUB_FREE     = 0,
    PLAT_EV_SUB_LIVE     = 1,
    PLAT_EV_SUB_DETACHED = 2
} plat_ev_sub_state_t;

typedef struct plat_ev_sub_info {
    plat_ev_sub_state_t state;
    int                 inflight;   /* удерживаемые снимки этого слота */
    plat_owner_t        owner;
    plat_event_id_t     ev;
} plat_ev_sub_info_t;

/* ── Расширенные счётчики (A1-P05-201/222/226/230) ─────────────────────────
 *
 * Читаются без лока диспетчера: все поля — атомики, снимок не блокируется
 * зависшим callback'ом. Поэтому снимок не мгновенный срез одного момента;
 * инварианты, которые обязаны держаться, перечислены в комментариях. */
typedef struct plat_event_stats2 {
    uint32_t version;            /* PLAT_EVENT_BOUND_ABI */
    uint32_t epoch;              /* поколение подсистемы (init) */
    uint64_t restarts;           /* сколько раз выполнялся init после fini */

    /* Кольцо фактов. */
    uint64_t emitted;            /* принятые факты (без отказов) */
    uint64_t seq_last;           /* seq последнего принятого факта */
    uint64_t seq_first_live;     /* seq самого старого живого слота кольца */
    uint32_t queued;             /* занятость кольца, <= ring_capacity всегда */
    uint32_t ring_capacity;      /* PLAT_EV_RING_CAP */
    uint64_t dropped_ring;       /* вытеснено переполнением кольца */

    /* Отказы до резервирования — кольцо не тронуто. */
    uint64_t rejected_oversize;  /* payload > PLAT_EV_PAY_MAX */
    uint64_t rejected_args;      /* id==0 / payload==NULL при len>0 */
    uint64_t rejected_not_ready;

    /* Доставка. Факт принят, но конкретный подписчик его не увидел. */
    uint64_t undelivered_reentry;/* слот уже в стеке этого потока */
    uint64_t undelivered_nest;   /* исчерпан PLAT_EV_MAX_NEST */
    uint64_t undelivered_detach; /* слот отсоединён между снимком и вызовом */
    uint32_t nest_depth_max;     /* наибольшая наблюдённая глубина */

    /* Таблица подписчиков. */
    uint32_t sub_active;         /* LIVE слоты */
    uint32_t sub_detached;       /* DETACHED, ещё не освобождённые */
    uint32_t sub_capacity;       /* PLAT_EV_MAX_SUB */
    uint64_t sub_reject_full;    /* отказ из-за исчерпания таблицы */
    uint64_t sub_reject_args;    /* generation 0, NULL fn, id 0 */
    uint64_t sub_reject_barrier; /* owner в STOPPING */
    uint64_t sub_reject_seq;     /* исчерпано пространство токенов */

    /* Ожидания. */
    uint64_t unsub_timeouts;     /* deadline истёк, detach сохранён */
    uint32_t barriers_active;

    /* Медленный подписчик (A1-P05-229). Факт, не действие: ни одна из этих
     * величин не приводит к автоматической отписке или перезапуску. */
    uint64_t slow_dispatches;    /* доставок дольше PLAT_EV_SLOW_NS */
    uint64_t slow_ns_max;        /* наибольшая наблюдённая доставка, нс */
    uint32_t slow_slot_last;     /* слот последней медленной доставки */
    uint32_t slow_threshold_ms;  /* объявленный порог, чтобы читатель знал его */
} plat_event_stats2_t;

/* Неблокирующий снимок. Не берёт лок диспетчера; возвращает PLAT_EV_E_ARGS
 * при out==NULL и PLAT_EV_E_NOT_READY до init. Никогда не PLAT_EV_E_BUSY —
 * оставлено в кодах для реализаций, которым понадобится trylock. */
int plat_event_stats2(plat_event_stats2_t *out);

/* ── Маркер для аудита (A1-P05-231) ────────────────────────────────────────
 * Позволяет отличить «событий не было» от «события были и потеряны».
 * Правило для потребителя: если seq_first_live > last_seen_seq + 1, между
 * ними подтверждённый gap ровно (seq_first_live - last_seen_seq - 1) фактов.
 * Если epoch отличается от прошлого чтения — был restart, и непрерывность
 * seq через границу epoch не заявляется. */
typedef struct plat_event_marker {
    uint32_t version;
    uint32_t epoch;
    uint64_t restarts;
    uint64_t seq_last;
    uint64_t seq_first_live;
    uint64_t dropped_total;      /* dropped_ring + rejected_oversize */
} plat_event_marker_t;

int plat_event_marker(plat_event_marker_t *out);

/* ── Owner эмитента в факте — ответ на A2 H12 (A1-P05-232) ────────────────
 * Старый plat_event_fn не отдавал owner того, кто выпустил факт, поэтому мост
 * plat_event→mbus мог проставить только свой. Расширенный callback получает
 * метаданные; флаг отличает «эмитент неизвестен» от «эмитент — owner 0/0/0».
 * Обратный мост из этого контракта не включается: emit_owner ничего не
 * подписывает и никуда не публикует. */
#define PLAT_EVENT_META_V1 1u
#define PLAT_EV_F_EMITTER_KNOWN 0x1u

typedef struct plat_event_meta {
    uint32_t        version;     /* PLAT_EVENT_META_V1 */
    uint32_t        flags;
    plat_event_id_t id;
    uint32_t        epoch;
    uint64_t        seq;
    plat_owner_t    emitter;     /* валиден только при PLAT_EV_F_EMITTER_KNOWN */
} plat_event_meta_t;

typedef void (*plat_event_fn2)(const plat_event_meta_t *meta, const void *payload,
                               size_t len, void *ud);

plat_sub_id_t plat_event_subscribe_ex(plat_owner_t owner, plat_event_id_t id,
                                      plat_event_fn2 fn, void *ud);
int plat_event_emit_owner(plat_owner_t emitter, plat_event_id_t id,
                          const void *payload, size_t len);

/* ── Ограниченный по времени unsubscribe (A1-P05-214) ──────────────────────
 * timeout_ns == 0 — только попытка без ожидания. При истечении возвращает
 * PLAT_EV_E_TIMEOUT, слот остаётся DETACHED: новые emit его не выбирают,
 * доставка прекращена, освобождение произойдёт, когда уйдёт последняя
 * inflight-ссылка. Состояние достоверно и читается plat_event_sub_state(). */
int plat_event_unsub_deadline(plat_sub_id_t sub, uint64_t timeout_ns);
int plat_event_unsub_owner_deadline(plat_owner_t owner, uint64_t timeout_ns,
                                    int *detached_out);

int plat_event_sub_state(plat_sub_id_t sub, plat_ev_sub_info_t *out);

/* ── Барьер остановки owner (A1-P05-215) ──────────────────────────────────
 * После begin новые подписки этого owner отвергаются с PLAT_EV_E_BARRIER,
 * поэтому подписка либо уже учтена в drain, либо не создана. begin не
 * отписывает существующие: это делает unsub_owner_deadline. */
int plat_event_barrier_begin(plat_owner_t owner);
int plat_event_barrier_end(plat_owner_t owner);
int plat_event_barrier_active(plat_owner_t owner);

/* ── Типизированный payload и его версия (A1-P05-218/220) ─────────────────
 * Заголовок факта. Диспетчер его не интерпретирует — он копирует байты.
 * Проверка версии живёт в consumer adapter, и неизвестная обязательная
 * версия обязана стать отказом, а не «декодируем как сможем». */
typedef struct plat_event_fact_hdr {
    uint16_t version;
    uint16_t body_len;
} plat_event_fact_hdr_t;

/* Возвращает PLAT_EV_OK, PLAT_EV_E_ARGS или PLAT_EV_E_VERSION. */
int plat_event_fact_accept(const void *payload, size_t len,
                           uint16_t min_ver, uint16_t max_ver,
                           plat_event_fact_hdr_t *hdr_out);
include/platx/fleet_plan.h
/* platx/fleet_plan.h -- immutable Central/Node planning and config CAS.
 *
 * This layer contains no transport and performs no node-side effect.  It is
 * the stable boundary shared by a central coordinator, mesh fan-out, DSL/MSX
 * dry-run and the evidence writer.  Execution adapters may consume a plan,
 * but cannot make an incomplete plan look globally successful. */

#define PLAT_FLEET_PLAN_VERSION 1u
#define PLAT_FLEET_MAX_NODES 64u
#define PLAT_FLEET_ID_MAX 64u
#define PLAT_FLEET_REASON_MAX 160u

typedef enum {
    PLAT_FLEET_NODE_PLANNED = 1,
    PLAT_FLEET_NODE_APPLIED,
    PLAT_FLEET_NODE_REFUSED,
    PLAT_FLEET_NODE_FAILED,
    PLAT_FLEET_NODE_UNAVAILABLE,
    PLAT_FLEET_NODE_EXPIRED,
    PLAT_FLEET_NODE_UNCOMPENSATED
} plat_fleet_node_state_t;

typedef enum {
    PLAT_FLEET_CAS_APPLIED = 0,
    PLAT_FLEET_CAS_IDEMPOTENT = 1,
    PLAT_FLEET_CAS_STALE = -1,
    PLAT_FLEET_CAS_CONFLICT = -2,
    PLAT_FLEET_CAS_INVALID = -3
} plat_fleet_cas_result_t;

typedef enum {
    PLAT_FLEET_GLOBAL_SUCCESS = 0,
    PLAT_FLEET_GLOBAL_PARTIAL_FAILURE = 1,
    PLAT_FLEET_GLOBAL_INVALID = 2
} plat_fleet_global_result_t;

typedef struct {
    uint64_t generation;
    uint8_t  digest[32];
    char     last_request_id[PLAT_FLEET_ID_MAX];
    uint64_t last_base_generation;
    uint64_t last_desired_generation;
    uint8_t  last_desired_digest[32];
} plat_fleet_config_state_t;

typedef struct {
    const char *request_id;
    uint64_t    base_generation;
    uint64_t    desired_generation;
    uint8_t     desired_digest[32];
} plat_fleet_config_patch_t;

typedef struct {
    char                    node_id[PLAT_FLEET_ID_MAX];
    plat_fleet_node_state_t state;
    char                    action[PLAT_FLEET_ID_MAX];
    char                    reason[PLAT_FLEET_REASON_MAX];
} plat_fleet_node_plan_t;

typedef struct {
    uint32_t               version;
    char                   plan_id[PLAT_FLEET_ID_MAX];
    uint64_t               config_generation;
    uint64_t               expires_at_ms;
    int                    dry_run;
    size_t                 node_count;
    plat_fleet_node_plan_t nodes[PLAT_FLEET_MAX_NODES];
} plat_fleet_plan_t;

plat_fleet_cas_result_t plat_fleet_config_cas(
    plat_fleet_config_state_t *state,
    const plat_fleet_config_patch_t *patch);

int plat_fleet_plan_init(plat_fleet_plan_t *plan, const char *plan_id,
                         uint64_t config_generation, uint64_t expires_at_ms,
                         int dry_run);
int plat_fleet_plan_add_node(plat_fleet_plan_t *plan, const char *node_id,
                             const char *action, plat_fleet_node_state_t state,
                             const char *reason);
plat_fleet_global_result_t plat_fleet_plan_result(
    const plat_fleet_plan_t *plan, uint64_t now_ms);

/* Deterministic JSON evidence.  Refuses truncation and invalid plans. */
int plat_fleet_plan_evidence_json(const plat_fleet_plan_t *plan,
                                  uint64_t now_ms, char *out, size_t out_size);
const char *plat_fleet_node_state_name(plat_fleet_node_state_t state);
include/platx/gencommit.h
/* gencommit.h — S410/S411/S435/S443: две генерации, третьей не бывает.
 *
 * Договор: на диске в любой момент лежит ЛИБО целиком прежняя генерация, ЛИБО
 * целиком новая. Смешанной не бывает никогда — ни при отказе проверки, ни при
 * смерти процесса на любом шаге.
 *
 * Порядок:
 *   1. кандидат пишется в <base>.next атомарной заменой;
 *   2. кандидат ПЕРЕЧИТЫВАЕТСЯ С ДИСКА и проверяется;
 *   3. годный — атомарно занимает <base>.current;
 *   4. негодный — остаётся под <base>.rejected вместе с <base>.rejected.why.
 *
 * Шаг 2 читает с диска, а не проверяет буфер в памяти: проверять надо то, что
 * действительно записалось. Буфер мог быть верным, а записанное — нет, и
 * различие видно только так.
 *
 * Отвергнутая генерация НЕ УДАЛЯЕТСЯ (S443). Она — единственное объяснение
 * того, почему система работает на прежней конфигурации, и стирать её значит
 * оставить оператора без ответа на первый же вопрос.
 */
struct xio_t;

/* Проверка кандидата. 0 — годен. Отрицательное — негоден; why заполняется
 * причиной для оператора. */
typedef int (*plat_gen_validate_fn)(const void *data, size_t len,
                                    char *why, size_t why_sz, void *ud);

typedef enum {
    PLAT_GEN_COMMITTED = 0,   /* новая генерация стала текущей   */
    PLAT_GEN_REJECTED  = 1    /* текущая не тронута, кандидат отложен */
} plat_gen_result_t;

/* 0 / -errno. Что произошло — в *res. */
int plat_gen_commit(const struct xio_t *io, const char *base,
                    const void *data, size_t len,
                    plat_gen_validate_fn validate, void *ud,
                    plat_gen_result_t *res);

/* Прочитать текущую генерацию. Возвращает длину, либо -errno.
 * -ENOENT означает «генерации ещё нет» — это ответ, а не сбой. */
long plat_gen_load(const struct xio_t *io, const char *base,
                   void *buf, size_t cap);

/* Причина последнего отказа, если она есть. Длина или -errno. */
long plat_gen_why(const struct xio_t *io, const char *base,
                  char *buf, size_t cap);
include/platx/host.h
/* platx/host.h — one descriptor, two execution hosts.
 *
 * EMBEDDED: module shares the worker address space. plat_abi_t* is real.
 * CHILD:    module is a separate process. It receives a virtual plat_abi
 *           whose functions serialize to the worker. provides[] are
 *           registered in the worker as proxy vtables, never as child
 *           function pointers.
 *
 * Isolation is chosen by the deployment profile from
 * isolation_preference / isolation_allowed. It is not a module type.
 *
 * Stub never launches children. Core child_host does:
 *   exec + sandbox profile (seccomp, rlimits, namespaces, no_new_privs).
 */
/* CHILD spawn is plat_child_host_* in child_host.h — Core, not stub. */

typedef enum plat_host_kind {
    PLAT_HOST_EMBEDDED = 0,
    PLAT_HOST_CHILD    = 1
} plat_host_kind_t;
include/platx/incident_env.h
/* platx/incident_env.h — INT-145: incident result envelope.
 *
 * Every output from MSX/DSL/Fabric/Hades is wrapped in a typed envelope
 * before it is stored or forwarded. The envelope adds provenance, timestamps,
 * and a correlation chain so a central node can link inputs to outputs.
 */

#define PLAT_ENV_SOURCE_MAX  32
#define PLAT_ENV_KIND_MAX    32
#define PLAT_ENV_CORR_MAX    64
#define PLAT_ENV_REASON_MAX  256
#define PLAT_ENV_BODY_MAX    2048

typedef struct plat_incident_env {
    char     source[PLAT_ENV_SOURCE_MAX];    /* subsystem producing this */
    char     kind[PLAT_ENV_KIND_MAX];        /* event type label         */
    char     correlation_id[PLAT_ENV_CORR_MAX]; /* chain: req→resp→audit */
    uint64_t issued_at_ms;      /* monotonic ms */
    uint64_t context_id;        /* plat_context handle.id (0 if none)   */
    uint64_t context_gen;       /* plat_context handle.generation        */
    uint64_t lease_id;          /* lease used (0 if none)               */
    char     body[PLAT_ENV_BODY_MAX]; /* free-form JSON or text payload  */
    char     reason[PLAT_ENV_REASON_MAX];    /* human-readable outcome   */
    int      success;           /* 0 = failure, 1 = success             */
} plat_incident_env_t;

void plat_env_init(plat_incident_env_t *env,
                   const char *source, const char *kind,
                   const char *correlation_id);

void plat_env_set_context(plat_incident_env_t *env,
                          uint64_t ctx_id, uint64_t ctx_gen);

void plat_env_set_lease(plat_incident_env_t *env, uint64_t lease_id);

void plat_env_set_body(plat_incident_env_t *env, const char *body);

void plat_env_finish(plat_incident_env_t *env, int success, const char *reason);

/* Emit the envelope to audit. No-op if audit_write not linked. */
void plat_env_audit(const plat_incident_env_t *env);
include/platx/jrec.h
/* jrec.h — S459/S460/S490: версия схемы у записи-строки и сохранение чужих полей.
 *
 * ЧТО ЗДЕСЬ РЕШАЕТСЯ
 * ──────────────────
 * Аудит, доказательства и трассы пишутся построчно как JSON-объекты. Такой
 * файл переживает выпуск, который его написал: его читает следующая версия,
 * его переписывает сторонний инструмент, его несут в другую систему. Значит
 * это ФОРМАТ, а не «просто лог», и у него обязаны быть три свойства.
 *
 * S459  У записи есть номер схемы, и для КАЖДОЙ поддерживаемой версии есть
 *       читатель, доказанный на замороженном образце. «Мы всё ещё умеем
 *       читать старое» без образца — это надежда, а не совместимость.
 *       Строка БЕЗ поля "schema" — это схема 1: так писал первый выпуск, и
 *       переписать историю задним числом нельзя.
 *
 * S460  Инструмент, переписывающий запись, обязан отдать НЕИЗВЕСТНЫЕ ему поля
 *       дословно. Потеря поля выглядит как решение автора: читатель не может
 *       отличить «этого не было» от «это выкинули по дороге». Поэтому чужие
 *       поля едут как сырой текст, в исходном порядке.
 *
 * S490  Читаем старое — пишем ТОЛЬКО текущее. Запись, отданная наружу, всегда
 *       объявляет текущую схему: иначе в файле окажется смесь, о которой никто
 *       не договаривался. И осмотр — только чтение: разбор записи не меняет ни
 *       байта на диске.
 *
 * ЧЕГО ЗДЕСЬ НЕТ, И ЭТО НАМЕРЕННО
 * ───────────────────────────────
 * Это не парсер JSON. Здесь режется ОДНА строка ОДНОГО объекта на пары
 * ключ/значение, и значение не разбирается вовсе — оно едет как текст. Полный
 * разбор не нужен для того, чтобы ничего не потерять, а лишний разбор — это
 * лишний способ потерять.
 */
#define PLAT_JREC_MAX_LINE   4096
#define PLAT_JREC_MAX_FIELDS 48

typedef enum {
    PLAT_JREC_AUDIT    = 0,
    PLAT_JREC_EVIDENCE = 1,
    PLAT_JREC_TRACE    = 2,
    PLAT_JREC_KIND_N   = 3
} plat_jrec_kind_t;

typedef enum {
    PLAT_JREC_OK        = 0,
    PLAT_JREC_MALFORMED = 1,
    PLAT_JREC_TOO_NEW   = 2,  /* схема новее текущей — отказ, не «как сможем» */
    PLAT_JREC_TOO_OLD   = 3,  /* схема старше поддерживаемой                  */
    PLAT_JREC_OVERFLOW  = 4,
    PLAT_JREC_BAD_ARG   = 5
} plat_jrec_verdict_t;

typedef struct {
    uint16_t koff, klen;   /* смещения в jrec.buf: ключ без кавычек   */
    uint16_t voff, vlen;   /* сырой текст значения, как в исходнике   */
    uint8_t  known;        /* 1 — поле известно ТЕКУЩЕЙ схеме        */
} plat_jrec_field_t;

typedef struct {
    char              buf[PLAT_JREC_MAX_LINE];
    size_t            len;
    plat_jrec_field_t f[PLAT_JREC_MAX_FIELDS];
    int               n;
    uint32_t          schema;      /* 1, если поля "schema" не было   */
    int               had_schema;
    plat_jrec_kind_t  kind;
} plat_jrec_t;

/* Границы поддержки. oldest — самая старая схема, для которой ЕСТЬ читатель. */
uint32_t plat_jrec_schema_current(plat_jrec_kind_t k);
uint32_t plat_jrec_schema_oldest (plat_jrec_kind_t k);

/* Разбор. Вход — const: осмотр ничего не меняет (S490). */
plat_jrec_verdict_t plat_jrec_parse(plat_jrec_kind_t kind,
                                    const char *line, plat_jrec_t *out);

/* Значение по ключу как сырой текст. NULL — поля нет. */
const char *plat_jrec_get(const plat_jrec_t *r, const char *key, size_t *len);

/* Сколько полей запись несёт сверх известных текущей схеме. */
int plat_jrec_unknown_count(const plat_jrec_t *r);

/* Заменить/добавить ЗНАЧЕНИЕ известного поля. rawval — готовый JSON-текст
 * (со своими кавычками, если это строка). Неизвестные поля менять нельзя:
 * не понимаешь смысл — не трогай. Возвращает 0/-1. */
int plat_jrec_set(plat_jrec_t *r, const char *key, const char *rawval);

/* Собрать строку обратно. Схема ВСЕГДА текущая (S490). Порядок полей
 * сохраняется; неизвестные поля выводятся дословно (S460). Без '\n'. */
plat_jrec_verdict_t plat_jrec_emit(const plat_jrec_t *r, char *out, size_t cap);

const char *plat_jrec_verdict_name(plat_jrec_verdict_t v);
include/platx/kernel.h
/* platx/kernel.h — static boot core that memfd links (libplatx.a).
 *
 * One process, one binary. This header is the boundary, not a second OS.
 *
 * IN  (libplatx.a / PLATX_KERNEL_SRCS — boot contract):
 *     staged boot 0..5, register + layer init/start, fail-closed ready,
 *     Core ABI, lifecycle + recovery decide/apply, keyring v1 ABI
 *
 * OUT of kernel (linked product — not libplatx.a, do not implement here):
 *     supervisor, RA2C, POE
 *     xio, log, taskmgr (KERNEL stage calls them; they are not the archive)
 *     PIC is archive — not a kernel layer
 *     DRM, plugin, vfs, mbus, mesh, script, audit
 *     src/transport / src/transports, keyring store/daemon
 *     CHILD host / IPC / abi_proxy; Event-as-bus, extra transports
 *
 * platx-core / platx-alpha are test bins. They are not a microkernel.
 */
/* --------------------------------------------------------------------------
 * Process boot stages — the one path.
 *
 * Why this axis exists: platform_ready() must mean "every mandatory
 * predecessor finished", not "something registered". A later stage
 * must not run after a mandatory fail (no half-ready: ACK without
 * channel, plugin without verify, scripts on a dead core).
 *
 * This is NOT a second state machine:
 *   plat_lifecycle  — per-module LOAD/CREATE/START (recovery requests).
 *   plat_layer      — init/start waves of already-registered subsys
 *                     (system → crypto → net → observe → automation,
 *                     then payload). subsys_boot / subsys_start_all
 *                     already walk those waves. Do not add another loop.
 *
 * Map, do not duplicate:
 *   KERNEL   owns register + the existing layer init/start of core
 *            subsys (system..automation).
 *   MODULES  owns PLAT_LAYER_PAYLOAD (PIC is archive, not kernel).
 *   CONFIG / MANIFEST / SCRIPTS / READY are process gates, not layers.
 *
 * Walk 0→5 in order. Optional work is entered then skipped, not jumped.
 * Mandatory fail at N: stay at N, do not enter N+1, platform_ready stays 0.
 * platform_ready() is true only after READY.
 * -------------------------------------------------------------------------- */

typedef enum platx_boot_stage {
    PLATX_STAGE_KERNEL   = 0, /* register + Core ABI; calls linked xio/log/taskmgr */
    PLATX_STAGE_CONFIG   = 1, /* presets / platx.conf; kv+env before DSL */
    PLATX_STAGE_MANIFEST = 2, /* DSL: what is mandatory vs optional */
    PLATX_STAGE_MODULES  = 3, /* ELF-load vfs PIC; fail closed on unwrap/bad sig */
    PLATX_STAGE_SCRIPTS  = 4, /* MSX after modules exist to serve it */
    PLATX_STAGE_READY    = 5  /* only here is the process "up" */
} platx_boot_stage_t;

#define PLATX_STAGE_COUNT  6
#define PLATX_STAGE_LAST   PLATX_STAGE_READY

const char *platx_boot_stage_name(platx_boot_stage_t s); /* inline interface */

/* Fail-closed: only the next ordinal. READY is terminal. Out-of-range
 * or a jump (skipping a stage that must be entered) is rejected. */
int platx_boot_stage_may_enter(platx_boot_stage_t from,
                                             platx_boot_stage_t to); /* inline interface */
include/platx/keyrot.h
/* keyrot.h — S480: смена ключей между узлами РАЗНЫХ версий.
 *
 * Смена ключа выглядит простой ровно до того момента, когда собеседники
 * умеют разное. Тогда появляются три разные ошибки, и все три тихие.
 *
 * 1. ВЫДАТЬ КЛЮЧ АЛГОРИТМА, КОТОРОГО У СОБЕСЕДНИКА НЕТ.
 *    Он не сможет им воспользоваться — и, если код «на всякий случай»
 *    откатится на что-нибудь общее, связь останется, а защита станет той,
 *    о которой никто не договаривался. Поэтому алгоритм нового ключа берётся
 *    только из ПЕРЕСЕЧЕНИЯ политик, и пустое пересечение — отказ, а не
 *    подстановка (см. plat_cryptopol_negotiate).
 *
 * 2. ПРОДОЛЖИТЬ ПОЛЬЗОВАТЬСЯ КЛЮЧОМ, КОТОРЫЙ БЫЛ ЗАКОННЫМ ВЧЕРА.
 *    Собеседник сузил политику — убрал алгоритм. Ключ, выданный до этого,
 *    формально исправен, но алгоритм теперь отсутствует у одной из сторон.
 *    «Отсутствует хотя бы у одной» проверяется В МОМЕНТ ИСПОЛЬЗОВАНИЯ, а не
 *    только при выдаче: политика — это то, что действует сейчас.
 *
 * 3. ПЕРЕПУТАТЬ ОКНО ПЕРЕКРЫТИЯ С РАЗРЕШЕНИЕМ.
 *    Во время смены обе стороны какое-то время принимают и старое поколение.
 *    Принимать — да. ПОДПИСЫВАТЬ старым — нет: иначе смена ключа не
 *    заканчивается никогда, и достаточно продержать окно открытым, чтобы она
 *    не состоялась вовсе.
 */
typedef struct {
    uint32_t policy;    /* маска PLAT_CP_* — что узел умеет СЕЙЧАС */
    uint16_t wire;      /* версия обрамления                       */
    uint64_t gen;       /* поколение ключа, известное узлу         */
} plat_peer_t;

typedef struct {
    uint32_t alg;       /* ОДИН бит PLAT_CP_*                      */
    uint64_t gen;
    int      valid;
} plat_key_t;

typedef enum {
    PLAT_KEYROT_OK          = 0,
    PLAT_KEYROT_NO_COMMON   = 1,  /* пустое пересечение политик         */
    PLAT_KEYROT_ALG_GONE    = 2,  /* алгоритм пропал у одной из сторон  */
    PLAT_KEYROT_STALE_GEN   = 3,  /* поколение старше принимаемого      */
    PLAT_KEYROT_NOT_FOR_NEW = 4,  /* старым поколением можно ПРИНИМАТЬ, не подписывать */
    PLAT_KEYROT_BAD_ARG     = 5
} plat_keyrot_verdict_t;

/* Выбрать алгоритм нового ключа и выдать его. Алгоритм — сильнейший из
 * пересечения по ЗАМОРОЖЕННОМУ порядку предпочтения (детерминированно).
 * При отказе *out не трогается. */
plat_keyrot_verdict_t plat_keyrot_issue(const plat_peer_t *local,
                                        const plat_peer_t *remote,
                                        plat_key_t *out);

/* Можно ли ПОДПИСЫВАТЬ этим ключом прямо сейчас. */
plat_keyrot_verdict_t plat_keyrot_may_sign(const plat_key_t *k,
                                           const plat_peer_t *local,
                                           const plat_peer_t *remote);

/* Можно ли ПРИНЯТЬ этим ключом. Окно перекрытия шире на overlap поколений
 * назад, но алгоритм обязан присутствовать у обеих сторон и здесь. */
plat_keyrot_verdict_t plat_keyrot_may_accept(const plat_key_t *k,
                                             const plat_peer_t *local,
                                             const plat_peer_t *remote,
                                             uint64_t overlap);

const char *plat_keyrot_verdict_name(plat_keyrot_verdict_t v);
include/platx/keystate.h
/* keystate.h — S437/S439: ключи и одноразовые значения переживают перезапуск.
 *
 * S437  Отсутствующий, отозванный или испорченный ключ хранения — это отказ
 *       ЗАКРЫТЫМ, а не повод завести новый. Автоматическая переинициализация
 *       выглядит как выздоровление, а на деле осиротит всё уже запечатанное:
 *       данные останутся на диске нечитаемыми навсегда, и никто не заметит
 *       момента, когда это случилось.
 *
 * S439  Одноразовое значение не имеет права повториться ни после чистого
 *       перезапуска, ни после краха, ни после сорванной контрольной точки.
 *       Поэтому на диск пишется не «последнее использованное», а ГРАНИЦА
 *       ВЫДАННОГО: блок резервируется долговечно ДО того, как из него выдадут
 *       хоть одно значение. Крах теряет остаток блока — и это правильный
 *       размен: потерянные номера ничего не стоят, повторённые стоят всего.
 */
struct xio_t;

typedef enum {
    PLAT_KEY_OK       = 0,
    PLAT_KEY_MISSING  = 1,
    PLAT_KEY_REVOKED  = 2,
    PLAT_KEY_CORRUPT  = 3
} plat_key_state_t;

/* Состояние ключа хранения. НИЧЕГО не создаёт и не чинит. */
plat_key_state_t plat_key_check(const struct xio_t *io, const char *path);

/* Открыть ключ для работы. Любое состояние кроме OK — отказ (-EKEYREJECTED
 * / -ENOKEY / -EBADMSG), и ключ НЕ создаётся. */
int plat_key_open(const struct xio_t *io, const char *path,
                  uint8_t out[32], plat_key_state_t *why);

/* ── одноразовые значения ────────────────────────────────────────────────── */

typedef struct {
    const struct xio_t *io;
    char     path[256];
    uint64_t next;       /* следующее к выдаче        */
    uint64_t reserved;   /* граница, записанная на диск */
    uint32_t block;      /* размер резервируемого блока */
} plat_nonce_t;

int plat_nonce_open(plat_nonce_t *n, const struct xio_t *io,
                    const char *path, uint32_t block);

/* Выдать следующее значение. 0 / -errno. */
int plat_nonce_next(plat_nonce_t *n, uint64_t *out);
include/platx/lifecycle.h
/* platx/lifecycle.h — formal module state machine.
 * All transitions go through the lifecycle manager. Recovery only requests.
 */
typedef enum plat_mod_state {
    PLAT_ST_DISCOVERED = 0,
    PLAT_ST_LOADED,
    PLAT_ST_CREATED,
    PLAT_ST_STARTING,
    PLAT_ST_RUNNING,
    PLAT_ST_DEGRADED,
    PLAT_ST_QUIESCING,
    PLAT_ST_STOPPING,
    PLAT_ST_STOPPED,
    PLAT_ST_DESTROYED,
    PLAT_ST_FAILED
} plat_mod_state_t;

typedef enum plat_health_code {
    PLAT_HEALTH_OK       = 0,
    PLAT_HEALTH_DEGRADED = -1,
    PLAT_HEALTH_FATAL    = -99
} plat_health_code_t;

typedef struct plat_health {
    plat_health_code_t code;
    char               reason[96];
} plat_health_t;

/* Recovery / supervisor name an action. Only the manager walks the SM
 * and calls descriptor hooks. RESTART is a sequence inside the manager,
 * not a license for recovery to call stop/start itself. */
typedef enum plat_lifecycle_action {
    PLAT_LC_LOAD = 1,
    PLAT_LC_CREATE,
    PLAT_LC_START,
    PLAT_LC_QUIESCE,
    PLAT_LC_STOP,
    PLAT_LC_DESTROY,
    PLAT_LC_FAIL,
    PLAT_LC_RESTART,
    PLAT_LC_DEGRADE
} plat_lifecycle_action_t;

struct plat_module_descriptor;
struct plat_create_args;

typedef struct plat_lifecycle {
    plat_mod_state_t                      state;
    const struct plat_module_descriptor  *desc;
    void                                 *instance;
    const struct plat_create_args        *create_args;
} plat_lifecycle_t;

/* Valid edges (anything else is rejected by the manager):
 *   DISCOVERED → LOADED | FAILED
 *   LOADED     → CREATED | DESTROYED | FAILED
 *   CREATED    → STARTING | DESTROYED | FAILED
 *   STARTING   → RUNNING | DEGRADED | FAILED
 *   RUNNING    → DEGRADED | QUIESCING | STOPPING | FAILED
 *   DEGRADED   → RUNNING | QUIESCING | STOPPING | FAILED
 *   QUIESCING  → STOPPING | FAILED
 *   STOPPING   → STOPPED | FAILED
 *   STOPPED    → STARTING | DESTROYED | FAILED
 *   FAILED     → DESTROYED | STOPPED   (never FAILED → RUNNING)
 *   DESTROYED  — terminal
 */
int plat_state_can_enter(plat_mod_state_t from, plat_mod_state_t to);

const char *plat_state_name(plat_mod_state_t s);

int plat_lifecycle_init(plat_lifecycle_t *lm);
int plat_lifecycle_bind(plat_lifecycle_t *lm,
                        const struct plat_module_descriptor *desc,
                        const struct plat_create_args *args);
/* Adopt a live instance. No hooks. Further changes go through request(). */
int plat_lifecycle_attach(plat_lifecycle_t *lm,
                          const struct plat_module_descriptor *desc,
                          void *instance, plat_mod_state_t state);

/* Illegal edge: reject, state unchanged.
 * Hook already ran and cannot undo: FAILED (not a silent half-state). */
int plat_lifecycle_request(plat_lifecycle_t *lm, plat_lifecycle_action_t action);

plat_mod_state_t plat_lifecycle_state(const plat_lifecycle_t *lm);
void *plat_lifecycle_instance(const plat_lifecycle_t *lm);
include/platx/lifecycle_serial.h
/* platx/lifecycle_serial.h — A1-P03: lifecycle serialization, bounded recovery.
 *
 * platx/lifecycle.h is the state machine and platx/recovery.h is the policy.
 * Neither of them says who is allowed to act, on which instance, how many
 * times, or for how long a decision stays true. This header is that missing
 * half, and it is deliberately additive: no type, field or entrypoint of the
 * two published headers changes, so every existing caller keeps compiling and
 * keeps its ABI.
 *
 * FOUR THINGS LIVE HERE
 *
 *  1. Identity.   A decision is taken about one generation of one instance.
 *                 Between deciding and applying, that instance can be
 *                 restarted; the generation moves and the decision is about
 *                 something that no longer exists. plat_lc_owner_publish()
 *                 records which generation is live, and every claim is
 *                 checked against it (P03-105). generation 0 is never an
 *                 identity, only the absence of one (P03-114).
 *
 *  2. Decisions.  A decision is a token, not a function call: issued once,
 *                 claimed at most once (P03-112), valid for a bounded time
 *                 (P03-106), and drawn from a fixed-size table so that a
 *                 producer which stops consuming fails loudly instead of
 *                 growing (P03-106, P03-135). The incumbent state is never
 *                 touched by a refused claim.
 *
 *  3. Admission.  Stopping is not a moment, it is an interval. The barrier
 *                 closes first and teardown runs after, so no admission can
 *                 slip between "we decided to stop" and "we started tearing
 *                 down" (P03-117). plat_lc_admits() is the query XIO submit
 *                 makes; plat_lc_admit_drain() is the bounded wait that
 *                 reports what was still in flight rather than pretending
 *                 the count was zero (P03-118, P03-124).
 *
 *  4. Deadlines.  A stop is one absolute deadline shared by every phase, not
 *                 a fresh timeout per phase. plat_lc_deadline_left_ms()
 *                 hands each phase the remainder (P03-123).
 *
 * WHAT THIS FILE DOES NOT DO
 *
 * It never calls a descriptor hook, never starts or stops anything, and never
 * blocks while holding its own lock. It is a table and a clock. The rule that
 * recovery decides and lifecycle alone executes (checked by
 * tests/platx/check_lifecycle_policy.sh) is not weakened by anything here.
 *
 * LOCK ORDER (P03-103)
 *
 *   PLATX_LR_LC_TXN (120)  the lifecycle transaction. Entry lock: held while
 *                          descriptor hooks and the resource/task/event
 *                          sweeps run. Everything below may be taken under
 *                          it; it may never be taken under anything below.
 *   PLATX_LR_REC_WATCH (121)  the recovery watch table. Must be released
 *                          before requesting a transition — a tick that held
 *                          it into plat_lifecycle_request() would invert the
 *                          order. Rank 121 > 120 makes that inversion a
 *                          reported violation rather than a comment.
 *   PLATX_LR_LC_SERIAL (216)  the tables in this file. A leaf: no callback,
 *                          no hook and no wait runs while it is held, which
 *                          is what lets XIO submit query the barrier while
 *                          holding its own owner/registry locks (211/212).
 *   PLATX_LR_CAPREG (217)  the capability registry, read from lc_start()
 *                          under the transaction lock.
 *
 * Ranks are checked only in builds that define PLATX_LOCK_RANK; see
 * platx/lock_rank.h. They are declared here rather than added to that file so
 * that P03 does not write into a header shared with every other stream; the
 * fold-in is a named handoff, and tests/platx/test_lifecycle_contract.c
 * asserts meanwhile that these four values collide with nothing in it.
 *
 * Cards: 101 state contract, 103 lock order, 104 decide/mutate split,
 *        105 generation gate, 106 bounded pending decisions, 112 idempotent
 *        apply, 113 unknown action, 114 generation zero, 115 generation
 *        exhaustion, 117 revoke ordering, 118 admission barrier for P06,
 *        123 single stop deadline, 124 honest drain timeout, 134 stable
 *        reason codes, 135 bounded history.
 */

/* ── bounds ──────────────────────────────────────────────────────────────
 *
 * Every table in this file is fixed at compile time. There is no allocation
 * anywhere in the implementation, so "the queue is full" is a real answer a
 * caller must handle, not a condition that quietly becomes heap growth. */
#define PLAT_LC_PENDING_MAX      16
#define PLAT_LC_OWNER_MAX        32
#define PLAT_LC_HISTORY_MAX      32
#define PLAT_LC_DECISION_TTL_SEC 30

/* ── result codes ────────────────────────────────────────────────────────
 *
 * Negative, stable, and distinct: a caller that gets -3 back must be able to
 * say "stale generation" and not merely "it failed". These numbers are part
 * of the contract; A4 receipts quote them (P03-134). */
#define PLAT_LC_OK                0
#define PLAT_LC_E_ARG           (-1)   /* NULL / malformed argument          */
#define PLAT_LC_E_GEN_ZERO      (-2)   /* generation 0 is not an identity    */
#define PLAT_LC_E_STALE_GEN     (-3)   /* decision is about a dead generation*/
#define PLAT_LC_E_DUPLICATE     (-4)   /* this token was already claimed     */
#define PLAT_LC_E_EXPIRED       (-5)   /* decision outlived its TTL          */
#define PLAT_LC_E_QUEUE_FULL    (-6)   /* pending table is full              */
#define PLAT_LC_E_ACTION        (-7)   /* action is not a lifecycle action   */
#define PLAT_LC_E_GEN_EXHAUSTED (-8)   /* no next generation without a wrap  */
#define PLAT_LC_E_CLOSED        (-9)   /* admission barrier is closed        */
#define PLAT_LC_E_TIMEOUT      (-10)   /* bounded wait expired, work remains */
#define PLAT_LC_E_NOENT        (-11)   /* no such owner / token              */
#define PLAT_LC_E_OWNER_FULL   (-12)   /* owner table is full                */

/* ── reason codes (P03-134) ──────────────────────────────────────────────
 *
 * One vocabulary for "why", shared by the recovery fact, the decision token,
 * the history ring and the A4 receipt. Values are frozen: append only, never
 * renumber. A failure, the decision it produced, the transition that decision
 * caused and the cleanup outcome are joined by (token, reason), which is what
 * lets a receipt be read without the source next to it. */
typedef enum plat_lc_reason {
    PLAT_LC_R_NONE               = 0,
    PLAT_LC_R_HEALTH_FATAL       = 1,
    PLAT_LC_R_HEALTH_DEGRADED    = 2,
    PLAT_LC_R_TASK_SILENT        = 3,   /* heartbeat silence > 2×interval    */
    PLAT_LC_R_CRASH              = 4,
    PLAT_LC_R_KILLED             = 5,
    PLAT_LC_R_EXITED             = 6,
    PLAT_LC_R_BUDGET_EXHAUSTED   = 7,
    PLAT_LC_R_BACKOFF            = 8,   /* refused: too soon                 */
    PLAT_LC_R_POLICY_NEVER       = 9,
    PLAT_LC_R_OPERATOR_STOP      = 10,  /* a human said stop; never undone   */
    PLAT_LC_R_STALE_GENERATION   = 11,
    PLAT_LC_R_DUPLICATE_DECISION = 12,
    PLAT_LC_R_DECISION_EXPIRED   = 13,
    PLAT_LC_R_QUEUE_FULL         = 14,
    PLAT_LC_R_DRAIN_TIMEOUT      = 15,
    PLAT_LC_R_GEN_EXHAUSTED      = 16,
    PLAT_LC_R_NO_PROVIDER        = 17,  /* nothing to restart into           */
    PLAT_LC_R_TAMPER             = 18,
    PLAT_LC_R_ROLLBACK_FAILED    = 19,  /* the retry failed too              */
    PLAT_LC_R__MAX               = 20
} plat_lc_reason_t;

/* Stable, allocation-free name for a reason. Unknown → "?" (never NULL). */
const char *plat_lc_reason_name(uint16_t reason);
/* Stable, allocation-free name for a result code. Unknown → "?". */
const char *plat_lc_err_name(int rc);

/* ── lock ranks (P03-103) ────────────────────────────────────────────────
 * See the header comment. Deliberately spaced into the gaps that
 * platx/lock_rank.h leaves between its layers. */
enum {
    PLATX_LR_LC_TXN     = 120,
    PLATX_LR_REC_WATCH  = 121,
    PLATX_LR_LC_SERIAL  = 216,
    PLATX_LR_CAPREG     = 217
};

/* ── identity ────────────────────────────────────────────────────────────
 *
 * The registry answers one question: for this (module_id, instance_id), which
 * generation is live right now? An instance that was never published has no
 * answer, and an unknown instance is not treated as stale — that would refuse
 * every caller who never opted in. Publication is what arms the check. */

/* Record `owner` as the live generation of its instance. Publishing a new
 * generation replaces the old one; that is the moment every decision about
 * the previous generation becomes stale. generation 0 → E_GEN_ZERO. */
int plat_lc_owner_publish(plat_owner_t owner);

/* Live generation of an instance. E_NOENT when it was never published. */
int plat_lc_owner_generation(uint32_t module_id, uint32_t instance_id,
                             uint32_t *out);

/* 1 = a different generation of this instance is live (the caller's decision
 * is about a corpse), 0 = current or never published, <0 = bad argument. */
int plat_lc_owner_stale(plat_owner_t owner);

/* Drop the record. Used on DESTROY so a slot is not held by a dead instance. */
int plat_lc_owner_forget(plat_owner_t owner);

/* Next generation of the same instance, without wrapping to a value that was
 * once live (P03-115). At UINT32_MAX the answer is E_GEN_EXHAUSTED and *out
 * is untouched: the context ends by an explicit refusal, not by reusing
 * generation 1 and silently validating every decision ever taken about it. */
int plat_lc_owner_next_generation(plat_owner_t cur, plat_owner_t *out);

/* ── decisions ───────────────────────────────────────────────────────────
 *
 * Issue produces a token and holds a slot. Claim consumes the slot and is the
 * single point where identity, lifetime and uniqueness are checked. The
 * mutation follows the claim and never precedes it, which is how one failure
 * becomes one decision and one observable state change (P03-104). */
typedef struct plat_lc_decision {
    uint64_t                token;      /* 0 = not a decision                */
    plat_owner_t            owner;
    plat_lifecycle_action_t action;
    uint16_t                reason;     /* plat_lc_reason_t                  */
    uint16_t                _pad;
    int64_t                 issued_at;  /* monotonic seconds, caller's clock */
    int64_t                 expires_at; /* issued_at + TTL                   */
} plat_lc_decision_t;

/* Issue a decision about `owner`. `now` is the caller's monotonic clock, so a
 * test can drive time without sleeping and a caller without a working clock
 * source cannot invent one here.
 *
 * E_GEN_ZERO   owner.generation == 0            (P03-114)
 * E_ACTION     action is outside the enum       (P03-113)
 * E_QUEUE_FULL the pending table is full        (P03-106) — the incumbent
 *              state is untouched and no slot is stolen from a live decision
 * E_STALE_GEN  a newer generation is already published */
int plat_lc_decision_issue(plat_owner_t owner, plat_lifecycle_action_t action,
                           uint16_t reason, int64_t now,
                           plat_lc_decision_t *out);

/* Consume the decision. Exactly one caller ever gets PLAT_LC_OK for a given
 * token; a second attempt is E_DUPLICATE, so replaying a decision cannot run
 * the effect twice or spend the restart budget twice (P03-112).
 *
 * E_STALE_GEN  the instance moved on since the decision was issued (P03-105)
 * E_EXPIRED    now >= expires_at; the slot is released
 * E_NOENT      the token was retired, expired away, or never issued */
int plat_lc_decision_claim(const plat_lc_decision_t *d, int64_t now);

/* Release a slot without claiming it — the decision is abandoned, not
 * applied. Idempotent: retiring an unknown token is E_NOENT, not a crash. */
int plat_lc_decision_retire(uint64_t token);

/* How many decisions are issued and not yet claimed, retired or expired. */
int plat_lc_decision_pending(void);

/* Drop every decision whose TTL has passed. Returns the number dropped, so a
 * caller can tell "nothing to do" from "we are shedding decisions". */
int plat_lc_decision_expire(int64_t now);

/* Total decisions refused because the table was full, since reset. A number
 * that grows is a producer outrunning its consumer, and is reported rather
 * than absorbed (P03-106). */
uint64_t plat_lc_decision_overflow(void);

/* ── history (P03-135) ───────────────────────────────────────────────────
 *
 * A fixed ring. When it wraps, the oldest record is lost and the loss is
 * counted — the ring never grows, and never claims a completeness it does not
 * have. Reading it copies out; no pointer into the ring escapes. */
typedef struct plat_lc_hist {
    uint64_t                token;
    plat_owner_t            owner;
    plat_lifecycle_action_t action;
    uint16_t                reason;
    int16_t                 outcome;    /* 0 = applied, else a PLAT_LC_E_*   */
    int64_t                 at;
} plat_lc_hist_t;

int      plat_lc_history_note(const plat_lc_decision_t *d, int outcome);
/* Oldest first. Returns the number written, at most `max`. */
int      plat_lc_history_read(plat_lc_hist_t *out, int max);
uint64_t plat_lc_history_dropped(void);

/* ── admission barrier (P03-117 / P03-118) ───────────────────────────────
 *
 * The contract handed to P06 (card 118, consumed by P06-268):
 *
 *   arm    is called once a start is fully allowed — after the module's start
 *          hook returned and the state is RUNNING or DEGRADED, never before,
 *          so no consumer sees admission open on a half-started module.
 *   close  is called on entry to STOPPING, before any teardown step. It is
 *          one atomic operation: after it returns, plat_lc_admits() is 0 for
 *          this owner and no new submission can be accepted. Submissions
 *          already inside the gate are counted, not cancelled.
 *   admits is the query on the submit path. It is a plain read of a leaf
 *          table: no allocation, no callback, no wait, safe to call while
 *          holding XIO owner/registry locks.
 *   enter/leave bracket one in-flight submission so close/drain can tell the
 *          truth about what was still running.
 *   drain  waits up to timeout_ms for the in-flight count to reach zero. It
 *          returns E_TIMEOUT and writes the remaining count when it does not,
 *          which is what stops a stop from reporting success over a module
 *          that still holds work (P03-124).
 *
 * The barrier is keyed by the full owner triple. A restart publishes a new
 * generation and therefore starts from a closed barrier — a stale generation
 * can never be admitted by the barrier its successor armed. */
int plat_lc_admit_arm(plat_owner_t owner);
/* Returns the barrier epoch (>= 1) that this close ended, or a negative
 * result code. Closing an already-closed barrier returns its epoch again and
 * is not an error: stop is idempotent. */
int plat_lc_admit_close(plat_owner_t owner);
/* 1 = admit, 0 = refuse. An owner with no barrier row refuses: admission is
 * something a module is granted, never something it has by default. */
int plat_lc_admits(plat_owner_t owner);
int plat_lc_admit_enter(plat_owner_t owner);
int plat_lc_admit_leave(plat_owner_t owner);
/* Current in-flight count, or a negative result code. */
int plat_lc_admit_inflight(plat_owner_t owner);
/* Bounded wait. `left` may be NULL. timeout_ms <= 0 polls once. */
int plat_lc_admit_drain(plat_owner_t owner, int timeout_ms, unsigned *left);
/* Drop the barrier row entirely (DESTROY). */
int plat_lc_admit_forget(plat_owner_t owner);

/* ── one stop deadline (P03-123) ─────────────────────────────────────────
 *
 * Quiesce, stop, task drain, XIO drain and unsubscribe drain are phases of
 * one stop, not five stops. Each asks for the remainder; none is handed a
 * fresh full timeout. An unarmed deadline reports "no limit" (-1) so that a
 * caller which never armed one behaves exactly as it did before. */
typedef struct plat_lc_deadline {
    int64_t at_ms;   /* monotonic milliseconds */
    int     armed;
} plat_lc_deadline_t;

int64_t plat_lc_now_ms(void);
void    plat_lc_deadline_arm(plat_lc_deadline_t *d, int total_ms);
/* Remaining milliseconds: -1 = unarmed (no limit), 0 = expired. */
int     plat_lc_deadline_left_ms(const plat_lc_deadline_t *d);
int     plat_lc_deadline_expired(const plat_lc_deadline_t *d);

/* ── state contract (P03-101) ────────────────────────────────────────────
 *
 * plat_state_can_enter() is the implementation of the edge table written in
 * the comment of platx/lifecycle.h. This is that comment, machine-readable,
 * so the two can be compared instead of trusted:
 * tests/platx/test_lifecycle_contract.c walks all 11×11 pairs and fails on
 * the first disagreement.
 *
 * `allowed` is a bitmask over plat_mod_state_t. `terminal` marks a state no
 * edge leaves. FAILED is reachable from every state except DESTROYED and is
 * therefore folded into every row rather than repeated in prose. */
#define PLAT_LC_BIT(s) (1u << (unsigned)(s))

typedef struct plat_lc_edges {
    plat_mod_state_t from;
    uint32_t         allowed;   /* including FAILED where reachable */
    int              terminal;
} plat_lc_edges_t;

/* The published contract, in declaration order DISCOVERED..FAILED. Returns
 * the number of rows; `*rows` points at static storage that outlives any
 * caller and is never written. */
int plat_lc_contract_edges(const plat_lc_edges_t **rows);

/* ── test / boot reset ───────────────────────────────────────────────────
 * Returns every table in this file to its initial state. Bounded, no
 * allocation, no callbacks. Safe to call before any other entrypoint. */
int plat_lc_reset(void);
include/platx/lock_rank.h
/* platx/lock_rank.h — W8-02 / S352: the lock order, made checkable.
 *
 * tools/lock_order.py extracts the ordering that the code actually follows
 * and reports whether it contradicts itself. That is a static answer about
 * the paths the extractor could see: it cannot follow a function pointer,
 * and it cannot know which branch runs. This is the dynamic half — the same
 * ordering, asserted on the acquisitions that really happen.
 *
 * THE RULE
 *
 * A thread may acquire a lock only if its rank is strictly greater than
 * every rank it already holds. Strictly, not merely greater-or-equal: two
 * locks of equal rank have no defined order between them, so nesting them
 * is exactly the situation where two threads can pick opposite orders. An
 * equal-rank nesting is therefore a violation, not a tie.
 *
 * RANKS
 *
 * The values below are a topological layering of build/lock-order.json,
 * which had no cycles — that is why an assignment exists at all. Layers are
 * spaced by 100 so a lock discovered later can be inserted between two
 * existing ones without renumbering the file and invalidating every rank in
 * the tree.
 *
 *   0xx  entry locks     boot paths, CLI command state, managers. Taken
 *                        first, held while calling inward.
 *   1xx  subsystem locks  the core state of one subsystem.
 *   2xx  leaf locks      ownership tables and registries. Taken last, held
 *                        briefly, and never held while calling back out —
 *                        which is what keeps the order acyclic.
 *
 * A lock with no rank is unranked and unchecked; unranked locks are ignored
 * rather than assumed safe, because guessing a rank would manufacture
 * violations that say nothing about the code.
 *
 * COST
 *
 * The checking is compiled out entirely unless PLATX_LOCK_RANK is defined.
 * Release builds carry no per-thread state, no branches, and no calls: the
 * macros expand to nothing. This is a debug-build assertion, in the sense
 * W8-02 asks for, not a runtime tax on production.
 *
 * SPDX-License-Identifier: GPL-2.0
 */

/* ── Ranks, layered from the extracted graph ─────────────────────────── */

enum {
    /* Layer 0 — entry locks. */
    PLATX_LR_CMD_MBUS        = 10,
    PLATX_LR_CMD_MESH        = 11,
    PLATX_LR_DRM_MANAGER     = 12,
    PLATX_LR_HOOK_BOOT       = 13,
    PLATX_LR_HOOK_UPROBE     = 14,
    PLATX_LR_TRACE_DRAIN     = 15,
    PLATX_LR_XIM_BOOT        = 16,
    PLATX_LR_XIO_EBPF_LIFE   = 17,
    PLATX_LR_XIO_MODE        = 18,

    /* Layer 1 — subsystem state. */
    PLATX_LR_HOOK_CORE       = 110,
    PLATX_LR_MESH_LIVE       = 111,
    PLATX_LR_XIM_CORE        = 112,
    PLATX_LR_XIM_HOST        = 113,
    PLATX_LR_XIO_EBPF_RX     = 114,
    PLATX_LR_XIO_MODE_DEF    = 115,
    /* D056: the provider fabric is taken BEFORE the SENSE registry, because
     * registration and publication call into it. It is not a leaf: no
     * provider vtable entry is ever called while it is held. */
    PLATX_LR_PROV_FABRIC     = 116,
    /* A3 handoff H7: ранги 117/118 приняты за платформой Windows. Слой 1, а
     * не 2, потому что порт завершения вызывает наружу, пока держится: лист
     * этого не делает по определению. Имена согласуются с A3; номера
     * закреплены здесь, и переиспользовать их под другое нельзя. */
    PLATX_LR_IOCP_PORT       = 117,
    PLATX_LR_IOCP_OP         = 118,

    /* Layer 2 — leaf tables. Nothing calls outward while holding these. */
    PLATX_LR_PLUGIN_MANAGER  = 210,
    PLATX_LR_XIO_OWNER       = 211,
    PLATX_LR_XIO_REGISTRY    = 212,
    /* SENSE055: the source table is taken before the ring, never after.
     * Both are leaves: no external callback runs while either is held. */
    PLATX_LR_SENSE_REGISTRY  = 213,
    PLATX_LR_SENSE_RING      = 214,
    /* SENSE081-094: плоскость покрытия берётся после кольца и
     * никогда до него. Лист: внешний код под ней не вызывается. */
    PLATX_LR_SENSE_COVERAGE  = 215,
    /* A1-P04: обе таблицы владения — листья, и это утверждение проверяемое.
     * Ни один releaser ресурса и ни одна точка входа задачи не вызываются
     * под своим локом: sweep помечает строку DRAINING, отпускает таблицу и
     * только потом зовёт releaser (plat_resource_owner.c), а трамплин задачи
     * забирает точку входа под локом и вызывает её после разблокировки.
     *
     * Таблица ресурсов берётся ПЕРЕД таблицей задач: releaser ресурса
     * PLAT_RES_THREAD ходит в plat_task, обратного пути нет — plat_task_init
     * ставит releaser уже после того, как отпустил свою таблицу. */
    PLATX_LR_RES_TABLE       = 216,
    PLATX_LR_TASK_TABLE      = 217,
    /* A1-P05: таблица подписчиков plat_event — лист, и это проверяемое
     * утверждение, а не намерение. Диспетчер отпускает лок перед вызовом
     * callback и берёт его снова после возврата; ожидание unsubscribe идёт
     * через pthread_cond_(timed)wait, который лок отпускает. Гейт
     * test_event_bound.c::c217 доказывает это поведением: callback изнутри
     * зовёт plat_event_recent() и plat_event_stats(), обе берут тот же
     * нерекурсивный мьютекс — под локом это был бы самозахват.
     *
     * Ранг ВЫШЕ таблиц ресурсов и задач: releaser ресурса и точка входа
     * задачи могут выпускать факт, обратного пути нет — plat_event ничего
     * не вызывает наружу под своим локом. */
    PLATX_LR_EVENT_TABLE     = 218,

    PLATX_LR_UNRANKED        = 0
};

/* ── Runtime, present only in instrumented builds ────────────────────── */

/* Record an acquisition. Reports a violation when `rank` is not strictly
 * greater than every rank this thread already holds. */
void platx_lock_rank_acquire(int rank, const char *name,
                             const char *file, int line);

/* Record a release. Releasing a rank this thread does not hold is itself
 * reported: it means an unlock is running on the wrong lock, or a lock
 * leaked out of the function that took it. */
void platx_lock_rank_release(int rank, const char *name);

/* Violations seen since the last reset. Tests read this instead of relying
 * on an abort, so that a deliberate inversion can be exercised and the
 * process can keep running to check the rest. */
unsigned platx_lock_rank_violations(void);
void     platx_lock_rank_reset(void);

/* Snapshot of the most recent violation into `dst` — for a test to assert on
 * which inversion was caught, not merely that one was.
 *
 * Copies under the report mutex rather than handing back a pointer into the
 * live buffer: report() rewrites that buffer from another thread, so a
 * returned pointer could be overwritten mid-read (B-007).
 *
 * Writes at most `cap - 1` bytes plus a terminator; `dst` may be NULL when
 * `cap` is 0. Returns the length of the full record, so a caller can tell a
 * truncated snapshot from a complete one. */
size_t platx_lock_rank_last(char *dst, size_t cap);

/* When set, a violation calls abort() after reporting. Off by default so a
 * test can observe violations; a debug run that wants to stop at the first
 * inversion turns it on. */
void platx_lock_rank_set_abort(int on);

/* Ranks this thread currently holds, outermost first. Returns the count. */
size_t platx_lock_rank_held(int *out, size_t max);

#define PLATX_LOCK_ACQUIRE(rank, name) \
    platx_lock_rank_acquire((rank), (name), __FILE__, __LINE__)
#define PLATX_LOCK_RELEASE(rank, name) \
    platx_lock_rank_release((rank), (name))

#define PLATX_LOCK_ACQUIRE(rank, name) ((void)0)
#define PLATX_LOCK_RELEASE(rank, name) ((void)0)
include/platx/logrot.h
/* logrot.h — S417: после ротации писатель пишет в живой файл, а не в сироту.
 *
 * ОТКУДА БЕРЁТСЯ СИРОТА
 * ─────────────────────
 * Дескриптор указывает на ИНОД, а не на имя. После rename(path, path.1) старый
 * дескриптор продолжает писать — в файл, который теперь называется path.1.
 * Если открыть новый path не удалось, а писателя не перепривязали, он останется
 * на этом иноде НАВСЕГДА: журнал внешне ротируется, а строки уходят в резервную
 * копию, которую следующая ротация переименует дальше и в конце концов удалит.
 *
 * Поэтому у ротации ровно три допустимых исхода, и «молча продолжать» среди них
 * нет:
 *   1. перепривязались к новому файлу — обычный успех;
 *   2. не смогли — вернули прежнее имя на место и остались на нём;
 *   3. не смогли и этого — писатель ОТКАЗЫВАЕТ на каждой записи, пока его не
 *      откроют заново. Потерянные строки видны как ошибки; строки, ушедшие в
 *      сироту, не видны никак.
 */
struct xio_t;

typedef struct {
    const struct xio_t *io;
    char path[256];
    int  fd;
    int  refusing;      /* 1 — перепривязка не удалась, записи отвергаются */
    unsigned long rotations;
} plat_writer_t;

int plat_writer_open (plat_writer_t *w, const struct xio_t *io, const char *path);
int plat_writer_write(plat_writer_t *w, const void *buf, size_t n);

/* Сдвинуть path.N→path.N+1 (не более keep), path→path.1, открыть новый path.
 * 0 / -errno. При -EIO писатель в состоянии отказа: см. исход 3. */
int plat_writer_rotate(plat_writer_t *w, unsigned keep);

int plat_writer_close(plat_writer_t *w);

/* Пишет ли писатель прямо сейчас в файл с ЖИВЫМ именем.
 * 1 — да, 0 — нет (сирота либо отказ). Для проверок и диагностики. */
int plat_writer_on_live_name(const plat_writer_t *w);
include/platx/module.h
/* platx/module.h — one descriptor, two hosts (embedded / child). */
#define PLAT_MOD_FLAG_OPTIONAL   0x0001u
#define PLAT_MOD_FLAG_SINGLETON  0x0002u
#define PLAT_MOD_FLAG_STATEFUL   0x0004u

/* Isolation is a deploy policy, not the nature of the module. */
typedef enum plat_isolate {
    PLAT_ISO_EMBEDDED = 0,
    PLAT_ISO_CHILD    = 1
} plat_isolate_t;

typedef struct plat_requirement {
    const char *capability;   /* logical name, e.g. PLAT_NAME_STORAGE_KEYRING */
    uint32_t    version;
} plat_requirement_t;

typedef struct plat_provide {
    const char *capability;
    uint32_t    version;
} plat_provide_t;

typedef struct plat_create_args {
    plat_module_ctx_t *ctx;
    const plat_abi_t  *abi;     /* real or proxy — same type */
    plat_owner_t       owner;
    plat_isolate_t     isolate; /* chosen by profile, not by the .c file */
    const char        *cfg_json;
} plat_create_args_t;

typedef struct plat_module_descriptor {
    uint32_t descriptor_size;
    uint32_t descriptor_version;

    const char *name;
    uint32_t    module_version;
    uint32_t    required_core_abi;

    const plat_requirement_t *requires;   /* NULL-terminated or n_requires */
    const plat_requirement_t *optional;
    const plat_provide_t     *provides;
    uint32_t n_requires, n_optional, n_provides;

    int  (*create)(const plat_create_args_t *args, void **instance);
    int  (*start)(void *instance);
    int  (*quiesce)(void *instance);   /* drain; no new work */
    int  (*stop)(void *instance);
    void (*destroy)(void *instance);

    int  (*health)(void *instance, plat_health_t *out);
    int  (*save_state)(void *instance, void **buf, size_t *len);
    int  (*load_state)(void *instance, const void *buf, size_t len);
    void (*free_state)(void *buf);

    int  (*register_cmds)(void *instance, void *cmd_reg);
    void (*register_script)(void *instance, void *script_ctx);

    /* Preferences. Profile may still pick the other allowed mode. */
    plat_isolate_t isolation_preference;
    uint32_t       isolation_allowed;  /* bitmask: 1<<EMBEDDED | 1<<CHILD */
    const char    *seccomp_profile;    /* used only if launched as CHILD */

    uint32_t flags;
    uint8_t  reserved[32];
} plat_module_descriptor_t;

/* Every PIC module exports this symbol. */
#define PLAT_MODULE_DESCRIPTOR  plat_module_descriptor

/* Are this descriptor's declared requires[] satisfiable right now.
 *
 * 0 = every mandatory capability is in the registry; -1 = one is not, and
 * *missing_out / *version_out name it. optional[] is not consulted: optional
 * means the module has a path without it.
 *
 * Declared here rather than in abi.h because it speaks about a descriptor, and
 * abi.h is included by this header, not the other way round.
 *
 * Lifecycle calls this before start(). A module that named a capability it
 * cannot work without must not reach start() and discover the absence halfway
 * through, with whatever it had already opened still standing. Both conventions
 * the descriptor allows -- n_requires, or NULL-terminated -- are walked. */
int plat_cap_requires_met(const plat_module_descriptor_t *d,
                          const char **missing_out, uint32_t *version_out);
include/platx/plat_ops_cap.h
/* platx/plat_ops_cap.h — plat_ops via capability APIs (INT-115). */
#define PLAT_OPS_CAP_DIAG "ops.diag:v1"
#define PLAT_OPS_CAP_CTRL "ops.ctrl:v1"
#define PLAT_OPS_CAP_REASON 128

typedef enum {
    PLAT_OPS_OK     = 0,
    PLAT_OPS_NOLINK = 1,
    PLAT_OPS_DENIED = 2,
    PLAT_OPS_ERROR  = 3,
} plat_ops_status_t;

typedef struct {
    plat_ops_status_t status;
    char reason[PLAT_OPS_CAP_REASON];
} plat_ops_result_t;

/* Gate an ops call via capability lease.  0/-1. */
int plat_ops_cap_gate(uint64_t ctx_id, uint32_t ctx_gen,
                      const char *cap_name, plat_ops_result_t *out);
include/platx/platx_dsl.h
/* platx_dsl.h — DSL engine public API */

/* INV-DSL-01: undefined variable → compile error (not runtime) */
/* INV-DSL-02: VM stack overflow → rule disabled (no crash)     */

/* VM */
typedef struct { uint8_t opcode; uint64_t operand; } dsl_insn_t;
int      dsl_vm_rule_add(const char *name, const dsl_insn_t *insns, uint32_t count);
int      dsl_vm_exec(uint32_t rule_idx, uint64_t *env_vars, uint32_t env_count);
int      dsl_vm_rule_count(void);
int      dsl_vm_rule_disabled(uint32_t idx);
void     dsl_vm_reset(void);

/* Route DSL */
int         dsl_route_add(const char *traffic, const char *condition,
                           const char *target, uint32_t rule_idx);
const char *dsl_route_match(const char *traffic, uint64_t *env, uint32_t ecount);
int         dsl_route_disable(uint32_t idx);
uint32_t    dsl_route_count(void);
void        dsl_route_reset(void);

/* Sensor DSL */
typedef enum { SENSOR_OP_GT=0, SENSOR_OP_LT=1, SENSOR_OP_EQ=2 } sensor_op_t;
typedef enum { SENSOR_ACT_LOG=0, SENSOR_ACT_ALERT=1, SENSOR_ACT_BLOCK=2 } sensor_action_t;
int      dsl_sensor_add(const char *field, sensor_op_t op,
                         uint64_t threshold, sensor_action_t action);
uint32_t dsl_sensor_eval(const char *field, uint64_t value);
uint64_t dsl_sensor_fire_count(uint32_t idx);
uint32_t dsl_sensor_count(void);
void     dsl_sensor_reset(void);

/* Filesystem watch DSL */
typedef enum {
    FS_EVENT_CREATE=0x01, FS_EVENT_MODIFY=0x02,
    FS_EVENT_DELETE=0x04, FS_EVENT_ANY=0xFF,
} fs_event_mask_t;
typedef void (*fs_action_fn)(const char *path, fs_event_mask_t event, void *ctx);
int      dsl_fs_watch_add(const char *path, fs_event_mask_t mask,
                           fs_action_fn action, void *ctx);
void     dsl_fs_dispatch(const char *path, fs_event_mask_t event);
int      dsl_fs_watch_disable(uint32_t idx);
uint64_t dsl_fs_trigger_count(uint32_t idx);
uint32_t dsl_fs_watch_count(void);
void     dsl_fs_reset(void);
include/platx/platx_features.h
/* SPDX-License-Identifier: GPL-2.0-only */
/* include/platx/platx_features.h - compile-time feature registry. Wave 2.43,
 * 2.44, 5.41. Feature flags are additive; a profile turns them off to build a
 * smaller image (MINIMAL mode). A feature being OFF must never fault another.
 * Default: all ON unless PLATX_PROFILE_MINIMAL is defined. */
#    define PLATX_FEATURE_HADES 0   /* MINIMAL: sense works without ebpf */
#  define PLATX_FEATURE_HADES 1
#  define PLATX_FEATURE_SENSE 1
#  define PLATX_FEATURE_SELFPROTECT 1
include/platx/platx_log.h
/* platx_log.h — public API forwarding for the log subsystem */
/* log_export.c */
/* INV-LOG-01: register audit sink so LOG_FATAL → audit ring */
int log_audit_sink_register(void);

/* Export ring snapshot to binary file.
 * Returns number of entries written or -1. */
int log_export_to_file(const char *path);

/* log_rotate.c */
typedef struct log_s log_t;
int log_rotate(log_t *l, const char *path);
include/platx/platx_mirage.h
/* include/platx/platx_mirage.h — MIRAGE sandbox public API (task 4.23) */

/* Feature flag — compile-time opt-in */

/* Invariants:
 * INV-MIRAGE-01: sandbox exit code != 0 → rehearsal FAILED
 * INV-MIRAGE-02: sandbox writes outside sandbox_root → immediate error
 * INV-MIRAGE-03: sandbox duration > MIRAGE_MAX_TTL_MS → SIGKILL
 */
#define MIRAGE_MAX_TTL_MS  10000u

typedef struct {
    int      exit_code;
    int      timed_out;     /* INV-MIRAGE-03 tripped */
    int      passed;        /* INV-MIRAGE-01: 1 iff exit_code==0 && !timed_out */
    uint64_t elapsed_ms;
    char     label[64];
} mirage_result_t;

/* World (sandbox environment) */
int mirage_world_define(const char *root, const char *workdir,
                        uint32_t uid, uint32_t gid, int readonly);
int mirage_world_get   (int id, char *root, char *work,
                        uint32_t *uid, uint32_t *gid, int *readonly);
int mirage_world_count (void);

/* Execution */
typedef struct {
    const char *argv[16];
    int         argc;
    int         world_id;
    int         probe;
} mirage_exec_req_t;

typedef struct {
    int     exit_code;
    int     timed_out;
    uint64_t elapsed_ms;
} mirage_exec_result_t;

int mirage_exec(const mirage_exec_req_t *req, mirage_exec_result_t *res);

/* Probe mode */
int mirage_probe_path(const char *sandbox_root, const char *rel_path);
int mirage_probe_is_writable(const char *sandbox_root, const char *path);

/* Record & compare */
int  mirage_record_append(const char *label, int exit_code,
                          int timed_out, uint64_t elapsed_ms);
void mirage_record_stats(uint64_t *rehearsals, uint64_t *passed, uint64_t *failed);
int  mirage_compare(int rehearsal_exit, int real_exit);
int  mirage_compare_elapsed(uint64_t r_ms, uint64_t real_ms, uint64_t tol_ms);

/* MIL gate */
int mirage_mil_gate(int rehearsal_id, int threat_level, int has_mirage);

/* GC + watchdog */
int mirage_gc_register(pid_t pid, uint64_t started_ms);
int mirage_gc_run(uint64_t now_ms, uint64_t max_ttl_ms);
int mirage_watchdog_wait(pid_t pid, uint64_t ttl_ms);

/* Audit */
void     mirage_audit_record(uint32_t op_type, uint32_t flags,
                             uint64_t rehearsal_id, int exit_code);
uint64_t mirage_audit_cursor(void);

/* Seccomp */
int mirage_seccomp_apply(void);
include/platx/platx_modhost.h
/* include/platx/platx_modhost.h — MODHOST public API (task 5.29) */

/* Invariants:
 * INV-MODHOST-01: module without MSX signature → REFUSE (§MIL-10)
 * INV-MODHOST-02: module without lease → REFUSE
 * INV-MODHOST-03: unload failing → force unload + panic report
 */

#define MODHOST_INVENTORY_SLOTS 64u
#define MODHOST_AUDIT_SLOTS     1024u

typedef struct {
    char     name[128];
    char     path[256];
    void    *handle;
    int      loaded;
    uint64_t load_count;
    uint32_t flags;
    uint64_t loaded_at_ms;
} modhost_slot_t;

/* Inventory */
int            modhost_inventory_add(const char *name, const char *path,
                                     void *handle, uint32_t flags);
int            modhost_inventory_remove(const char *name);
modhost_slot_t *modhost_inventory_find(const char *name);
int            modhost_inventory_count(void);

/* Hot reload */
int modhost_reload(const char *name, const char *new_path);

/* Sandbox */
int modhost_sandbox_run(const char *module_path, int world_id);

/* Audit */
void     modhost_audit_record(uint32_t op, uint32_t flags,
                              const char *name, int result);
uint64_t modhost_audit_cursor(void);

/* Lockdown (TL4) */
int modhost_lockdown_check(int threat_level, const char *name);

/* Metrics */
void modhost_metrics_inc_loaded(void);
void modhost_metrics_inc_failed(void);
void modhost_metrics_inc_unloaded(void);
int  modhost_metrics_export(void (*emit)(const char*, uint64_t, void*), void *ctx);
include/platx/platx_msx.h
/* include/platx/platx_msx.h — MSX signature public API (task 5.29) */

/* INV-MSX-01: revoked signature → immediate module unload */
#define MSX_REVOKE_SLOTS 256u
#define MSX_KEY_SLOTS    8u

/* Revocation */
int msx_revoke_add(const uint8_t *fingerprint);
int msx_revoke_check(const uint8_t *fingerprint); /* 1=revoked, 0=ok */
int msx_revoke_count(void);

/* Key rotation */
int            msx_key_add(uint32_t key_id, const uint8_t *pubkey);
int            msx_key_rotate(uint32_t new_key_id);
int            msx_key_active_id(void);
const uint8_t *msx_key_get_pubkey(uint32_t key_id);

/* §MIL-10 enforcement */
int msx_mil_verify(const char *path, const uint8_t *fingerprint);
int msx_mil_check_fp(const uint8_t *fingerprint);

/* Existing API stubs (implemented in msx_verify.c / msx_sign.c) */
int msx_verify_path(const char *path);
include/platx/platx_plugin.h
/* include/platx/platx_plugin.h — Plugin public API (task 5.29) */

#define PLUGIN_API_VERSION_MAJOR 1u
#define PLUGIN_API_VERSION_MINOR 0u
#define PLUGIN_API_VERSION       ((PLUGIN_API_VERSION_MAJOR << 16) | PLUGIN_API_VERSION_MINOR)

typedef struct {
    uint32_t    api_version;
    const char *name;
    const char *description;
    int (*init)(void *ctx);
    int (*fini)(void *ctx);
    int (*call)(uint32_t op, void *in, void *out, size_t out_len);
} plugin_api_t;

/* Registration */
int                 plugin_api_register(const plugin_api_t *api);
const plugin_api_t *plugin_api_find(const char *name);
uint32_t            plugin_api_version(void);
int                 plugin_api_count(void);

/* Sandbox */
int plugin_sandbox_register(const char *name, int sandboxed);
int plugin_sandbox_check(const char *name);

/* Revoke */
int plugin_revoke_add(const char *name);
int plugin_revoke_check(const char *name); /* 1=revoked */
int plugin_revoke_count(void);
include/platx/platx_poe.h
/* platx_poe.h — Proof-of-Execution public API */

#define POE_CHAIN_SLOTS  256
#define POE_RECEIPT_CLAIM    0u
#define POE_RECEIPT_DECISION 1u
#define POE_RECEIPT_EFFECT   2u

/* INV-POE-01: every EFFECT must have CLAIM + DECISION predecessors */
int            poe_chain_append(uint32_t receipt_type, const uint8_t *payload, size_t plen);
int            poe_chain_verify(uint64_t seq);
uint64_t       poe_chain_count(void);
const uint8_t *poe_chain_tip_hash(void);     /* 32-byte SHA-256, NULL if empty */

int            poe_anchor_tip(uint64_t corr_id, uint64_t source_id);
int            poe_anchor_check_chain(uint32_t t0, uint32_t t1, uint32_t t2);
uint64_t       poe_anchor_chain_depth(void);
include/platx/platx_proxy.h
/* platx_proxy.h — Proxy/X public API (PLATX_HAS_PROXY; not in MIL mode) */

/* §7.31: gated by PLATX_HAS_PROXY; §7.32: unavailable in MIL mode */

void        proxy_intercept_set_mil(int v);
int         proxy_intercept_record(const char *url, const uint8_t *req, uint32_t rlen,
                                    const uint8_t *resp, uint32_t resplen, int tls);
uint32_t    proxy_intercept_count(void);
const char *proxy_intercept_url(uint32_t idx);
void        proxy_intercept_reset(void);

/* HTTP decode */
typedef struct {
    char method[16]; char path[512]; char version[16];
    struct { char name[64]; char value[256]; } headers[32];
    uint32_t hdr_count, body_offset, body_len;
} proxy_request_t;
int         proxy_decode_request(const uint8_t *raw, size_t len, proxy_request_t *out);
const char *proxy_decode_header(const proxy_request_t *req, const char *name);

/* Scan */
int      proxy_scan_add(const char *pattern, const char *label);
uint32_t proxy_scan_check(const uint8_t *data, size_t dlen);
uint64_t proxy_scan_match_count(uint32_t idx);
void     proxy_scan_reset(void);

/* Modify */
int proxy_modify_add(const char *field, const char *old, const char *new_v);
int proxy_modify_apply(char *buf, size_t bufsz, const char *field);
void proxy_modify_reset(void);

/* Replay */
int            proxy_replay_store(const uint8_t *req, uint32_t len);
const uint8_t *proxy_replay_get(uint32_t idx, uint32_t *len_out);
uint32_t       proxy_replay_count(void);
void           proxy_replay_reset(void);

/* Fuzz */
int            proxy_fuzz_init_builtins(void);
int            proxy_fuzz_add(const uint8_t *data, uint32_t len);
const uint8_t *proxy_fuzz_payload(uint32_t idx, uint32_t *len_out);
uint32_t       proxy_fuzz_count(void);
void           proxy_fuzz_reset(void);

/* Report */
int      proxy_report_add(const char *url, const char *finding,
                           uint32_t pattern_mask, int severity);
int      proxy_report_write(const char *path);
uint32_t proxy_report_count(void);
void     proxy_report_reset(void);

/* POE export */
int proxy_export_to_poe(const char *url, uint32_t pattern_mask,
                        int blocked, uint64_t corr_id);

/* Encode */
int proxy_urlencode(const char *in, char *out, size_t outsz);
int proxy_urldecode(const char *in, char *out, size_t outsz);
int proxy_base64_encode(const uint8_t *in, size_t ilen, char *out, size_t outsz);
include/platx/platx_taskmgr.h
/* platx_taskmgr.h — public API forwarding for the task manager */
/* INV-TASKMGR-01: task without deadline → MAX_TASK_TTL (300 s) */
#define MAX_TASK_TTL  300

/* taskmgr_queue.c */
void taskmgr_queue_init(void);
int  taskmgr_queue_push(const char *name, const char *owner,
                        task_prio_t prio, time_t deadline);
int  taskmgr_queue_depth(void);

/* taskmgr_sched.c */
void taskmgr_sched_init(void);
int  taskmgr_sched_submit(const char *name, const char *owner,
                          task_prio_t prio, time_t deadline);
int  taskmgr_sched_cancel(int slot);
int  taskmgr_sched_tick(void);
int  taskmgr_sched_pending(void);
include/platx/platx_trace.h
/* platx_trace.h — public API forwarding for the trace subsystem */
/* trace_propagate.c */
int trace_propagate_capture(trace_ctx_t *out);
int trace_propagate_restore(const trace_ctx_t *ctx);
int trace_propagate_encode(const trace_ctx_t *ctx, char *buf, size_t buflen);
int trace_propagate_decode(const char *header, trace_ctx_t *out);

/* trace_export.c */
int trace_export_to_file(const trace_id_t *id, const char *path);
int trace_export_jsonl(const char *path);
include/platx/platx_vault.h
/* platx_vault.h — Vault secret storage public API (INV-VAULT-01) */

#define VAULT_SLOTS      32
#define VAULT_SECRET_MAX 512
#define VAULT_HMAC_LEN   32

/* INV-VAULT-01: vault requires valid profile HMAC (§SEC-0) before any op */
int vault_set_profile_hmac(const uint8_t *hmac, size_t len);

int vault_seal(const uint8_t *secret, size_t slen,
               const uint8_t *key_hmac, size_t klen,
               uint32_t *out_id);
int vault_unseal(uint32_t id, const uint8_t *key_hmac, size_t klen,
                 uint8_t *out, size_t out_cap, size_t *out_len);
int vault_unseal_safe(uint32_t id, const uint8_t *key_hmac, size_t klen,
                      uint8_t *out, size_t out_cap, size_t *out_len);
int vault_close(uint32_t id);

int vault_rotate_init(const uint8_t *initial_key, size_t klen);
int vault_rotate_slot(uint32_t id,
                      const uint8_t *old_key, size_t oklen,
                      const uint8_t *new_key, size_t nklen);
int vault_rotate_profile(const uint8_t *new_hmac, size_t len);
include/platx/platx_vfs.h
/* platx_vfs.h — public API for the VFS subsystem */
/* vfs_file.c */
int vfs_file_load(const char *fspath, const char *vpath, vfs_kind_t kind);
int vfs_file_save(const char *vpath, const char *fspath);
int vfs_file_read_text(const char *vpath, char **out, size_t *out_len);
int vfs_file_write_text(const char *vpath, vfs_kind_t kind,
                        const char *text, size_t len);

/* vfs_dir.c */
int vfs_dir_list(const char *prefix, vfs_stat_t *buf, int buf_count);
int vfs_dir_count(const char *prefix);
int vfs_dir_exists(const char *prefix);

/* vfs_async.c */
typedef void (*vfs_async_cb)(int result, void *arg);
int vfs_async_get(const char *path, vfs_async_cb cb, void *arg);
int vfs_async_put(const char *path, vfs_kind_t kind,
                  const uint8_t *data, size_t len,
                  vfs_async_cb cb, void *arg);
int vfs_async_drain(void);
include/platx/platx_wiper.h
/* platx_wiper.h — public API for the wiper subsystem */
/* INV-WIPER-01: wiper_mem must be called for every secret at cleanup.
 * The volatile barrier prevents compiler elision. */
void wiper_mem(void *ptr, size_t len);

/* Convenience wrappers */
void wiper_key(void *key, size_t klen);
void wiper_key32(void *key);   /* wipes 32 bytes */
void wiper_key64(void *key);   /* wipes 64 bytes */
void wiper_key_field(void *obj, size_t offset, size_t klen);

/* 3-pass DoD file overwrite (0x00 / 0xFF / 0x55) + fsync after each pass.
 * Returns 0 on success, -1 on I/O error. */
int  wiper_file(const char *path);
include/platx/pluginset.h
/* pluginset.h — S489: набор плагинов принимается ЦЕЛИКОМ или не принимается.
 *
 * ПОЧЕМУ НАБОР, А НЕ ПЛАГИН
 * ─────────────────────────
 * Плагины загружают списком, и список почти всегда смешанный: часть собрана
 * против прошлого выпуска, часть против нынешнего. Загрузка «по одному, пока
 * получается» оставляет систему в состоянии, которого нет ни в одном плане:
 * три плагина работают, четвёртый отвергнут, пятый не пробовали. Оператор
 * видит «частично загружено» и не знает, что из этого действует.
 *
 * Поэтому две фазы, и граница между ними — главное в этом файле:
 *
 *   ДОПУСК     все члены проверяются ДО того, как хоть один начал работать.
 *              Ни одного побочного действия: проверка не «пробует загрузить».
 *   ВКЛЮЧЕНИЕ  начинается только если допуск прошёл ВЕСЬ набор.
 *
 * И третье, без чего первые два — половина решения: если включение упало на
 * середине, уже включённые ВЫКЛЮЧАЮТСЯ. Иначе всё сводится к тому же
 * «частично», только позже.
 *
 * Отказ обязан назвать ПЕРВОГО виновника поимённо. «Набор отвергнут» без
 * имени заставляет выключать плагины по одному, чтобы найти который.
 */
#define PLAT_PSET_MAX      32
#define PLAT_PSET_NAME_MAX 48

/* Поддерживаемый диапазон ABI плагина. Смешанный набор — это набор, где
 * встречаются оба края диапазона; он допустим. Выход за край — нет. */
#define PLAT_PSET_ABI_MIN 2u
#define PLAT_PSET_ABI_MAX 3u

typedef enum {
    PLAT_PSIG_OK       = 0,
    PLAT_PSIG_BAD      = 1,
    PLAT_PSIG_MISSING  = 2
} plat_psig_t;

typedef struct {
    char        name[PLAT_PSET_NAME_MAX];
    uint32_t    abi;
    plat_psig_t sig;
    int         active;    /* выставляет только plat_pset_activate */
} plat_plugin_t;

typedef enum {
    PLAT_PSET_OK          = 0,
    PLAT_PSET_ABI_OLD     = 1,
    PLAT_PSET_ABI_NEW     = 2,
    PLAT_PSET_SIG         = 3,
    PLAT_PSET_DUPLICATE   = 4,
    PLAT_PSET_ACTIVATE    = 5,  /* включение упало — набор откачен */
    PLAT_PSET_BAD_ARG     = 6
} plat_pset_verdict_t;

/* Проверка всего набора. НИЧЕГО не включает и не меняет.
 * bad_idx (если не NULL) — индекс ПЕРВОГО непринятого. */
plat_pset_verdict_t plat_pset_admit(const plat_plugin_t *set, size_t n,
                                    size_t *bad_idx);

/* Включатель одного плагина: 0 — включён, иначе отказ. */
typedef int (*plat_pset_on_fn)(plat_plugin_t *p, void *ud);
/* Выключатель для отката. Обязан быть безусловным. */
typedef void (*plat_pset_off_fn)(plat_plugin_t *p, void *ud);

/* Допуск + включение. Если допуск не прошёл — НИ ОДИН не включается.
 * Если включение упало на k-м — все включённые до него выключаются, и в
 * наборе не остаётся ни одного активного. */
plat_pset_verdict_t plat_pset_activate(plat_plugin_t *set, size_t n,
                                       plat_pset_on_fn on,
                                       plat_pset_off_fn off, void *ud,
                                       size_t *bad_idx);

/* Сколько активных сейчас. */
size_t plat_pset_active_count(const plat_plugin_t *set, size_t n);

const char *plat_pset_verdict_name(plat_pset_verdict_t v);
include/platx/policy_dist.h
/* platx/policy_dist.h — INT-144: Signed policy bundle distribution.
 * HMAC verification hook is weak; unit tests link without a crypto impl. */

#define PLAT_POLICY_MAX_CAPS    32
#define PLAT_POLICY_MAX_BUNDLES 16
#define PLAT_POLICY_ID_LEN      64
#define PLAT_POLICY_CAP_LEN     64

typedef struct plat_policy_cap_entry {
    char     capability[PLAT_POLICY_CAP_LEN];
    uint32_t version;
    uint32_t flags;
} plat_policy_cap_entry_t;

typedef struct plat_policy_bundle {
    char                    policy_id[PLAT_POLICY_ID_LEN];
    uint32_t                version;
    plat_policy_cap_entry_t caps[PLAT_POLICY_MAX_CAPS];
    size_t                  n_caps;
    uint8_t                 hmac_digest[32]; /* HMAC-SHA256 signature */
    uint64_t                issued_at_ms;
    uint64_t                expiry_ms;       /* 0 = no expiry */
} plat_policy_bundle_t;

typedef struct plat_policy_dist_result {
    int  loaded;
    int  verified;   /* 1 if HMAC verified; 0 if verifier not linked */
    int  applied;
    char reason[128];
} plat_policy_dist_result_t;

int  plat_policy_dist_init(void);
void plat_policy_dist_fini(void);

/* Load and optionally verify a bundle. Returns 0 on success. */
int plat_policy_dist_load(const plat_policy_bundle_t *bundle,
                          plat_policy_dist_result_t *out);

/* Apply a loaded bundle to the running ABI. Returns 0 on success. */
int plat_policy_dist_apply(const plat_policy_bundle_t *bundle,
                           plat_policy_dist_result_t *out);

/* Revoke by policy_id. Returns 0 if found, -1 if not found. */
int plat_policy_dist_revoke(const char *policy_id);

/* Count active (applied, not expired) bundles. */
int plat_policy_dist_count(void);
include/platx/privcheck.h
/* privcheck.h — S519: недостающее право называется ДО попытки.
 *
 * ПОЧЕМУ «ДО», А НЕ «ПОСЛЕ»
 * ─────────────────────────
 * Привилегированную операцию почти всегда пишут так: попробовать, поймать
 * EPERM, пожаловаться. Это работает ровно до тех пор, пока операция не
 * оставляет следа. А она оставляет: половина цепочки уже выполнена, сокет
 * открыт, файл создан, поколение потрачено. Отказ приходит в середине, и
 * убирать за ним приходится тому же коду, который только что узнал, что он
 * не имел права начинать.
 *
 * И вторая, более тихая беда: EPERM ничего не называет. «Operation not
 * permitted» одинаково означает «нет CAP_NET_RAW», «нет CAP_SYS_ADMIN»,
 * «мешает NO_NEW_PRIVS» и «ядро старое». Все четыре чинятся по-разному,
 * и оператор различает их только чтением исходника.
 *
 * Поэтому здесь: объявленная таблица привилегированных операций, и у
 * каждой — требуемое право, способ ЭТО ПРОВЕРИТЬ и точное указание, что
 * сделать. Проверка происходит до первого побочного действия.
 *
 * ЧЕГО ЗДЕСЬ НЕТ
 * ──────────────
 * Это не замена проверке ядра. Ядро всё равно решает, и его отказ остаётся
 * последним словом. Здесь — предварительный ответ на вопрос «а стоит ли
 * начинать», и он обязан быть ЧЕСТНЫМ в обе стороны: сказать «нельзя» там,
 * где можно, так же вредно, как наоборот.
 */
typedef enum {
    PLAT_PRIV_RAW_SOCKET   = 0,  /* захват с интерфейса                */
    PLAT_PRIV_BPF_LOAD     = 1,  /* загрузка программы eBPF            */
    PLAT_PRIV_IO_URING_SQ  = 2,  /* SQPOLL: опрашивающий поток ядра    */
    PLAT_PRIV_USERNS       = 3,  /* своё пространство пользователей    */
    PLAT_PRIV_LANDLOCK     = 4,  /* ограничение доступа к ФС           */
    PLAT_PRIV_PTRACE       = 5,  /* присоединение к чужому процессу    */
    PLAT_PRIV_COUNT        = 6
} plat_priv_op_t;

typedef enum {
    PLAT_PRIV_OK       = 0,  /* можно начинать                        */
    PLAT_PRIV_MISSING  = 1,  /* права нет — названо какого            */
    PLAT_PRIV_NO_KERNEL= 2,  /* ядро не умеет — правом не поможешь    */
    PLAT_PRIV_BLOCKED  = 3,  /* запрещено политикой хоста             */
    PLAT_PRIV_UNKNOWN  = 4   /* проверить нечем — начинать вслепую    */
} plat_priv_verdict_t;

typedef struct {
    plat_priv_verdict_t verdict;
    char                op[48];      /* что собирались делать         */
    char                need[64];    /* чего именно не хватает        */
    char                how[160];    /* что сделать оператору         */
    char                probed[96];  /* чем проверяли — чтобы можно было перепроверить */
} plat_priv_report_t;

/* Проверить ДО попытки. Никаких побочных действий: ни сокета, ни файла,
 * ни процесса. Возвращает вердикт и заполняет отчёт. */
plat_priv_verdict_t plat_priv_check(plat_priv_op_t op, plat_priv_report_t *out);

/* Человеку — одной строкой. Никогда не NULL. */
const char *plat_priv_op_name(plat_priv_op_t op);
const char *plat_priv_verdict_name(plat_priv_verdict_t v);

/* Есть ли у нас возможность по имени бита (CAP_*). -1 — проверить нечем. */
int plat_priv_have_cap(int cap_bit);
include/platx/profile_check.h
/* platx/profile_check.h — INT-141: profile minimum capability contract.
 *
 * A profile declares the minimum set of capabilities that must be live
 * before it is considered READY. plat_profile_check() walks the required
 * list and verifies each is registered in the core capability table.
 *
 * Profiles are static descriptors; no dynamic registration.
 * "READY" means every required capability is present AND version-compatible.
 * A missing or version-incompatible capability is a hard fail, not a warning.
 *
 * ── A1-P02 wave 2 (2026-09-07). Additive; no legacy field moved. ─────────
 *   P02-054  The extended-policy block moved INSIDE the include guard and
 *            INSIDE extern "C". It used to sit after the `#endif`, guarded
 *            only by `#pragma once`. `#pragma once` keys on file identity,
 *            so two physical copies of this header on one include path
 *            (vendored copy, out-of-tree stage dir, A3 portable copy)
 *            were both expanded and every enum/struct was redeclared:
 *            35 hard errors. It was also outside extern "C", so a C++
 *            consumer mangled plat_profile_validate_ext and failed to link
 *            against the C core. Both are fixed here.
 *   P02-058  Error codes name the phase that refused. PLAT_PROFVAL_TRUSTGEN
 *            split out of PLAT_PROFVAL_BOUNDS; plat_profval_err_name()
 *            returns a stable name that never carries profile bytes,
 *            key material or digests.
 *   P02-055  plat_profile_snapshot_t: identity + revision + digest + boot
 *            generation, published once, read by every consumer.
 *   P02-056  plat_profile_apply_defaults(): defaults for absent optional
 *            fields that never weaken a mandatory CIVIL or MIL rule.
 *   P02-057  Semantic rejection of incompatible capability/limit combos.
 *   P02-063  plat_profile_ext_note_field(): order-independent duplicate
 *            accounting for mandatory fields, shared with the wire decoder.
 *   P02-071  plat_profile_set_limits(): the only supported mutator; it
 *            refuses after freeze with PLAT_PROFVAL_FROZEN.
 */

#define PLAT_PROFILE_ID_MAX     32
#define PLAT_PROFILE_CAP_MAX    16
#define PLAT_PROFILE_REASON_MAX 128

/* ── Required capability entry ───────────────────────────────────────── */
typedef struct plat_profile_cap_req {
    const char *capability;   /* capability name (must match capreg) */
    uint32_t    min_version;  /* minimum acceptable version */
} plat_profile_cap_req_t;

/* ── Profile descriptor ──────────────────────────────────────────────── */
typedef struct plat_profile_desc {
    char                       id[PLAT_PROFILE_ID_MAX];
    const plat_profile_cap_req_t *required;  /* NULL-terminated array */
    size_t                     n_required;
} plat_profile_desc_t;

/* ── Check result ────────────────────────────────────────────────────── */
typedef struct plat_profile_result {
    int  ready;                              /* non-zero = all caps present */
    char missing[PLAT_PROFILE_CAP_MAX][PLAT_PROFILE_ID_MAX]; /* absent caps */
    int  n_missing;
    char reason[PLAT_PROFILE_REASON_MAX];
} plat_profile_result_t;

/* ── API ─────────────────────────────────────────────────────────────── */

/* Check all required capabilities for profile desc.
 * Returns 0 (all present) or -1 (at least one missing).
 * out is always populated; out->ready reflects the overall verdict.
 */
int plat_profile_check(const plat_profile_desc_t *desc,
                       plat_profile_result_t      *out);

/* Well-known profile IDs */
#define PLAT_PROFILE_AGENT  "agent"   /* minimal agent: context+lease+gadget */
#define PLAT_PROFILE_SERVER "server"  /* server: agent + mbus + ra2c */
#define PLAT_PROFILE_FULL   "full"    /* full stack: all fabric subsystems */

/* Predefined profiles (call plat_profile_check with these) */
extern const plat_profile_desc_t plat_profile_agent;
extern const plat_profile_desc_t plat_profile_server;
extern const plat_profile_desc_t plat_profile_full;

/* ── Extended policy validation (A1-POLICY-LOCAL P02:051-080) ────────── */

/* Profile kind: governs MIL→CIVIL transition prevention (P02-070). */
typedef enum {
    PLAT_PROFILE_KIND_CIVIL = 0,
    PLAT_PROFILE_KIND_MIL   = 1,
} plat_profile_kind_t;

/* Module verification policy (P02-080): how the profile asserts module integrity. */
typedef enum {
    PLAT_MODVER_NONE     = 0,  /* No module verification required */
    PLAT_MODVER_REQUIRED = 1,  /* Module verification capability must be present */
    PLAT_MODVER_STRICT   = 2,  /* Strict: verifier must be linked and confirm */
} plat_modver_policy_t;

/* Watchdog co-validation parameters (P02-078). */
typedef struct plat_watchdog_policy {
    uint32_t interval_ms;    /* Heartbeat interval; 0 = not configured */
    uint32_t max_strikes;    /* Max missed heartbeats before restart; 0 = uncapped */
    uint32_t restart_ceil;   /* Max restarts before permanent failure; 0 = uncapped */
} plat_watchdog_policy_t;

/* Audit retention policy fields (P02-079). */
typedef struct plat_audit_policy {
    int      required;          /* Non-zero: audit sink is mandatory at startup */
    uint32_t min_retention_s;   /* Minimum retention in seconds; 0 = no minimum */
} plat_audit_policy_t;

/* Numeric budget limits passed from profile snapshot to runtime (P02-076). */
typedef struct plat_profile_limits {
    uint32_t max_xio_owners;   /* 0 = not set (P02-077 check) */
    uint32_t max_tasks;        /* 0 = not set */
    uint32_t max_events;       /* 0 = not set */
} plat_profile_limits_t;

#define PLAT_PROFILE_EXT_VERSION 1

/* P02-063: mandatory-field identifiers. The wire decoder reports each
 * mandatory field it has seen through plat_profile_ext_note_field(); a
 * second report of the same identifier is a duplicate regardless of the
 * order the fields arrived in. */
typedef enum {
    PLAT_PROFFIELD_KIND     = 0,
    PLAT_PROFFIELD_MODVER   = 1,
    PLAT_PROFFIELD_LIMITS   = 2,
    PLAT_PROFFIELD_WATCHDOG = 3,
    PLAT_PROFFIELD_AUDIT    = 4,
    PLAT_PROFFIELD_TRUSTGEN = 5,
    PLAT_PROFFIELD__COUNT   = 6,
} plat_proffield_t;

/* Extended profile descriptor: carries all load-time policy fields.
 * Fields are validated by plat_profile_validate_ext() before startup.
 * After plat_profile_freeze(), the struct must not be mutated (P02-071). */
typedef struct plat_profile_ext {
    uint32_t               version;         /* Must be PLAT_PROFILE_EXT_VERSION */
    uint32_t               n_required;      /* Cap count (bounds-checked, P02-061) */
    plat_profile_kind_t    kind;            /* MIL or CIVIL (P02-070) */
    plat_modver_policy_t   modver;          /* Module verification policy (P02-080) */
    plat_watchdog_policy_t watchdog;        /* Watchdog co-validation (P02-078) */
    plat_audit_policy_t    audit;           /* Audit retention policy (P02-079) */
    plat_profile_limits_t  limits;          /* Numeric budget limits (P02-076) */
    uint64_t               trust_gen;       /* Trust root generation (P02-066) */
    uint64_t               issued_at_ms;    /* Issuance time; 0 = no freshness (P02-069) */
    uint64_t               expires_at_ms;   /* 0 = no expiry */
    int                    revoked;         /* Non-zero: revoked; check before READY (P02-067) */
    int                    frozen;          /* Non-zero: locked, no mutations (P02-071) */
    uint32_t               seen_mask;       /* P02-063: plat_proffield_t bitmap */
    /* No padding fields: reserved bits are not extensible silently (P02-064) */
} plat_profile_ext_t;

/* Validation error codes (P02-058).
 * Each code names the phase that refused. No code carries profile bytes,
 * key material or a digest — see plat_profval_err_name(). */
typedef enum {
    PLAT_PROFVAL_OK          = 0,
    PLAT_PROFVAL_BOUNDS      = 1,   /* P02-061: truncated/out-of-range input */
    PLAT_PROFVAL_OVERFLOW    = 2,   /* P02-062: length/offset overflow */
    PLAT_PROFVAL_DUPLICATE   = 3,   /* P02-063: duplicate mandatory field */
    PLAT_PROFVAL_CRITICAL    = 4,   /* P02-064: unknown critical field */
    PLAT_PROFVAL_REVOKED     = 5,   /* P02-067: revoked profile */
    PLAT_PROFVAL_NO_VERIFIER = 6,   /* P02-068: verifier absent, auth required */
    PLAT_PROFVAL_STALE       = 7,   /* P02-069: freshness check failed */
    PLAT_PROFVAL_MIL_CIVIL   = 8,   /* P02-070: MIL→CIVIL transition blocked */
    PLAT_PROFVAL_FROZEN      = 9,   /* P02-071: mutation after freeze */
    PLAT_PROFVAL_ZERO_BUDGET = 10,  /* P02-077: unconfigured critical budget */
    PLAT_PROFVAL_WATCHDOG    = 11,  /* P02-078: watchdog co-validation failed */
    PLAT_PROFVAL_AUDIT       = 12,  /* P02-079: audit retention field invalid */
    PLAT_PROFVAL_MODVER      = 13,  /* P02-080: module verification policy violation */
    PLAT_PROFVAL_TRUSTGEN    = 14,  /* P02-066: trust-root generation mismatch */
    PLAT_PROFVAL_MAGIC       = 15,  /* P02-052: wire magic/version not ours */
    PLAT_PROFVAL_SEMANTICS   = 16,  /* P02-057: capability/limit combo refused */
    PLAT_PROFVAL_AUTH        = 17,  /* P02-065: authenticated span does not
                                     *          cover the semantic bytes */
} plat_profval_err_t;

typedef struct plat_profile_val_result {
    plat_profval_err_t code;
    char               reason[PLAT_PROFILE_REASON_MAX];
} plat_profile_val_result_t;

/* Validate an extended profile descriptor. Must be called before READY.
 * current_trust_gen: current boot's trust root generation (for P02-066).
 * now_ms: current monotonic time in ms (for P02-069 freshness check).
 * current_kind: running profile kind (for P02-070 MIL→CIVIL check).
 * Returns PLAT_PROFVAL_OK on success; populates out on any failure.
 */
plat_profval_err_t plat_profile_validate_ext(
        const plat_profile_ext_t   *ext,
        uint64_t                    current_trust_gen,
        uint64_t                    now_ms,
        plat_profile_kind_t         current_kind,
        plat_profile_val_result_t  *out);

/* Freeze the profile after successful validation (P02-071/072).
 * Sets ext->frozen = 1. Subsequent mutation attempts through
 * plat_profile_set_limits() are refused with PLAT_PROFVAL_FROZEN.
 */
void plat_profile_freeze(plat_profile_ext_t *ext);

/* P02-071/075: the only supported mutator for a validated descriptor.
 * Refuses with PLAT_PROFVAL_FROZEN once plat_profile_freeze() ran, so a
 * caller that kept a non-const alias cannot widen policy after READY. */
plat_profval_err_t plat_profile_set_limits(plat_profile_ext_t          *ext,
                                           const plat_profile_limits_t *lim,
                                           plat_profile_val_result_t   *out);

/* Extract validated numeric limits from a frozen profile (P02-076).
 * Returns -1 if ext is NULL or not frozen.
 */
int plat_profile_get_limits(const plat_profile_ext_t *ext,
                            plat_profile_limits_t    *out);

/* P02-056: fill absent optional fields with their defaults.
 * Defaults are chosen so they never weaken a mandatory rule: a MIL profile
 * gets audit.required=1 and modver=REQUIRED, a CIVIL profile keeps audit
 * optional but still gets a finite watchdog once any watchdog field is set.
 * Mandatory fields (limits, kind) are never invented — an absent limit
 * stays zero and is refused by P02-077. Refuses on a frozen descriptor. */
plat_profval_err_t plat_profile_apply_defaults(plat_profile_ext_t        *ext,
                                               plat_profile_val_result_t *out);

/* P02-063: record that a mandatory field was present on the wire.
 * Returns PLAT_PROFVAL_DUPLICATE if the same field was already recorded,
 * independently of the order fields arrived in. */
plat_profval_err_t plat_profile_ext_note_field(plat_profile_ext_t        *ext,
                                               plat_proffield_t           f,
                                               plat_profile_val_result_t *out);

/* Stable name of a validation code. Never contains profile bytes, key
 * material or digests — a refusal names the phase, not the secret (P02-058). */
const char *plat_profval_err_name(plat_profval_err_t code);

/* ── Immutable profile snapshot (P02-055/073/074/075) ─────────────────── */

#define PLAT_PROFILE_DIGEST_LEN 32

/* One immutable instance of the selected policy. Published exactly once
 * per boot by plat_profile_snapshot_publish(); every consumer reads the
 * same bytes through plat_profile_snapshot_get(). */
typedef struct plat_profile_snapshot {
    char                  id[PLAT_PROFILE_ID_MAX];      /* profile identity  */
    uint32_t              revision;                     /* profile revision  */
    uint8_t               digest[PLAT_PROFILE_DIGEST_LEN]; /* authenticated  */
    uint64_t              boot_gen;                     /* P02-073 boot gen  */
    plat_profile_kind_t   kind;
    plat_profile_limits_t limits;
    /* P02-078/A1-P04-184: watchdog-политика входит в снимок, потому что
     * «сколько молчания считается слишком долгим» — решение профиля, а не
     * константа в коде подсистемы задач. Пока её здесь не было, plat_task.c
     * пользовался зашитым «2× heartbeat_sec»: числом, которого никто не
     * утверждал и которое нельзя изменить, не пересобрав ядро. */
    plat_watchdog_policy_t watchdog;
    int                   published;                    /* non-zero = live   */
} plat_profile_snapshot_t;

/* P02-055/073: publish the one immutable snapshot for this boot.
 * The digest is copied in, not referenced, so a later mutation of the
 * caller's buffer cannot change runtime policy (P02-071).
 * Refuses with PLAT_PROFVAL_FROZEN if a snapshot is already published for
 * this boot generation — a second publish would be a policy swap without
 * a new boot context (P02-075). */
plat_profval_err_t plat_profile_snapshot_publish(
        const plat_profile_ext_t  *ext,
        const char                *id,
        uint32_t                   revision,
        const uint8_t              digest[PLAT_PROFILE_DIGEST_LEN],
        uint64_t                   boot_gen,
        plat_profile_val_result_t *out);

/* Read the published snapshot. Returns 0 on success, -1 if none published.
 * The caller receives a copy: there is no writable alias to live policy. */
int plat_profile_snapshot_get(plat_profile_snapshot_t *out);

/* P02-074: the CLI/diagnostic path. Returns a one-line status containing
 * the profile id, revision, boot generation and the first 8 hex digits of
 * the digest. Never prints key material. Returns the number of bytes that
 * would be written (snprintf semantics), or -1 if nothing is published. */
int plat_profile_snapshot_status(char *buf, size_t buflen);

/* P02-074/075: any attempt to install a different policy after a snapshot
 * is live is refused. Returns PLAT_PROFVAL_FROZEN and leaves the active
 * digest and limits untouched. Exposed so the CLI and the API setter share
 * one refusal, rather than each inventing its own. */
plat_profval_err_t plat_profile_snapshot_replace_attempt(
        plat_profile_val_result_t *out);

/* Test-only: drop the published snapshot so a fixture can simulate a new
 * boot context. Never called on a production startup path. */
void plat_profile_snapshot_reset_for_test(void);
include/platx/profile_wire.h
/* platx/profile_wire.h — A1-P02: portable load-time profile codec.
 *
 * Two things live on the wire and they are NOT the same thing (P02-053):
 *
 *   build composition   what this binary actually contains. Fixed at link
 *                       time, discoverable, never read from a file.
 *   load-time policy    what the operator asks this binary to enforce.
 *                       Read from the profile blob, authenticated, then
 *                       frozen.
 *
 * A profile that requests a provider the binary does not contain is a
 * composition error and is refused before any module initialises — it is
 * not a policy that "degrades". plat_profile_wire_bind() is where the two
 * meet, and it is the only place they are allowed to meet.
 *
 * ── Base image [FROZEN at 120 bytes by C31] ──────────────────────────────
 * Byte-for-byte the layout platx-windows/win/plat/platx_profile.h freezes.
 * That file is A3's; this is the portable Linux reader for the same bytes,
 * so a vector produced on either side decodes identically (P02-052/059).
 * All integers are big-endian. Offsets are absolute from byte 0.
 *
 *    off  size  field
 *      0     4  magic          0x504C5458
 *      4     4  version        1
 *      8     4  flags          PLAT_PROFW_FLAG_*
 *     12     4  threat_level   0..4
 *     16     4  max_leases
 *     20     4  max_tasks
 *     24     4  max_events
 *     28     4  audit_mask
 *     32     4  lease_ttl      seconds
 *     36     4  task_ttl       seconds
 *     40     8  padding        must be zero
 *     48    32  platform_id
 *     80     8  reserved2      must be zero
 *     88    32  hmac           authenticates bytes [0, 88)
 *    120        end of base image
 *
 * ── Versioned extension area (P02-054) ───────────────────────────────────
 * New policy is added AFTER byte 120 as TLV sections, never by re-reading
 * a legacy offset with a new meaning. A decoder that does not know a
 * section skips it — unless the section is marked critical, in which case
 * the profile is refused (P02-064). Section header, big-endian:
 *
 *      0     2  type      PLAT_PROFW_SEC_*
 *      2     2  sflags    bit 0 = critical
 *      4     4  length    payload bytes that follow this 8-byte header
 *
 * ── Authentication span (P02-065) ────────────────────────────────────────
 * The verifier is bound to the exact bytes the semantic parser accepted.
 * plat_profile_wire_t.auth_len records that span. Nothing is normalised,
 * re-encoded, trimmed or case-folded before verification: the decoder
 * reads out of the caller's buffer and never writes back into it.
 */

/* ── Base image constants [FROZEN — mirror of A3's platx_profile.h] ────── */
#define PLAT_PROFW_MAGIC        0x504C5458u
#define PLAT_PROFW_VERSION      1u
#define PLAT_PROFW_SIZE         120u
#define PLAT_PROFW_HMAC_OFF     88u
#define PLAT_PROFW_HMAC_LEN     32u
#define PLAT_PROFW_PLATID_OFF   48u
#define PLAT_PROFW_PLATID_LEN   32u
#define PLAT_PROFW_PAD_OFF      40u
#define PLAT_PROFW_PAD_LEN      8u
#define PLAT_PROFW_RSV2_OFF     80u
#define PLAT_PROFW_RSV2_LEN     8u

/* Flags. Any bit outside the known mask is an unknown mandatory semantic
 * and is refused with PLAT_PROFVAL_CRITICAL (P02-064). */
#define PLAT_PROFW_FLAG_MIL_MODE     0x0001u
#define PLAT_PROFW_FLAG_AUDIT_ALL    0x0002u
#define PLAT_PROFW_FLAG_NO_NETWORK   0x0004u
#define PLAT_PROFW_FLAG_LOCKDOWN_FS  0x0008u
#define PLAT_PROFW_FLAG_SIGNED_ONLY  0x0010u
#define PLAT_PROFW_FLAG_NO_EXEC      0x0020u
#define PLAT_PROFW_FLAG_COVERT       0x0040u
#define PLAT_PROFW_FLAG_DAL_B        0x0080u
#define PLAT_PROFW_FLAG_GOST         0x0100u
#define PLAT_PROFW_FLAG_MIL_STD882   0x0200u
#define PLAT_PROFW_FLAGS_KNOWN_MASK  0x03FFu

/* Range contract, agreed with A3 in platx_profile.h (P01-022). */
#define PLAT_PROFW_LIMIT_MIN    1u
#define PLAT_PROFW_LIMIT_MAX    65535u
#define PLAT_PROFW_TTL_MIN_S    1u
#define PLAT_PROFW_TTL_MAX_S    86400u
#define PLAT_PROFW_THREAT_MAX   4u

/* ── Extension sections (P02-054/062/063/064) ─────────────────────────── */
#define PLAT_PROFW_SECHDR_LEN   8u
#define PLAT_PROFW_SEC_CRITICAL 0x0001u   /* sflags bit 0 */

typedef enum {
    PLAT_PROFW_SEC_WATCHDOG = 1,      /* 12 bytes: interval, strikes, ceil    */
    PLAT_PROFW_SEC_AUDIT    = 2,      /*  8 bytes: required, min_retention_s  */
    PLAT_PROFW_SEC_TRUST    = 3,      /* 24 bytes: trust_gen, issued, expires */
    PLAT_PROFW_SEC_MODVER   = 4,      /*  4 bytes: plat_modver_policy_t       */
    PLAT_PROFW_SEC_END      = 0xFFFF, /*  0 bytes: mandatory terminator       */
} plat_profw_sec_t;

/* ── Why the extension area needs a terminator (P02-061) ──────────────────
 * The base image is frozen at 120 bytes and has no room for a total
 * length, so nothing in it states how many sections should follow. That
 * makes a blob truncated exactly on a section boundary a well-formed
 * SHORTER blob — and dropping the trailing sections is a policy DOWNGRADE,
 * not a corruption: cutting the TRUST section off a MIL profile leaves
 * trust_gen == 0, which switches off both the trust-generation check
 * (P02-066) and the mandatory-verifier check (P02-068).
 *
 * This was found by the truncation sweep in test_profile_wire.c, which
 * accepted a 148-byte prefix of a 180-byte MIL vector.
 *
 * So: when an extension area is present it MUST end with a critical,
 * zero-length PLAT_PROFW_SEC_END. Truncating at any section boundary
 * removes the terminator and the blob is refused with BOUNDS. The
 * terminator is inside the authenticated span, so it cannot be re-added
 * by anyone who cannot re-sign the profile.
 */

/* Extension area limits. A blob larger than this is refused outright
 * rather than parsed, so a hostile length field can never drive the
 * decoder past the caller's buffer (P02-061/062). */
#define PLAT_PROFW_EXT_MAX      4096u
#define PLAT_PROFW_BLOB_MAX     (PLAT_PROFW_SIZE + PLAT_PROFW_EXT_MAX)

/* ── Decoded image (host byte order) ──────────────────────────────────── */
typedef struct plat_profile_wire {
    uint32_t magic;
    uint32_t version;
    uint32_t flags;
    uint32_t threat_level;
    uint32_t max_leases;
    uint32_t max_tasks;
    uint32_t max_events;
    uint32_t audit_mask;
    uint32_t lease_ttl;
    uint32_t task_ttl;
    uint8_t  platform_id[PLAT_PROFW_PLATID_LEN];
    uint8_t  hmac[PLAT_PROFW_HMAC_LEN];

    /* P02-065: the exact byte span the verifier must authenticate. It is
     * an offset into the CALLER's buffer; the decoder copies nothing back
     * and normalises nothing, so verify(buf, auth_len) and the semantic
     * result below are derived from the same bytes. */
    size_t   auth_len;

    /* Policy assembled from base flags + extension sections. */
    plat_profile_ext_t ext;
} plat_profile_wire_t;

/* Decode and semantically check a profile blob.
 *
 * len must be exactly PLAT_PROFW_SIZE, or PLAT_PROFW_SIZE plus a
 * well-formed extension area. Every shorter prefix is refused with
 * PLAT_PROFVAL_BOUNDS and no byte past len is read (P02-061).
 *
 * Returns PLAT_PROFVAL_OK, or the code naming the phase that refused.
 * out is always populated when non-NULL; *w is only meaningful on OK.
 */
plat_profval_err_t plat_profile_wire_decode(const uint8_t             *buf,
                                            size_t                     len,
                                            plat_profile_wire_t       *w,
                                            plat_profile_val_result_t *out);

/* ── Build composition vs load-time policy (P02-053) ──────────────────── */

/* What this binary contains. Set at link time by the composition unit;
 * a profile can only ever ask for a subset of these. */
typedef struct plat_profile_composition {
    uint32_t providers;       /* PLAT_PROFW_PROV_* bitmap */
    int      has_verifier;    /* module verification provider linked */
    int      has_audit_sink;  /* audit sink provider linked */
    int      has_watchdog;    /* watchdog provider linked */
} plat_profile_composition_t;

#define PLAT_PROFW_PROV_AUDIT     0x0001u
#define PLAT_PROFW_PROV_WATCHDOG  0x0002u
#define PLAT_PROFW_PROV_MODVER    0x0004u
#define PLAT_PROFW_PROV_NETWORK   0x0008u

/* Reject a profile that asks for a provider this binary does not contain.
 * This runs BEFORE module initialisation, so a missing provider is a
 * refused startup and never a silently reduced policy (P02-053/057).
 * Returns PLAT_PROFVAL_OK or PLAT_PROFVAL_SEMANTICS. */
plat_profval_err_t plat_profile_wire_bind(
        const plat_profile_wire_t        *w,
        const plat_profile_composition_t *comp,
        plat_profile_val_result_t        *out);

/* The composition of the binary the caller is linked into. Weak: a unit
 * test that does not link a composition unit gets the empty composition,
 * which refuses every provider request rather than allowing it. */
const plat_profile_composition_t *plat_profile_composition(void);
include/platx/provgen.h
/* provgen.h — S479: смена поколения провайдера ПРИ ЖИВЫХ СЕССИЯХ.
 *
 * Заменить транспорт или провайдера на работающем узле — это не «переставить
 * указатель». В момент перестановки есть сессии, которые уже начались на
 * прежнем поколении, и у каждой из них своё представление о том, с кем она
 * разговаривает.
 *
 * Отсюда четыре правила, и каждое закрывает свой способ потерять данные.
 *
 * 1. СЕССИЯ НЕ ПЕРЕЕЗЖАЕТ. Начавшаяся на поколении N доживает на N. Перевести
 *    её на N+1 посередине значит поменять под ней состояние, о котором она
 *    ничего не знает: буферы, ключи, счётчики — всё чужое.
 *
 * 2. ПРЕЖНЕЕ ПОКОЛЕНИЕ ОБСЛУЖИВАЕТ ДО КОНЦА. Оно снимается не тогда, когда
 *    пришло новое, а когда ушла ЕГО ПОСЛЕДНЯЯ сессия. Иначе «замена без
 *    простоя» означает обрыв всех текущих соединений.
 *
 * 3. ОТКАЗ НИЧЕГО НЕ МЕНЯЕТ. Если новое поколение не поднялось, старое
 *    остаётся текущим — и не «возвращается назад», а просто никогда не
 *    переставало им быть. Разница видна при отказе на середине: возвращать
 *    некуда, потому что не уходили.
 *
 * 4. НОВЫЕ СЕССИИ ИДУТ НА НОВОЕ ТОЛЬКО ПОСЛЕ ФИКСАЦИИ. Пока идёт подготовка,
 *    новое поколение не принимает никого: сессия, начатая на непринятом
 *    поколении, при откате остаётся сиротой.
 */
#define PLAT_PROVGEN_MAX 4   /* текущее + отставные с недошедшими сессиями */

typedef enum {
    PLAT_PG_EMPTY    = 0,
    PLAT_PG_STAGING  = 1,   /* готовится, никого не принимает      */
    PLAT_PG_CURRENT  = 2,   /* принимает новые сессии              */
    PLAT_PG_DRAINING = 3    /* новых не принимает, старые доживают */
} plat_pg_state_t;

typedef struct {
    uint64_t        gen;
    plat_pg_state_t state;
    uint32_t        sessions;   /* сколько живых сессий на этом поколении */
} plat_pg_slot_t;

typedef struct {
    plat_pg_slot_t slot[PLAT_PROVGEN_MAX];
    uint64_t       next_gen;
} plat_provgen_t;

typedef enum {
    PLAT_PG_OK        = 0,
    PLAT_PG_BUSY      = 1,  /* нет места: слишком много непросохших поколений */
    PLAT_PG_NO_STAGED = 2,
    PLAT_PG_REFUSED   = 3,  /* подготовка не прошла — текущее не тронуто */
    PLAT_PG_NO_CURRENT= 4,
    PLAT_PG_BAD_ARG   = 5
} plat_pg_verdict_t;

/* Проверка поднявшегося поколения перед фиксацией. 0 — годно. */
typedef int (*plat_pg_probe_fn)(uint64_t gen, void *ud);

void plat_provgen_init(plat_provgen_t *p);

/* Завести новое поколение в состоянии STAGING. Текущее продолжает работать. */
plat_pg_verdict_t plat_provgen_stage(plat_provgen_t *p, uint64_t *out_gen);

/* Проверить и зафиксировать. При отказе пробы STAGING снимается, текущее
 * НЕ ТРОГАЕТСЯ (правило 3), живые сессии не задеты. */
plat_pg_verdict_t plat_provgen_commit(plat_provgen_t *p,
                                      plat_pg_probe_fn probe, void *ud);

/* Открыть сессию. Всегда на CURRENT — и только на нём (правило 4).
 * Возвращает поколение сессии в *gen. */
plat_pg_verdict_t plat_provgen_open(plat_provgen_t *p, uint64_t *gen);

/* Закрыть сессию своего поколения. Опустевшее DRAINING освобождается. */
plat_pg_verdict_t plat_provgen_close(plat_provgen_t *p, uint64_t gen);

uint64_t plat_provgen_current(const plat_provgen_t *p);
uint32_t plat_provgen_sessions(const plat_provgen_t *p, uint64_t gen);
int      plat_provgen_alive(const plat_provgen_t *p, uint64_t gen);
size_t   plat_provgen_live_gens(const plat_provgen_t *p);

const char *plat_pg_verdict_name(plat_pg_verdict_t v);
include/platx/reclog.h
/* reclog.h — S402/S416: кадрированный журнал записей.
 *
 * ФОРМАТ
 * ──────
 *   Заголовок файла (16 байт):
 *     magic    uint32   PLAT_RECLOG_MAGIC
 *     version  uint16   версия формата
 *     hdr_len  uint16   длина ЭТОГО заголовка
 *     reserved uint64   обязан быть нулём
 *
 *   Запись:
 *     len      uint32   длина полезной нагрузки
 *     crc      uint32   CRC-32 от (len || payload)
 *     payload  len байт
 *
 * Почему CRC накрывает ДЛИНУ, а не только полезную нагрузку: испорченная длина
 * иначе неотличима от правильной, и читатель разложил бы по ней весь остаток
 * файла — одна перевёрнутая ячейка сместила бы границы всех последующих
 * записей, и каждая из них прошла бы как «целая». Длина под CRC делает
 * рассинхронизацию видимой на первой же записи.
 *
 * hdr_len отдельно от version: читатель обязан уметь ПРОПУСТИТЬ заголовок
 * большей длины, чем знает. Без этого поля добавление поля в заголовок —
 * несовместимое изменение, а с ним — совместимое.
 *
 * ЧТО ЗНАЧИТ «ВОССТАНОВИТЬ»
 * ─────────────────────────
 * Читатель возвращает САМЫЙ ДЛИННЫЙ ПРОВЕРЯЕМЫЙ ПРЕФИКС и останавливается на
 * первой записи, которая обрезана или не сходится по CRC. Он НЕ пропускает
 * плохую запись, чтобы продолжить дальше: за обрывом лежат байты, о которых
 * ничего не известно, и принять их за записи значило бы принять подделанный
 * хвост. Пропуск с продолжением — именно тот способ, которым журнал
 * «восстанавливается» в чужие данные.
 *
 * Отказ на первой же записи — это префикс нулевой длины, а не ошибка чтения:
 * пустой проверяемый префикс тоже ответ.
 */
#define PLAT_RECLOG_MAGIC    0x50584C47u   /* "PXLG" */
#define PLAT_RECLOG_VERSION  1u
#define PLAT_RECLOG_HDR_LEN  16u
#define PLAT_RECLOG_MAX_REC  (16u * 1024u * 1024u)

/* Почему обрыв. Значение отличает «файл кончился» от «байты не сходятся»:
 * первое — обычный след краха, второе — повод не доверять источнику. */
typedef enum {
    PLAT_RECLOG_END_CLEAN     = 0,  /* дочитан до конца, всё сошлось        */
    PLAT_RECLOG_END_TRUNCATED = 1,  /* файл обрывается посреди записи       */
    PLAT_RECLOG_END_BADCRC    = 2,  /* запись на месте, но байты не сходятся*/
    PLAT_RECLOG_END_BADLEN    = 3   /* длина невозможна (0 или > максимума) */
} plat_reclog_end_t;

typedef struct {
    uint64_t          records;    /* сколько записей проверено              */
    uint64_t          good_bytes; /* смещение конца проверяемого префикса   */
    plat_reclog_end_t end;
} plat_reclog_scan_t;

struct xio_t;

/* Создать пустой журнал (заголовок файла). 0 / -errno. */
int plat_reclog_create(const struct xio_t *io, const char *path, int mode);

/* Дописать запись и сделать её долговечной. 0 / -errno.
 * Заголовок записи и нагрузка пишутся ОДНИМ вызовом: два вызова оставляли бы
 * после краха заголовок без нагрузки заметно чаще, чем это необходимо. */
int plat_reclog_append(const struct xio_t *io, const char *path,
                       const void *rec, size_t len);

/* Пройти журнал. Возвращает 0, если файл открыт и заголовок принят,
 * -errno иначе (в том числе при чужой magic, старшей версии и ненулевом
 * reserved). Результат обхода — в out; end говорит, чем кончился префикс.
 *
 * cb вызывается на каждую ПРОВЕРЕННУЮ запись; NULL допустим, если нужен
 * только размер префикса. */
int plat_reclog_scan(const struct xio_t *io, const char *path,
                     void (*cb)(const void *rec, size_t len, void *ud),
                     void *ud, plat_reclog_scan_t *out);

/* Причина карантина. Отличать обязательно: обрыв — обычный след краха, и
 * усечь его до проверенного префикса законно; несошедшиеся байты и чужая
 * версия — чужие данные, и трогать их нельзя. */
typedef enum {
    PLAT_RECLOG_Q_NONE      = 0,  /* нечего делать                          */
    PLAT_RECLOG_Q_TRIMMED   = 1,  /* хвост отрезан до проверенного префикса */
    PLAT_RECLOG_Q_SET_ASIDE = 2   /* файл отложен целиком, содержимое цело  */
} plat_reclog_quar_t;

/* Привести журнал в состояние, на которое можно опираться.
 *
 *   обрыв (TRUNCATED)        → усечь до проверенного префикса. Отрезаются
 *                              только байты недописанной записи: они не
 *                              содержат ничьего решения.
 *   несходство (BADCRC/BADLEN) → отложить ВЕСЬ файл под ограниченным именем и
 *                              с правами 0600, на его месте создать пустой
 *                              журнал. Хвост может быть подделан, а может
 *                              быть уликой — ни то ни другое не удаляют.
 *   чужая версия / magic     → отложить, НЕ ТРОГАЯ НИ БАЙТА (S430): формат
 *                              старше нашего понимания читается откатом или
 *                              человеком, и «починить» его мы не в праве.
 *
 * Имя карантина ограничено: путь + ".quar" + номер, не более 32 попыток,
 * после чего честный отказ. Неограниченный перебор на повреждённом входе —
 * это вечный цикл, в который систему и загоняют (S429).
 *
 * Повторный вызов на уже приведённом журнале ничего не делает и возвращает
 * PLAT_RECLOG_Q_NONE: приведение обязано быть идемпотентным, потому что
 * перезапуск после краха может случиться и во время него самого (S426).
 *
 * 0 / -errno. Что именно сделано — в *what. */
int plat_reclog_quarantine(const struct xio_t *io, const char *path,
                           plat_reclog_quar_t *what, char *quar_path,
                           size_t quar_path_sz);

/* CRC-32 (IEEE 802.3), вынесен ради проверок. */
uint32_t plat_reclog_crc32(uint32_t seed, const void *data, size_t len);
include/platx/recovery.h
/* platx/recovery.h — recovery decides; lifecycle alone executes.
 * This file must never call descriptor hooks.
 */
typedef enum plat_recovery_event {
    PLAT_REC_DEGRADED = 1,
    PLAT_REC_FATAL,     /* health fatal / task ERROR */
    PLAT_REC_CRASH,     /* abort / unexpected death */
    PLAT_REC_KILLED,    /* intentional kill */
    PLAT_REC_EXITED     /* clean DONE */
} plat_recovery_event_t;

typedef enum plat_recovery_policy {
    PLAT_REC_NEVER = 0,
    PLAT_REC_ON_FAILURE,
    PLAT_REC_ON_ABNORMAL,
    PLAT_REC_ALWAYS
} plat_recovery_policy_t;

typedef enum plat_recovery_status {
    PLAT_REC_ST_OK = 0,
    PLAT_REC_ST_BACKOFF,
    PLAT_REC_ST_FATAL
} plat_recovery_status_t;

typedef enum plat_recovery_action {
    PLAT_REC_ACT_NONE = 0,
    PLAT_REC_ACT_DEGRADE,
    PLAT_REC_ACT_RESTART,
    PLAT_REC_ACT_FAIL
} plat_recovery_action_t;

typedef struct plat_recovery_opts {
    plat_recovery_policy_t policy; /* default ON_FAILURE */
    int max_restarts;              /* 0 → 5 */
    int window_sec;                /* 0 → 60 */
    int backoff_start;             /* 0 → 1 */
    int backoff_max;               /* 0 → 300 */
} plat_recovery_opts_t;

typedef struct plat_recovery {
    plat_recovery_opts_t   opts;
    plat_recovery_status_t status;
    plat_recovery_action_t last_action;
    int                    restart_count;
    time_t                 window_start;
    time_t                 next_allowed;
    int                    backoff_sec;
    plat_lifecycle_t      *lm;     /* observed, not owned; may be NULL */
} plat_recovery_t;

int plat_recovery_init(plat_recovery_t *r, plat_lifecycle_t *lm,
                       const plat_recovery_opts_t *opts);

/* Policy + window + backoff. No lifecycle call. */
plat_recovery_action_t plat_recovery_choose(plat_recovery_t *r,
                                            plat_recovery_event_t ev,
                                            time_t now);

/* choose() + plat_lifecycle_request(). Exhaustion → FAIL, not another RESTART. */
int plat_recovery_apply(plat_recovery_t *r, plat_recovery_event_t ev);
int plat_recovery_apply_at(plat_recovery_t *r, plat_recovery_event_t ev,
                           time_t now);

/* Default ON_FAILURE, one-shot state. Existing tests / simple callers. */
int plat_recovery_decide(plat_lifecycle_t *lm, plat_recovery_event_t ev);

/* Watch table keyed by plat_owner_t. r is caller-owned and must outlive
 * unwatch *and* a concurrent tick (unwatch waits for in-flight tick).
 * generation 0 is rejected. Stale generation does not match. */
int plat_recovery_watch(plat_recovery_t *r, plat_lifecycle_t *lm,
                        plat_owner_t owner, const plat_recovery_opts_t *opts);
int plat_recovery_unwatch(plat_owner_t owner);
/* Same instance, new generation. Does not re-init r (budget stays). */
int plat_recovery_retarget(plat_owner_t from, plat_owner_t to);
/* health() + task silence > 2×heartbeat_sec → apply_at. Never start/stop. */
void plat_recovery_tick(time_t now);

/* Live watch row. r is the pointer passed to watch; name is desc->name. */
typedef struct plat_recovery_snap {
    plat_owner_t      owner;
    plat_recovery_t  *r;
    plat_lifecycle_t *lm;
    const char       *name;
} plat_recovery_snap_t;

/* Copy the watch table. out/max invalid → 0. No mbus, no JSON. */
int plat_recovery_snapshot(plat_recovery_snap_t *out, int max);

/* Fact after apply. Stack-copied into PLAT_EV_RECOVERY. No pointers. */
typedef struct plat_recovery_fact {
    plat_recovery_action_t action; /* RESTART, or FAIL when budget is gone */
    uint32_t               id;     /* module_id if lm has owner, else 0 */
    int                    restart_count;
} plat_recovery_fact_t;
include/platx/replay.h
/* replay.h — S438/S440: окно антиповтора и ровно одна личность.
 *
 * S438  Окно приёма хранит границу принятого и битовую карту недавних. После
 *       перезапуска оно НЕ имеет права начаться с нуля: нулевое окно принимает
 *       заново всё, что уже было принято, — то есть перезапуск открывает
 *       повтор. Если состояние не читается, окно ПЕРЕСОГЛАСУЕТСЯ: граница
 *       поднимается так, что старое не проходит. Отказ идёт в сторону, которая
 *       не принимает лишнего.
 *
 * S440  Личность меняется целиком. В любой момент действует РОВНО ОДНА
 *       генерация: незавершённая смена не оставляет двух действующих и не
 *       оставляет ни одной.
 */
struct xio_t;

#define PLAT_RWIN_BITS 64

typedef struct {
    uint64_t high;               /* наибольший принятый номер            */
    uint64_t mask;               /* битовая карта: high-1 .. high-64      */
    int      renegotiated;       /* состояние не прочлось — окно поднято  */
} plat_rwin_t;

typedef enum {
    PLAT_RWIN_ACCEPT = 0,
    PLAT_RWIN_REPLAY = 1,   /* уже принимали                      */
    PLAT_RWIN_TOO_OLD = 2   /* вышло за окно — судить не можем    */
} plat_rwin_verdict_t;

/* Загрузить окно. Если файла нет или он нечитаем, окно ПЕРЕСОГЛАСОВАНО:
 * high поднимается до floor, и всё ниже отвергается. */
int plat_rwin_load(plat_rwin_t *w, const struct xio_t *io, const char *path,
                   uint64_t floor);

plat_rwin_verdict_t plat_rwin_check(const plat_rwin_t *w, uint64_t seq);

/* Принять номер и сохранить состояние долговечно. 0 / -errno / -EEXIST. */
int plat_rwin_accept(plat_rwin_t *w, const struct xio_t *io, const char *path,
                     uint64_t seq);

/* ── S440: смена личности ────────────────────────────────────────────────── */

typedef struct {
    uint64_t generation;
    char     id[64];
} plat_identity_t;

/* Опубликовать новую личность. Атомарно: либо старая, либо новая. */
int plat_identity_rotate(const struct xio_t *io, const char *base,
                         const plat_identity_t *next);

/* Действующая личность. -ENOENT, если её нет. */
int plat_identity_active(const struct xio_t *io, const char *base,
                         plat_identity_t *out);

/* ── S441: аутентификация не переживает перезапуск ───────────────────────
 *
 * Сессия помнит, что была аутентифицирована. Это знание привязано к ЭПОХЕ
 * ЗАГРУЗКИ и восстановлению не подлежит: после перезапуска ключи сессии в
 * памяти утрачены, состояние сопряжения с той стороной неизвестно, а сама та
 * сторона могла смениться. Поднять сохранённую сессию сразу в AUTHENTICATED
 * значит пустить данные по каналу, которого никто заново не подтверждал.
 *
 * Поэтому загруженная сессия оказывается максимум в NEEDS_AUTH, и передача
 * данных отвергается до нового подтверждения — даже если на диске записано
 * AUTHENTICATED и с момента записи прошла секунда.
 */
typedef enum {
    PLAT_SESS_NEW           = 0,
    PLAT_SESS_NEEDS_AUTH    = 1,
    PLAT_SESS_AUTHENTICATED = 2
} plat_sess_state_t;

typedef struct {
    char              epoch_id[64];
    plat_sess_state_t state;
    uint64_t          peer;
} plat_session_t;

int  plat_session_open(plat_session_t *s, uint64_t peer);
int  plat_session_authenticate(plat_session_t *s);

/* Состояние сессии ПРЯМО СЕЙЧАС, с учётом эпохи. */
plat_sess_state_t plat_session_state(const plat_session_t *s);

/* Можно ли передавать данные. 0 / -EACCES. */
int  plat_session_may_send(const plat_session_t *s);

/* Сохранить и загрузить. Загрузка НИКОГДА не даёт AUTHENTICATED. */
int  plat_session_save(const struct xio_t *io, const char *path,
                       const plat_session_t *s);
int  plat_session_load(const struct xio_t *io, const char *path,
                       plat_session_t *out);
include/platx/resilience.h
/* resilience.h — S436/S444/S445: пределы, размыкатель и приоритет под давлением.
 *
 * S436  Ротация резервных копий ограничена И числом, И объёмом — но последняя
 *       ПРОВЕРЕННАЯ точка восстановления не удаляется никогда, даже если она
 *       одна нарушает оба предела. Предел, ради соблюдения которого стирают
 *       единственную возможность восстановиться, защищает диск от системы, а
 *       не систему.
 *
 * S444  Размыкатель цикла падений. Счётчик попыток переживает перезапуск;
 *       после предела автоматическое восстановление ПРЕКРАЩАЕТСЯ и остаётся
 *       видимое оператору состояние отказа. Бесконечный автоперезапуск — это
 *       не устойчивость, а способ скрыть неустранимую поломку и сжечь диск
 *       журналами.
 *
 * S445  Под нехваткой места первым отказывают отладочные трассы и
 *       необязательная телеметрия, а состояние безопасности и целостность
 *       аудита пишутся до последнего. Порядок отказа — это решение, и
 *       принимать его надо заранее, а не тем, чей write первым вернул ENOSPC.
 */
struct xio_t;

/* ── S436: ротация ───────────────────────────────────────────────────────── */

typedef struct {
    size_t   kept;
    size_t   removed;
    size_t   protected_verified;  /* сохранено вопреки пределам             */
    uint64_t bytes_kept;
} plat_retain_report_t;

/* Проверенной считается копия, рядом с которой лежит <имя>.verified.
 * Ротация удаляет самые старые, пока не уложится в пределы, и НИКОГДА не
 * удаляет последнюю проверенную. 0 / -errno. */
int plat_retain_rotate(const struct xio_t *io, const char *dir,
                       const char *prefix, size_t max_count,
                       uint64_t max_bytes, plat_retain_report_t *out);

/* ── S444: размыкатель ───────────────────────────────────────────────────── */

typedef enum {
    PLAT_BREAKER_CLOSED = 0,  /* попытки разрешены                        */
    PLAT_BREAKER_OPEN   = 1   /* предел исчерпан: автовосстановление стоит */
} plat_breaker_state_t;

/* Отметить начало рискованной попытки. Возвращает состояние ПОСЛЕ отметки:
 * OPEN означает, что пытаться больше нельзя и нужен оператор. */
plat_breaker_state_t plat_breaker_attempt(const struct xio_t *io,
                                          const char *path, uint32_t limit);

/* Отметить успех: счётчик обнуляется. */
int plat_breaker_success(const struct xio_t *io, const char *path);

/* Сколько попыток записано. -errno при ошибке чтения. */
long plat_breaker_count(const struct xio_t *io, const char *path);

/* ── S445: приоритет под давлением ───────────────────────────────────────── */

typedef enum {
    PLAT_CLASS_SECURITY  = 0,  /* состояние безопасности — последним        */
    PLAT_CLASS_AUDIT     = 1,
    PLAT_CLASS_OPERATION = 2,
    PLAT_CLASS_TRACE     = 3,
    PLAT_CLASS_TELEMETRY = 4   /* необязательное — первым                   */
} plat_write_class_t;

/* 1 — писать разрешено, 0 — отказано заранее.
 * pressure: 0 нет давления, 1 мало места, 2 критично. */
int plat_write_allowed(plat_write_class_t cls, int pressure);
include/platx/resource.h
/* platx/resource.h — ownership with instance + generation. */
typedef enum plat_res_type {
    PLAT_RES_FD = 1,
    PLAT_RES_THREAD,
    PLAT_RES_SOCKET,
    PLAT_RES_MEMFD,
    PLAT_RES_TIMER,
    PLAT_RES_EVENT_SUB,
    PLAT_RES_SERVICE,
    PLAT_RES_CHILD
} plat_res_type_t;

typedef struct plat_owner {
    uint32_t module_id;
    uint32_t instance_id;
    uint32_t generation;   /* increments on each recreate; stale cbs die */
} plat_owner_t;

typedef uint64_t plat_res_id_t;

typedef struct plat_resource_api {
    plat_res_id_t (*own)(plat_owner_t owner, plat_res_type_t type,
                         intptr_t handle, const char *name);
    int           (*release)(plat_res_id_t id);
    int           (*release_owner)(plat_owner_t owner); /* sweep; gen 0 no-op */
    int           (*check)(plat_res_id_t id, plat_owner_t expected);
} plat_resource_api_t;

int  plat_res_init(void);
void plat_res_fini(void);
extern const plat_resource_api_t plat_res_api;
include/platx/resource_owner.h
/* platx/resource_owner.h — A1-P04: владение ресурсом с настоящим освобождением.
 *
 * ЗАЧЕМ ЭТОТ ЗАГОЛОВОК СУЩЕСТВУЕТ
 *
 * `platx/resource.h` описывает таблицу владения: кто чем владеет. Ровно это
 * она и делает — и ничего больше. `plat_res_api.release()` обнуляет строку
 * таблицы; дескриптор, поток, memfd или сокет, ради которых строка заводилась,
 * не закрываются никогда. Учёт был честным, освобождение — отсутствовало.
 * Формулировка «ownership без остатков» до этого файла означала «без остатков
 * в таблице», а не «без остатков в системе»; разница и есть весь P04.
 *
 * Здесь у каждого kind появляется releaser: функция, которая действительно
 * закрывает handle, и результат которой сохраняется. Отказавшийся releaser не
 * исчезает и не превращается в успех — слот остаётся в состоянии
 * RELEASE_FAILED и виден в snapshot до конца жизни процесса или до явного
 * повторного вызова. Отчёт о нуле остатков теперь может быть неверным только
 * если врёт сам OS, а не потому, что мы не пытались.
 *
 * ЧТО ЗДЕСЬ НЕ МЕНЯЕТСЯ
 *
 * Опубликованный `plat_resource_api_t` из platx/resource.h остаётся байт в
 * байт тем же: он входит в plat_abi_t, а тот заморожен A1-CONTRACT. Старый
 * API становится тонкой обёрткой над этим ядром и получает освобождение
 * бесплатно. Новые возможности — токены, счётчики, snapshot, deadline —
 * живут только здесь и не сдвигают ни одного offset в ABI.
 *
 * МОДЕЛЬ СОСТОЯНИЙ
 *
 *   FREE      слот свободен.
 *   RESERVED  claim прошёл, publish ещё нет. Ёмкость занята, но снаружи
 *             ресурса не существует: snapshot его не показывает, sweep его
 *             не трогает, releaser по нему не зовётся.
 *   LIVE      опубликован. Единственное состояние, в котором ресурс можно
 *             использовать.
 *   REVOKED   использование запрещено, handle ещё принадлежит нам. Это
 *             логический запрет, он мгновенен и не ждёт медленного close().
 *   DRAINING  releaser выполняется прямо сейчас, вне таблицы и вне её лока.
 *   RELEASE_FAILED  releaser вернул ошибку. Слот НЕ освобождается: строка с
 *             утечкой обязана быть видимой, иначе отчёт «утечек нет» становится
 *             следствием сокрытия, а не проверки.
 *
 * FREE → RESERVED → LIVE → REVOKED → DRAINING → FREE
 *                     └──────────────┘         └→ RELEASE_FAILED
 *
 * Обратных переходов нет. RESERVED уходит в FREE только через abandon.
 *
 * ЛОК И CALLBACK
 *
 * Ни один releaser не вызывается под таблицей. Sweep копирует пачку строк под
 * локом, помечает их DRAINING, отпускает лок, зовёт releaser'ы и возвращается
 * записать исход. Releaser, который сам освобождает другой ресурс, поэтому не
 * может замкнуть цикл на нашем локе — а именно так и выглядит естественный
 * releaser для составного ресурса.
 *
 * SPDX-License-Identifier: GPL-2.0
 */

/* ── Ёмкость (A1-P04-152) ────────────────────────────────────────────────
 *
 * Ёмкость фиксирована на этапе сборки и не растёт в рантайме: таблица
 * статическая, §SEC-3 запрещает ядру ходить в кучу ради владения ресурсом.
 * Значение обязано быть доступно диагностике вместе с digest профиля —
 * «сколько ресурсов помещается» бессмысленно без «в какой политике». Пара
 * отдаётся одним вызовом plat_res_capacity_report(), чтобы никто не сложил
 * ёмкость этой сборки с digest соседней. */
#define PLAT_RES_MAX 512
#error "PLAT_RES_MAX must fit the 16-bit slot index of plat_res_token_t"

#define PLAT_RES_CAPACITY ((size_t)PLAT_RES_MAX)

/* ── Токен (A1-P04-156/157) ──────────────────────────────────────────────
 *
 * plat_res_id_t из старого API — просто монотонный счётчик; чтобы найти
 * ресурс, приходится линейно сканировать таблицу, а чтобы понять, тот ли это
 * ресурс, — сравнивать владельца, которого старый release() даже не
 * спрашивает. Токен решает обе задачи сразу:
 *
 *   биты 0..15    индекс слота          — адресация за O(1)
 *   биты 16..63   поколение слота       — защита от переиспользования
 *
 * Поколение слота увеличивается при КАЖДОМ освобождении. Токен, выданный до
 * освобождения, после него не совпадёт с поколением слота и не сможет
 * закрыть ресурс следующего владельца — сценарий, ради которого карточка 156
 * и существует.
 *
 * 48 бит поколения не переполняются за время жизни процесса при любой
 * достижимой частоте, но «недостижимо» не равно «невозможно»: при исчерпании
 * слот ВЫВОДИТСЯ ИЗ ОБОРОТА (RETIRED) вместо возврата к поколению 1. Пригодная
 * ёмкость при этом уменьшается на единицу, и это наблюдается в
 * plat_res_capacity_report(): retired_slots растёт, usable убывает, а
 * counters.slots_retired считает событие. Заметный и объяснимый исход, в
 * отличие от молчаливого повтора идентичности (A1-P04-157). */
typedef uint64_t plat_res_token_t;

#define PLAT_RES_TOKEN_NONE      ((plat_res_token_t)0)
#define PLAT_RES_TOKEN_IDX_BITS  16
#define PLAT_RES_TOKEN_IDX_MASK  ((plat_res_token_t)0xFFFFu)
/* 2^48 поколений недостижимы прогоном, и из-за этого ветка RETIRED в
 * slot_free() до волны 5 не была проверена ни разу — код существовал, а
 * доказательства не было. Предел вынесен в переопределяемую константу
 * (A1-P04-157): фикстура собирается с маленьким пределом и исполняет ту же
 * производственную ветку, не изменяя ни раскладку токена, ни сам код.
 * Продуктовая сборка предел не переопределяет. */
#define PLAT_RES_GEN_MAX         (((plat_res_token_t)1 << 48) - 1)

unsigned plat_res_token_slot(plat_res_token_t t); /* inline interface */
uint64_t plat_res_token_gen(plat_res_token_t t); /* inline interface */

/* ── Коды результата ─────────────────────────────────────────────────────
 *
 * Отдельный код на каждый отказ, а не общий -1. Причина прикладная: вызывающий
 * на пути остановки обязан различать «таблица полна» и «владелец запечатан» —
 * первое чинится ожиданием, второе не чинится никогда. Старый API возвращал
 * 0 и на «не готово», и на «нет места»; отличить их было нечем. */
typedef enum plat_res_err {
    PLAT_RES_OK          =  0,
    PLAT_RES_E_ARGS      = -1,  /* NULL, generation==0, неизвестный kind      */
    PLAT_RES_E_NOTREADY  = -2,  /* plat_res_owner_init() не выполнялся        */
    PLAT_RES_E_NOSPACE   = -3,  /* нет свободного слота (152/187)             */
    PLAT_RES_E_SEALED    = -4,  /* владелец остановлен, новые claim закрыты   */
    PLAT_RES_E_STALE     = -5,  /* токен не совпал с поколением слота (156)   */
    PLAT_RES_E_NOTFOUND  = -6,  /* слот свободен                              */
    PLAT_RES_E_STATE     = -7,  /* переход запрещён из текущего состояния     */
    PLAT_RES_E_FOREIGN   = -8,  /* слот принадлежит другому владельцу (154)   */
    PLAT_RES_E_EXHAUSTED = -9,  /* поколения слота исчерпаны (157)            */
    PLAT_RES_E_RELEASE   = -10, /* releaser отказал; строка сохранена (163)   */
    PLAT_RES_E_TIMEOUT   = -11, /* дедлайн истёк, остаток не освобождён (164) */
    PLAT_RES_E_RETRY     = -12  /* snapshot не сошёлся, повторить (165)       */
} plat_res_err_t;

const char *plat_res_err_name(plat_res_err_t e);

/* ── Состояния слота ─────────────────────────────────────────────────── */
typedef enum plat_res_state {
    PLAT_RES_ST_FREE           = 0,
    PLAT_RES_ST_RESERVED       = 1,
    PLAT_RES_ST_LIVE           = 2,
    PLAT_RES_ST_REVOKED        = 3,
    PLAT_RES_ST_DRAINING       = 4,
    PLAT_RES_ST_RELEASE_FAILED = 5,
    PLAT_RES_ST_RETIRED        = 6
} plat_res_state_t;

const char *plat_res_state_name(plat_res_state_t s);

/* ── Каталог kind и releaser'ов (A1-P04-151) ─────────────────────────────
 *
 * Каждый kind обязан иметь releaser. Kind без releaser — это ресурс, который
 * учитывается, но не освобождается, то есть ровно тот дефект, который P04
 * закрывает; таблица инициализации отвергает такую строку при сборке.
 *
 * Контракт releaser'а:
 *   • вызывается ВНЕ таблицы и вне её лока;
 *   • получает handle и имя, не получает указателей внутрь таблицы;
 *   • возвращает 0 при освобождении и <0 при отказе; отказ сохраняется;
 *   • обязан быть идемпотентен относительно уже закрытого handle —
 *     повторный вызов на закрытом дескрипторе возвращает 0, а не ошибку
 *     (A1-P04-161: повтор не даёт второго OS close). */
typedef int (*plat_res_releaser_fn)(plat_res_type_t type, intptr_t handle,
                                    const char *name, void *ctx);

typedef struct plat_res_kind_desc {
    plat_res_type_t      type;
    const char          *name;      /* стабильное имя для диагностики   */
    plat_res_releaser_fn releaser;  /* никогда NULL                     */
    int                  is_fd;     /* handle — файловый дескриптор     */
    int                  external;  /* освобождается не ядром, а модулем */
} plat_res_kind_desc_t;

/* Каталог целиком: n строк, по одной на каждый PLAT_RES_* тип. Возвращаемая
 * таблица неизменяема и живёт всё время процесса. */
const plat_res_kind_desc_t *plat_res_kind_table(size_t *n_out);
const plat_res_kind_desc_t *plat_res_kind_lookup(plat_res_type_t type);

/* Переопределить releaser одного kind. Только для fixtures: production-путь
 * пользуется таблицей по умолчанию. Возвращает прежний releaser, чтобы
 * фикстура могла его восстановить, и NULL при неизвестном kind. */
plat_res_releaser_fn plat_res_kind_set_releaser(plat_res_type_t      type,
                                                plat_res_releaser_fn fn,
                                                void                *ctx);

/* ── Счётчики (A1-P04-166) ──────────────────────────────────────────────
 *
 * Инвариант, ради которого счётчики разделены:
 *
 *   reserved + live + revoked + draining + release_failed + retired
 *       + free == PLAT_RES_CAPACITY
 *
 * Одно число «занято» этот инвариант не выражает, и при ошибке releaser'а по
 * нему нельзя понять, чем именно занята ёмкость: незавершённой транзакцией,
 * запрещённым, но не закрытым ресурсом, или строкой с утечкой. */
typedef struct plat_res_counters {
    uint32_t reserved;
    uint32_t live;
    uint32_t revoked;
    uint32_t draining;
    uint32_t release_failed;
    uint32_t retired;
    uint32_t free_slots;
    /* Кумулятивные, не сбрасываются: */
    uint64_t claims_ok;
    uint64_t claims_refused_full;
    uint64_t claims_refused_sealed;
    uint64_t claims_dedup;        /* повтор live-claim, A1-P04-155        */
    uint64_t releases_ok;
    uint64_t releases_failed;
    uint64_t releases_stale;      /* токен не совпал, A1-P04-156          */
    uint64_t sweeps_timed_out;    /* дедлайн, A1-P04-164                  */
    uint64_t slots_retired;       /* исчерпание поколения, A1-P04-157     */
} plat_res_counters_t;

void plat_res_counters_get(plat_res_counters_t *out);

/* Отчёт о ёмкости вместе с digest профиля (A1-P04-152). digest_ok == 0
 * означает, что снимок профиля не опубликован: ёмкость известна, политика —
 * нет, и такой отчёт нельзя выдавать за профиль-привязанный. */
typedef struct plat_res_capacity_report {
    size_t   capacity;
    size_t   in_use;
    uint32_t profile_max_tasks;
    uint32_t profile_max_xio_owners;
    uint8_t  profile_digest[32];
    int      digest_ok;
    char     profile_id[64];
    uint32_t profile_revision;
    /* A1-P04-157 (волна 5). До этой правки заголовок обещал, что при
     * исчерпании поколения «ёмкость уменьшается на единицу», а
     * plat_res_capacity_report() отдавал константу PLAT_RES_CAPACITY: слот
     * выводился из оборота, и в отчёте о ёмкости это не было видно ничем.
     * Фикстура с уменьшенным PLAT_RES_GEN_MAX показала capacity=512->512 при
     * 21 выведенном слоте.
     *
     * capacity остаётся сконфигурированным максимумом — его смысл не меняется,
     * иначе сломались бы читатели, сверяющие его с профилем. Убыль ёмкости
     * наблюдается отдельными полями: usable == capacity - retired_slots. */
    uint32_t retired_slots;   /* слоты, выведенные из оборота навсегда */
    size_t   usable;          /* capacity - retired_slots */
} plat_res_capacity_report_t;

void plat_res_capacity_report(plat_res_capacity_report_t *out);

/* ── Снимок (A1-P04-165) ────────────────────────────────────────────────
 *
 * Наружу отдаётся копия. Указателя внутрь таблицы не существует ни в одном
 * публичном виде: он пережил бы освобождение слота и превратил бы любой
 * диагностический обход в чтение освобождённой строки. */
typedef struct plat_res_entry {
    plat_res_token_t token;
    plat_res_id_t    id;          /* совместимость со старым API      */
    plat_res_type_t  type;
    plat_res_state_t state;
    plat_owner_t     owner;
    intptr_t         handle;
    int              last_release_rc;
    char             name[48];
} plat_res_entry_t;

/* Копирует до cap строк в out. *n_out — сколько строк реально существует
 * (не сколько скопировано), поэтому вызывающий видит нехватку буфера, а не
 * тихо усечённый список. PLAT_RES_E_NOSPACE: cap мал, *n_out — требуемое.
 * owner == NULL: все владельцы. */
plat_res_err_t plat_res_snapshot(const plat_owner_t *owner,
                                 plat_res_entry_t *out, size_t cap,
                                 size_t *n_out);

/* Одна строка по токену. */
plat_res_err_t plat_res_describe(plat_res_token_t t, plat_res_entry_t *out);

/* Мост к старому API: найти токен по plat_res_id_t. Существует только ради
 * platx/resource.h, чья сигнатура release(id) не несёт ни владельца, ни
 * поколения. Новый код пользуется токеном. */
plat_res_token_t plat_res_token_by_id(plat_res_id_t id);

/* ── Жизненный цикл ядра ─────────────────────────────────────────────── */
int  plat_res_owner_init(void);
void plat_res_owner_fini(void);

/* ── Транзакция claim → publish (A1-P04-158) ────────────────────────────
 *
 * Причина двух шагов: слот, заполненный наполовину, не должен быть виден
 * никому. Пока идёт настройка ресурса (dup2, привязка, регистрация в
 * подсистеме), слот занят — ёмкость честно уменьшена — но состояние RESERVED,
 * и его не увидит ни snapshot LIVE-строк, ни sweep. Опубликовать может только
 * тот, у кого токен.
 *
 * abandon откатывает RESERVED и НЕ зовёт releaser: ресурса ещё не было, звать
 * releaser значило бы закрыть handle, которым владеет вызывающий. */
plat_res_err_t plat_res_claim(plat_owner_t owner, plat_res_type_t type,
                              intptr_t handle, const char *name,
                              plat_res_token_t *out);
plat_res_err_t plat_res_publish(plat_res_token_t t);
plat_res_err_t plat_res_abandon(plat_res_token_t t);

/* Одношаговый вариант для ресурсов, готовых к использованию сразу.
 * Ровно claim+publish под одним взятием лока — никакого окна между ними.
 *
 * A1-P04-155: если у ЭТОГО ЖЕ владельца уже есть LIVE-строка с тем же type и
 * handle, возвращается её токен и claims_dedup++. Второй строки, а значит и
 * второго вызова releaser'а на один handle, не появляется. */
plat_res_err_t plat_res_own_live(plat_owner_t owner, plat_res_type_t type,
                                 intptr_t handle, const char *name,
                                 plat_res_token_t *out);

/* ── Разделение запрета и освобождения (A1-P04-162) ──────────────────────
 *
 * revoke мгновенен и не зовёт releaser: он лишь запрещает использование.
 * Ресурс, освобождение которого стоит секунды (сетевой сокет с graceful
 * shutdown, дочерний процесс), не должен эти секунды оставаться разрешённым
 * к использованию только потому, что физически ещё жив. */
plat_res_err_t plat_res_revoke(plat_res_token_t t);
plat_res_err_t plat_res_revoke_owner(plat_owner_t owner, size_t *n_out);

/* Проверка допуска: PLAT_RES_OK только для LIVE-строки этого владельца.
 * REVOKED → PLAT_RES_E_STATE; чужой владелец → PLAT_RES_E_FOREIGN; токен от
 * прошлого поколения слота → PLAT_RES_E_STALE. */
plat_res_err_t plat_res_check_token(plat_res_token_t t, plat_owner_t expected);

/* ── Освобождение ───────────────────────────────────────────────────────
 *
 * A1-P04-161: повторный вызов на уже освобождённом токене возвращает
 * PLAT_RES_E_STALE и НЕ зовёт releaser второй раз. Это объявленный результат,
 * а не ошибка вызывающего: на пути остановки два независимых пути вполне
 * законно доходят до одного ресурса. */
plat_res_err_t plat_res_release_token(plat_res_token_t t);

/* ── Дедлайн (A1-P04-164) ────────────────────────────────────────────────
 *
 * Один АБСОЛЮТНЫЙ момент на всю уборку владельца, а не таймаут на ресурс.
 * Иначе общий бюджет остановки молча умножается на число ресурсов: 200 мс на
 * ресурс при сорока ресурсах — это восемь секунд, которых никто не назначал.
 *
 * budget_ms из plat_res_deadline_in() отсчитывается по CLOCK_MONOTONIC:
 * перевод системных часов не продлевает и не сокращает срок. */
typedef struct plat_res_deadline {
    uint64_t at_mono_ms;   /* 0 = без ограничения */
} plat_res_deadline_t;

plat_res_deadline_t plat_res_deadline_in(int budget_ms);
uint64_t            plat_res_mono_ms(void);
int                 plat_res_deadline_remaining_ms(plat_res_deadline_t d);

/* ── Барьер остановки (A1-P04-159) ──────────────────────────────────────
 *
 * Между «владелец останавливается» и «владелец убран» существует окно, в
 * котором его же поток может успеть зарегистрировать ресурс. Без барьера
 * этот ресурс не попадает ни в один drain и остаётся навсегда.
 *
 * seal закрывает окно: после него claim от этого владельца — PLAT_RES_E_SEALED,
 * а всё, что уже зарегистрировано, гарантированно входит в sweep. Третьего
 * исхода нет, и это и есть приёмка карточки. */
plat_res_err_t plat_res_owner_seal(plat_owner_t owner);
int            plat_res_owner_sealed(plat_owner_t owner);
plat_res_err_t plat_res_owner_unseal(plat_owner_t owner);   /* fixtures */

/* Итог уборки владельца. released + failed + left == найдено на входе. */
typedef struct plat_res_sweep_result {
    size_t         found;
    size_t         released;
    size_t         failed;      /* releaser отказал; строки сохранены */
    size_t         left;        /* дедлайн истёк, не пытались         */
    plat_res_err_t worst;
} plat_res_sweep_result_t;

/* Отзывает и освобождает все ресурсы владельца в пределах ОДНОГО дедлайна.
 * Releaser'ы вызываются вне лока таблицы (A1-P04-160).
 *
 * PLAT_RES_E_TIMEOUT: дедлайн истёк раньше, чем закончились ресурсы. Строки,
 * до которых не дошли, остаются LIVE/REVOKED и видны в snapshot — успех над
 * неубранной работой не возвращается ни при каких условиях. */
plat_res_err_t plat_res_sweep_owner(plat_owner_t owner,
                                    plat_res_deadline_t deadline,
                                    plat_res_sweep_result_t *out);

/* Повторная попытка по строкам RELEASE_FAILED (A1-P04-163). Ничего не
 * скрывает: строка уходит из таблицы только при успехе releaser'а. */
plat_res_err_t plat_res_retry_failed(plat_owner_t owner,
                                     plat_res_deadline_t deadline,
                                     plat_res_sweep_result_t *out);

/* Число строк владельца в состояниях, которые считаются остатком:
 * LIVE, REVOKED, DRAINING, RELEASE_FAILED. RESERVED не считается — это
 * незавершённая транзакция вызывающего, а не оставленный ресурс. */
size_t plat_res_leftover_owner(plat_owner_t owner);
include/platx/safe.h
/* platx/safe.h — Wave 7: checked arithmetic, wire, handles, time, path.
 *
 * These helpers exist so production code does not invent a local "it'll fit"
 * conversion at every boundary. Unknown, overflow, and empty are refusals,
 * not silent truncation. Not a second allocator stack and not a new registry.
 */

enum {
    PLAT_SAFE_OK            = 0,
    PLAT_SAFE_OVERFLOW      = -1,
    PLAT_SAFE_RANGE         = -2,
    PLAT_SAFE_ARGS          = -3,
    PLAT_SAFE_PATH_TOO_LONG = -4,
    PLAT_SAFE_EMPTY         = -5,
    PLAT_SAFE_MALFORMED     = -6,
    PLAT_SAFE_UNSET         = -7,
    PLAT_SAFE_HOSTILE       = -8,
    /* B-018: отказ аллокатора — не переполнение. Раньше plat_mem_alloc()
     * возвращал PLAT_SAFE_OVERFLOW, хотя никакого переполнения не было. */
    PLAT_SAFE_NOMEM         = -9,
    /* B-022: ресурс исчерпан — не ошибка аргументов. plat_guard_push()
     * возвращал PLAT_SAFE_ARGS и на !g/!fn, и на полный guard; у вызывающего
     * это разные ситуации с разным лечением. */
    PLAT_SAFE_FULL          = -10
};

/* Stable API class (S330). Never return a raw errno from a public helper. */
typedef enum {
    PLAT_ERR_OK        = 0,
    PLAT_ERR_ARGS      = 1,
    PLAT_ERR_RANGE     = 2,
    PLAT_ERR_NOMEM     = 3,
    PLAT_ERR_IO        = 4,
    PLAT_ERR_PERM      = 5,
    PLAT_ERR_AGAIN     = 6,
    PLAT_ERR_UNSUPPORTED = 7,
    PLAT_ERR_INTERNAL  = 8
} plat_err_class_t;

#define PLAT_FD_NONE (-1)

/* ── S311 / S319 narrowing ───────────────────────────────────────────── */

int plat_narrow_size_to_int(size_t v, int *out);
int plat_narrow_size_to_u32(size_t v, uint32_t *out);
int plat_narrow_u64_to_u32(uint64_t v, uint32_t *out);
int plat_narrow_i64_to_i32(int64_t v, int32_t *out);
int plat_narrow_off_to_size(int64_t v, size_t *out);
int plat_fd_from_int(int raw, int *out);          /* 0 is a valid fd */

/* ── S316 shifts / masks ─────────────────────────────────────────────── */

int plat_shl_u32(uint32_t v, unsigned bits, uint32_t *out);
int plat_shl_u64(uint64_t v, unsigned bits, uint64_t *out);
int plat_shr_u32(uint32_t v, unsigned bits, uint32_t *out);
uint32_t plat_mask_u32(uint32_t v, unsigned width); /* width 0 or >32 → 0 */

/* ── S317 / S318 overflow ────────────────────────────────────────────── */

int plat_add_size(size_t a, size_t b, size_t *out);
int plat_mul_size(size_t a, size_t b, size_t *out);
int plat_add_u32(uint32_t a, uint32_t b, uint32_t *out);
int plat_mul_u32(uint32_t a, uint32_t b, uint32_t *out);

/* ── S320 / S321 / S323 endian + unaligned, no packed deref ──────────── */

uint16_t plat_load_le16(const void *p);
uint32_t plat_load_le32(const void *p);
uint64_t plat_load_le64(const void *p);
uint16_t plat_load_be16(const void *p);
uint32_t plat_load_be32(const void *p);
uint64_t plat_load_be64(const void *p);
void plat_store_le16(void *p, uint16_t v);
void plat_store_le32(void *p, uint32_t v);
void plat_store_le64(void *p, uint64_t v);
void plat_store_be16(void *p, uint16_t v);
void plat_store_be32(void *p, uint32_t v);
void plat_store_be64(void *p, uint64_t v);

/* ── S312 / S341 buffers and paths ───────────────────────────────────── */

int plat_str_copy(char *dst, size_t cap, const char *src); /* NUL or refuse */
int plat_path_join(char *dst, size_t cap, const char *a, const char *b);

/* ── S342 environment ────────────────────────────────────────────────── */

int plat_env_u64(const char *name, uint64_t *out); /* empty/malformed/overflow */

/* ── S343 monotonic time ─────────────────────────────────────────────── */

int plat_mono_now_ns(uint64_t *out);
int plat_mono_deadline_ns(uint64_t timeout_ns, uint64_t *out);
int plat_mono_expired(uint64_t deadline_ns);

/* ── S327 / S328 ownership sentinels ─────────────────────────────────── */

int  plat_fd_valid(int fd);                 /* fd 0 is valid; -1 is not */
int  plat_fd_close(int *fd);                /* idempotent; never closes -1 as 0 */
void plat_ptr_move(void **dst, void **src); /* src becomes NULL */
#define PLAT_ASSERT_MOVED(p) do { if ((p) != NULL) __builtin_trap(); } while (0)
#define PLAT_ASSERT_MOVED(p) ((void)(p))

/* ── S329 / S330 errno and API class ─────────────────────────────────── */

int plat_errno_save(void);
void plat_errno_restore(int saved);
plat_err_class_t plat_err_from_errno(int e);
const char *plat_err_class_name(plat_err_class_t c);

/* ── S331 / S332 / S333 allocation ───────────────────────────────────── */

enum { PLAT_ALLOC_HEAP = 1, PLAT_ALLOC_WIRE = 2 };

typedef struct plat_mem {
    void    *p;
    size_t   n;
    uint32_t family;
} plat_mem_t;

int plat_mem_alloc(plat_mem_t *m, size_t n, uint32_t family);
int plat_mem_realloc(plat_mem_t *m, size_t n);   /* never `p = realloc(p)` */
void plat_mem_free(plat_mem_t *m);
int plat_mem_free_checked(plat_mem_t *m, uint32_t family); /* cross-family → fail */

/* Test hook (S334). NULL restores libc. Hook may return NULL to inject OOM. */
typedef void *(*plat_alloc_fn)(size_t n);
typedef void *(*plat_realloc_fn)(void *p, size_t n);
typedef void  (*plat_free_fn)(void *p);
void plat_alloc_install(plat_alloc_fn a, plat_realloc_fn r, plat_free_fn f);

/* ── S324 flexible / trailing buffer ─────────────────────────────────── */

void *plat_flex_alloc(size_t hdr, size_t count, size_t elem, size_t *out_total);
void plat_flex_free(void *p);

/* ── S325 copy out of transient storage ──────────────────────────────── */

char *plat_dup_bounded(const char *s, size_t max);

/* ── S335 reverse-order cleanup ──────────────────────────────────────── */

typedef void (*plat_dtor_fn)(void *obj);
typedef struct plat_guard {
    plat_dtor_fn fn[8];
    void        *obj[8];
    unsigned     n;
} plat_guard_t;

void plat_guard_init(plat_guard_t *g);
int  plat_guard_push(plat_guard_t *g, plat_dtor_fn fn, void *obj);
void plat_guard_disarm(plat_guard_t *g);
void plat_guard_run(plat_guard_t *g); /* reverse order; idempotent */

/* ── S336 signal: handler may only note ──────────────────────────────── */

void plat_signal_note(int sig);          /* async-signal-safe */
int  plat_signal_take(void);             /* coordinator thread */
int  plat_signal_pending(void);

/* ── S337 generation / flags memory order ────────────────────────────── */

void     plat_gen_store(uint32_t *slot, uint32_t v); /* release */
uint32_t plat_gen_load(const uint32_t *slot);        /* acquire */
void     plat_flag_set(uint32_t *slot);
int      plat_flag_seen(const uint32_t *slot);

/* ── S338 / S339 typed callback ──────────────────────────────────────── */

typedef int (*plat_cb_fn)(void *ctx, uint32_t abi_minor, const void *msg);

typedef struct plat_cb_adapter {
    uint32_t   abi_minor;
    plat_cb_fn fn;
    void      *ctx;
} plat_cb_adapter_t;

int plat_cb_invoke(const plat_cb_adapter_t *a, uint32_t expect_minor,
                   const void *msg);
#define PLAT_CHECK_CB(fn) \
    ((void)sizeof(_Generic((fn), plat_cb_fn: (fn), default: (fn))))
#define PLAT_CHECK_CB(fn) ((void)(fn))

/* ── S313 / S314 enum validation ─────────────────────────────────────── */

int plat_enum_u32_ok(uint32_t v, uint32_t lo, uint32_t hi_inclusive);

/* ── S326 zero-before-use ────────────────────────────────────────────── */

void plat_zero(void *p, size_t n);

/* ── S340 / S344 small typed inlines instead of unsafe macros ────────── */

size_t plat_min_size(size_t a, size_t b); /* inline interface */
size_t plat_max_size(size_t a, size_t b); /* inline interface */
int plat_safe_snprintf(char *dst, size_t cap, const char *fmt, ...)
    __attribute__((format(printf, 3, 4)));
int plat_safe_snprintf(char *dst, size_t cap, const char *fmt, ...);
include/platx/services/echo_v1.h
/* platx/services/echo_v1.h — tiny loopback capability for CHILD IPC.
 * Same vtable both ways: worker-local test.echo and child-provided
 * test.childcap. Not a domain product API.
 */
#define PLAT_CAP_ECHO      PLAT_NAME_TEST_ECHO
#define PLAT_CAP_CHILDCAP  PLAT_NAME_TEST_CHILDCAP
#define PLAT_ECHO_V1       0x00010001u

#define PLAT_ECHO_M_ECHO   1u

typedef struct plat_echo_v1 {
    uint32_t struct_size;
    int (*echo)(const void *in, size_t in_len,
                void *out, size_t out_cap, size_t *out_len);
} plat_echo_v1_t;
include/platx/shutdown_quiesce.h
/* platx/shutdown_quiesce.h — Coordinated shutdown quiesce order (INT-109). */
#define PLAT_QUIESCE_TIMEOUT_MS_DEFAULT  5000
#define PLAT_QUIESCE_REASON              128

typedef enum {
    PLAT_QUIESCE_OK      = 0,
    PLAT_QUIESCE_TIMEOUT = 1,
    PLAT_QUIESCE_ERROR   = 2,
} plat_quiesce_status_t;

typedef struct {
    plat_quiesce_status_t status;
    int   steps_completed;
    char  reason[PLAT_QUIESCE_REASON];
} plat_quiesce_result_t;

/* Execute ordered quiesce: Flow → MBus → Transport → FSX → FUSE → VFS.
 * timeout_ms: per-step budget (0 = default).  0/-1. */
int plat_shutdown_quiesce(int timeout_ms, plat_quiesce_result_t *out);
include/platx/span.h
/* span.h — S418/S420: неполное помечено неполным, родство не выдумано.
 *
 * S418  После обрыва экспорта восстанавливаются ЛИБО целые отрезки, ЛИБО
 *       явно помеченные неполными. Незакрытый отрезок не достраивается
 *       временем обрыва: у него нет конца, и приписать ему конец значит
 *       сочинить длительность, по которой потом будут делать выводы.
 *
 *       И отдельно: отрезок НЕ ПРИСОЕДИНЯЕТСЯ к родителю из другой генерации.
 *       Идентификаторы переиспользуются, генерация растёт при каждом
 *       пересоздании владельца; сшить их по одному лишь совпадению номера
 *       значит построить дерево вызовов, которого не было.
 *
 * S420  Состояние, экспорт которого оборвался, читается как UNKNOWN.
 *       Честное «не знаю» дешевле придуманной непрерывности: по второму
 *       принимают решения, по первому идут смотреть.
 */
#define PLAT_SPAN_MAX 256

typedef enum {
    PLAT_SPAN_INCOMPLETE = 0,  /* начат, конец не записан           */
    PLAT_SPAN_COMPLETE   = 1
} plat_span_state_t;

typedef struct {
    uint64_t          id;
    uint64_t          generation;
    uint64_t          parent;        /* 0 — корень                    */
    uint64_t          parent_gen;
    uint64_t          begin_ns;
    uint64_t          end_ns;        /* значим только при COMPLETE    */
    plat_span_state_t state;
    int               orphan;        /* родитель не найден в этой генерации */
} plat_span_t;

typedef struct {
    plat_span_t s[PLAT_SPAN_MAX];
    size_t      n;
    size_t      incomplete;
    size_t      cross_gen_refused;   /* сшивок, которых не сделали     */
    size_t      overflow;
} plat_span_view_t;

struct xio_t;

int plat_span_begin(const struct xio_t *io, const char *log, uint64_t id,
                    uint64_t generation, uint64_t parent, uint64_t parent_gen,
                    uint64_t ns);
int plat_span_end  (const struct xio_t *io, const char *log, uint64_t id,
                    uint64_t generation, uint64_t ns);

int plat_span_replay(const struct xio_t *io, const char *log,
                     plat_span_view_t *out);

const plat_span_t *plat_span_find(const plat_span_view_t *v, uint64_t id,
                                  uint64_t generation);
include/platx/sqlrage.h
/* sqlrage.h — offensive.sqlrage:v1
 *
 * ARENA / LAB OFFENSIVE PROFILE ONLY.
 * Этот заголовок запрещён в defensive shipped profile (C19/C20).
 * CI-тест include-guard проверяет отсутствие в defensive manifest.
 *
 * SQLRage — Red-side SQL injection fixture для PLATX ARENA.
 * Архитектура: Grammar → Bandit(select/mutate) → XIO(request) →
 *              Evaluator(score) → Extractor(harvest) → Finding(watermark)
 *
 * Все сетевые операции идут через xio:v1.
 * Все findings несут (world_gen, watermark) текущей Arena-сессии.
 * External target вне Arena CIDR → SQLRAGE_ERR_TARGET_OUTSIDE_ARENA.
 */

/* ── версия ABI ─────────────────────────────────────────────────────────── */
#define SQLRAGE_ABI_VERSION  1
#define SQLRAGE_SCHEMA_VER   1

/* ── SQL диалекты ───────────────────────────────────────────────────────── */
typedef enum {
    SQLRAGE_DIALECT_MYSQL  = 0,
    SQLRAGE_DIALECT_MSSQL  = 1,
    SQLRAGE_DIALECT_PGSQL  = 2,
    SQLRAGE_DIALECT_ORACLE = 3,
    SQLRAGE_DIALECT_AUTO   = 255   /* определить fingerprint-ом */
} sqlrage_dialect_t;

/* ── техника инъекции ───────────────────────────────────────────────────── */
typedef enum {
    SQLRAGE_TECH_NONE          = 0,
    SQLRAGE_TECH_BOOLEAN_BLIND = 1,
    SQLRAGE_TECH_ERROR         = 2,
    SQLRAGE_TECH_UNION         = 3,
    SQLRAGE_TECH_TIME_BLIND    = 4,
    SQLRAGE_TECH_STACKED       = 5,
    SQLRAGE_TECH_SECOND_ORDER  = 6,
    SQLRAGE_TECH_OOB_DNS       = 7,
} sqlrage_technique_t;

/* ── уверенность ────────────────────────────────────────────────────────── */
typedef enum {
    SQLRAGE_CONF_NONE   = 0,  /* не публиковать finding              */
    SQLRAGE_CONF_LOW    = 1,  /* score < 0.5; подозрение             */
    SQLRAGE_CONF_MEDIUM = 2,  /* score ≥ 0.5; вероятна              */
    SQLRAGE_CONF_HIGH   = 3,  /* score ≥ 0.9; подтверждена           */
} sqlrage_confidence_t;

/* ── результат операции ─────────────────────────────────────────────────── */
typedef enum {
    SQLRAGE_RESULT_CONFIRMED   = 0,  /* injection найдена и подтверждена  */
    SQLRAGE_RESULT_PARTIAL     = 1,  /* probe завершена частично           */
    SQLRAGE_RESULT_CLEAN       = 2,  /* injection не обнаружена            */
    SQLRAGE_RESULT_UNAVAILABLE = 3,  /* target недостижим / cap отозвана   */
    SQLRAGE_RESULT_STALE       = 4,  /* world_gen устарел                  */

    SQLRAGE_ERR_BAD_ARG             = -1,
    SQLRAGE_ERR_TARGET_OUTSIDE_ARENA= -2,  /* external IP вне Arena CIDR  */
    SQLRAGE_ERR_XIO_FAIL            = -3,
    SQLRAGE_ERR_NO_PROVIDER         = -4,
    SQLRAGE_ERR_GENERATION_MISMATCH = -5,
    SQLRAGE_ERR_CORPUS_IO           = -6,
    SQLRAGE_ERR_BUDGET_EXCEEDED     = -7,
} sqlrage_result_t;

/* ── WAF вендор (fingerprint) ────────────────────────────────────────────── */
typedef enum {
    SQLRAGE_WAF_UNKNOWN     = 0,
    SQLRAGE_WAF_NONE        = 1,
    SQLRAGE_WAF_CLOUDFLARE  = 2,
    SQLRAGE_WAF_MODSECURITY = 3,
    SQLRAGE_WAF_AWS_WAF     = 4,
    SQLRAGE_WAF_F5_ASM      = 5,
    SQLRAGE_WAF_BARRACUDA   = 6,
    SQLRAGE_WAF_SUCURI      = 7,
} sqlrage_waf_t;

/* ── bandit-алгоритм ────────────────────────────────────────────────────── */
typedef enum {
    SQLRAGE_BANDIT_UCB1            = 0,
    SQLRAGE_BANDIT_THOMPSON        = 1,
    SQLRAGE_BANDIT_EPSILON_GREEDY  = 2,
} sqlrage_bandit_t;

/* ── finding: одна подтверждённая injection-точка ───────────────────────── */
#define SQLRAGE_PARAM_MAX    64
#define SQLRAGE_PAYLOAD_MAX  512
#define SQLRAGE_STRATEGY_MAX 32
#define SQLRAGE_URL_MAX      1024
#define SQLRAGE_WATERMARK_LEN 16

typedef struct {
    uint32_t            schema_ver;          /* = SQLRAGE_SCHEMA_VER        */
    uint64_t            world_gen;           /* Arena world generation       */
    uint8_t             watermark[SQLRAGE_WATERMARK_LEN]; /* Mirage provenance */

    uint64_t            probe_ts_ms;         /* время обнаружения (monotonic)*/
    char                target_url[SQLRAGE_URL_MAX];
    char                param[SQLRAGE_PARAM_MAX];
    sqlrage_dialect_t   dialect;
    sqlrage_technique_t technique;
    sqlrage_confidence_t confidence;
    float               score;               /* 0.0 .. 1.0                  */

    char                tamper_strategy[SQLRAGE_STRATEGY_MAX];
    uint32_t            rounds_used;
    char                payload[SQLRAGE_PAYLOAD_MAX];

    sqlrage_waf_t       waf_detected;
    uint32_t            requests_sent;
    uint32_t            bytes_sent;

    int                 extraction_done;     /* 1 если Extract-фаза прошла  */
} sqlrage_finding_t;

/* ── extracted data record ──────────────────────────────────────────────── */
#define SQLRAGE_FIELD_MAX  64
#define SQLRAGE_VALUE_MAX  256

typedef struct {
    uint64_t  world_gen;
    uint8_t   watermark[SQLRAGE_WATERMARK_LEN];

    char      db_name[SQLRAGE_FIELD_MAX];
    char      table_name[SQLRAGE_FIELD_MAX];
    char      column_name[SQLRAGE_FIELD_MAX];
    uint32_t  row_index;
    char      value[SQLRAGE_VALUE_MAX];      /* hex-encoded если binary      */
    int       is_hex;

    sqlrage_technique_t via_technique;
    uint32_t  requests_cost;                 /* запросов потрачено на запись */
} sqlrage_record_t;

/* ── corpus: persistence состояния бандита ──────────────────────────────── */
typedef struct sqlrage_corpus sqlrage_corpus_t;  /* opaque */

/* ── конфигурация probe-сессии ──────────────────────────────────────────── */
typedef struct {
    const char         *target_url;          /* arena:// или http://arena.* */
    const char * const *params;              /* NULL-terminated список       */
    int                 params_n;
    sqlrage_dialect_t   dialect;             /* AUTO → fingerprint           */

    sqlrage_bandit_t    bandit;
    uint32_t            max_rounds;          /* default: 60                  */
    uint32_t            max_requests;        /* hard budget; default: 500    */

    /* техники: битовая маска SQLRAGE_TECH_* */
    uint32_t            techniques_mask;     /* 0 = все                      */

    /* tamper */
    float               epsilon;             /* для ε-greedy; default: 0.15  */

    /* timing */
    uint32_t            time_sec;            /* sleep длительность; default 5*/
    uint32_t            timing_samples;      /* калибровка; default: 6       */
    float               sigma_k;            /* порог; default: 3.0          */

    /* extraction */
    int                 do_extract;          /* 0 = probe only               */
    uint32_t            max_rows;            /* 0 = без лимита               */

    /* OOB DNS */
    const char         *oob_domain;         /* NULL = отключено             */

    /* markers */
    const char         *true_marker_re;     /* NULL = auto                  */
    const char         *false_marker_re;
    const char         *union_marker;

    /* HTTP */
    int                 use_post;
    const char         *cookie;
    const char * const *extra_headers;      /* NULL-terminated              */
    uint32_t            timeout_ms;         /* default: 15000               */
    uint32_t            delay_ms;           /* между запросами              */

    /* PLATX context */
    uint64_t            world_gen;          /* текущая Arena world generation*/
    const uint8_t      *operator_scope;    /* signed scope из Arena session  */
    uint32_t            operator_scope_len;

    /* corpus */
    sqlrage_corpus_t   *corpus;             /* NULL = не персистить          */

    int                 verbose;
} sqlrage_config_t;

/* ── session handle ─────────────────────────────────────────────────────── */
typedef struct sqlrage_session sqlrage_session_t; /* opaque */

/* ═══════════════════════════════════════════════════════════════════════════
 * API
 * ═══════════════════════════════════════════════════════════════════════════ */

/* Создать probe-сессию. Валидирует target против Arena CIDR.
 * Возвращает NULL + errno при ошибке. */
sqlrage_session_t *sqlrage_session_create(const sqlrage_config_t *cfg);

/* Запустить probe. Блокирующий вызов (должен жить в CHILD task).
 * findings и findings_n — out-параметры; память принадлежит сессии.
 * Возвращает sqlrage_result_t. */
sqlrage_result_t sqlrage_probe(sqlrage_session_t    *s,
                               sqlrage_finding_t   **findings,
                               uint32_t             *findings_n);

/* Extract-фаза для конкретного finding.
 * records, records_n — out; память принадлежит сессии. */
sqlrage_result_t sqlrage_extract(sqlrage_session_t    *s,
                                 const sqlrage_finding_t *finding,
                                 sqlrage_record_t    **records,
                                 uint32_t             *records_n);

/* Опросить WAF до probe. Можно вызвать отдельно. */
sqlrage_result_t sqlrage_fingerprint_waf(sqlrage_session_t *s,
                                         sqlrage_waf_t     *out_waf);

/* Прервать текущую probe/extract (thread-safe, async-signal-safe). */
void sqlrage_cancel(sqlrage_session_t *s);

/* Освободить сессию. Вызов после sqlrage_cancel() безопасен. */
void sqlrage_session_destroy(sqlrage_session_t *s);

/* ── corpus ─────────────────────────────────────────────────────────────── */
sqlrage_corpus_t *sqlrage_corpus_open(const char *path);
int               sqlrage_corpus_load(sqlrage_corpus_t *c);
int               sqlrage_corpus_save(sqlrage_corpus_t *c);
int               sqlrage_corpus_discard(sqlrage_corpus_t *c); /* atomic rm */
void              sqlrage_corpus_close(sqlrage_corpus_t *c);

/* ── provider info ──────────────────────────────────────────────────────── */
const char *sqlrage_provider_version(void); /* build hash + date            */
int         sqlrage_abi_version(void);      /* = SQLRAGE_ABI_VERSION         */

/* ── helpers ────────────────────────────────────────────────────────────── */
const char *sqlrage_result_str(sqlrage_result_t r);
const char *sqlrage_technique_str(sqlrage_technique_t t);
const char *sqlrage_confidence_str(sqlrage_confidence_t c);
const char *sqlrage_waf_str(sqlrage_waf_t w);
include/platx/startup_barrier.h
/* platx/startup_barrier.h — A1-P02: trusted startup readiness barrier.
 *
 * Nothing outside a very short bootstrap allowlist is allowed to touch the
 * world until the profile has been read, authenticated, frozen and every
 * mandatory provider has declared itself usable. That moment is the
 * barrier. Before it there are no external effects; after it the policy is
 * immutable until the next boot.
 *
 * The phase ladder only ever moves forward, and only one step at a time:
 *
 *   BOOTSTRAP ─► PROFILE_READ ─► PROFILE_VERIFY ─► POLICY_FROZEN
 *                                      │                │
 *                                      ▼                ▼
 *                                   FAILED  ◄──── PROVIDERS_READY ──► READY
 *
 * FAILED is reachable from every phase and is terminal for this attempt.
 * plat_startup_reset() starts a new attempt and is the only way back
 * (P02-090); it must return every counter to its baseline.
 *
 * Cards: 081 effect gating, 082 audit handshake, 083 watchdog readiness,
 *        084 protection readiness, 085 no partial capability publication,
 *        086 verification receipt bound to bytes + generation,
 *        087 immutable bytes token for loader handoff, 088 bootstrap
 *        allowlist, 089 cancellation, 090 idempotent repeated failure.
 */

#define PLAT_STARTUP_NAME_MAX     32
#define PLAT_STARTUP_PROV_MAX     16
#define PLAT_STARTUP_CAP_MAX      32
#define PLAT_STARTUP_DIGEST_LEN   32

/* ── phases ──────────────────────────────────────────────────────────── */
typedef enum {
    PLAT_SU_BOOTSTRAP      = 0,
    PLAT_SU_PROFILE_READ   = 1,
    PLAT_SU_PROFILE_VERIFY = 2,
    PLAT_SU_POLICY_FROZEN  = 3,
    PLAT_SU_PROVIDERS_READY= 4,
    PLAT_SU_READY          = 5,
    PLAT_SU_FAILED         = 6,
} plat_startup_phase_t;

/* ── effect classes (P02-081/088) ────────────────────────────────────── */
/* The bootstrap allowlist is deliberately tiny. Everything else is an
 * external effect and is refused until the barrier is accepted. */
typedef enum {
    PLAT_EFFECT_READ_PROFILE = 0,  /* allowed in BOOTSTRAP: read the blob   */
    PLAT_EFFECT_READ_ANCHOR  = 1,  /* allowed in BOOTSTRAP: identity anchor */
    PLAT_EFFECT_CLOCK        = 2,  /* allowed in BOOTSTRAP: monotonic time  */
    PLAT_EFFECT_NETWORK      = 3,  /* refused before READY                  */
    PLAT_EFFECT_FS_WRITE     = 4,  /* refused before READY                  */
    PLAT_EFFECT_EXEC         = 5,  /* refused before READY                  */
    PLAT_EFFECT_AUDIT_EMIT   = 6,  /* refused before PROVIDERS_READY        */
    PLAT_EFFECT_MODULE_INIT  = 7,  /* refused before POLICY_FROZEN          */
    PLAT_EFFECT__COUNT       = 8,
} plat_effect_class_t;

/* ── provider readiness (P02-082/083/084) ────────────────────────────── */
/* The contract distinguishes three states, because "initialised" and
 * "usable" are not the same claim and a consumer that conflates them will
 * publish READY over a sink that cannot take a record. */
typedef enum {
    PLAT_PROV_UNKNOWN     = 0,  /* never declared                          */
    PLAT_PROV_INITIALIZED = 1,  /* constructed, not yet proven usable      */
    PLAT_PROV_USABLE      = 2,  /* proven usable: a probe round-tripped    */
    PLAT_PROV_FAILED      = 3,  /* declared failure; no silent retry       */
} plat_prov_state_t;

/* ── module bytes token (P02-086/087) ────────────────────────────────── */
/* A pathname is not an identity: the file behind it can be replaced
 * between verify and use. Consumers receive this token instead, and a
 * receipt is only valid for the exact (len, digest, boot_gen) it names. */
typedef struct plat_module_token {
    uint8_t  digest[PLAT_STARTUP_DIGEST_LEN];
    size_t   len;
    uint64_t boot_gen;
    int      valid;
} plat_module_token_t;

/* ── lifecycle ───────────────────────────────────────────────────────── */

/* Begin a startup attempt at BOOTSTRAP for the given boot generation.
 * Clears all per-attempt counters. Returns 0, or -1 if an attempt is
 * already in flight and has not failed or been cancelled. */
int plat_startup_begin(uint64_t boot_gen);

plat_startup_phase_t plat_startup_phase(void);
uint64_t             plat_startup_boot_gen(void);

/* Advance exactly one phase. Any other transition is refused with -1 and
 * leaves the phase untouched. */
int plat_startup_advance(plat_startup_phase_t to);

/* P02-085: fail the attempt. Every capability published during this
 * attempt is retracted before this returns, so no capability of a module
 * that was never admitted stays visible. */
int plat_startup_fail(const char *reason);

/* P02-089: cancellation during profile read or verify. Temporary
 * resources are released and READY is never published. Distinguished from
 * failure so a caller can tell "we stopped" from "we refused". */
int plat_startup_cancel(const char *reason);

/* P02-090: start over. Every counter this module owns returns to its
 * baseline, so N consecutive failed attempts leave the same footprint as
 * one. */
void plat_startup_reset(void);

const char *plat_startup_reason(void);

/* ── effect gating (P02-081/088) ─────────────────────────────────────── */

/* Returns 1 if the effect is permitted in the current phase, else 0.
 * Every call is counted, permitted or not, so a fixture can assert that
 * zero effects were attempted before the barrier. */
int plat_startup_effect_allowed(plat_effect_class_t e);

/* Counters for fixtures. attempted counts every call to
 * effect_allowed(); permitted counts the calls that returned 1. */
uint32_t plat_startup_effect_attempted(void);
uint32_t plat_startup_effect_permitted(void);
uint32_t plat_startup_effect_refused(void);

/* ── provider handshake (P02-082/083/084) ────────────────────────────── */

/* Declare a provider mandatory for this profile. Mandatory providers must
 * reach PLAT_PROV_USABLE before PROVIDERS_READY is accepted. */
int plat_startup_provider_require(const char *name);

/* Declare the provider's current state. A provider may move
 * UNKNOWN → INITIALIZED → USABLE, or to FAILED from anywhere. Moving
 * backwards out of FAILED is refused: a failed mandatory provider does
 * not silently recover inside one attempt. */
int plat_startup_provider_declare(const char *name, plat_prov_state_t st);

plat_prov_state_t plat_startup_provider_state(const char *name);

/* Returns 1 if every mandatory provider is USABLE. Populates *missing
 * with the first provider that is not, when non-NULL. */
int plat_startup_providers_ready(const char **missing);

/* ── capability publication (P02-085) ────────────────────────────────── */

/* Publish a capability for this attempt. Publications are tracked so
 * plat_startup_fail() can retract all of them atomically. */
int plat_startup_publish_cap(const char *name);
int plat_startup_cap_visible(const char *name);
uint32_t plat_startup_cap_count(void);

/* ── verification receipts (P02-086/087) ─────────────────────────────── */

/* Non-cryptographic content digest, for change detection ONLY.
 * This is NOT an integrity proof and must never be presented as one
 * (P02-099). It detects a module blob that changed between verify and
 * use; it does not detect an adversary who can compute a preimage. The
 * cryptographic verifier is a separate, linked provider. */
void plat_startup_content_digest(const void *bytes, size_t len,
                                 uint8_t out[PLAT_STARTUP_DIGEST_LEN]);

/* Mint a token for a module blob. The token names the bytes, not the
 * path, and carries the boot generation it was minted in (P02-087). */
plat_module_token_t plat_startup_token_mint(const void *bytes, size_t len);

/* Record that the verifier admitted these exact bytes in this generation. */
int plat_startup_receipt_record(const plat_module_token_t *tok);

/* P02-086: is the recorded admission still valid for these bytes?
 * Returns 1 only if a receipt exists whose digest, length and boot
 * generation all match the bytes presented now. Replacing the blob after
 * verify makes the admission invalid. */
int plat_startup_receipt_valid(const void *bytes, size_t len);

uint32_t plat_startup_receipt_count(void);

/* ── barrier ─────────────────────────────────────────────────────────── */

/* Accept the barrier and publish READY. Refuses unless the phase is
 * PROVIDERS_READY, every mandatory provider is USABLE, and an immutable
 * profile snapshot is published. Returns 0 on success, -1 on refusal;
 * *why is set to a stable reason when non-NULL. */
int plat_startup_accept_barrier(const char **why);

/* Resource balance for the idempotence fixture (P02-090): the number of
 * tracked live resources this module holds. Must be 0 at baseline and 0
 * again after any failure or cancellation. */
int plat_startup_resource_balance(void);

/* Temporary decoded-copy accounting (P02-072). A temporary is registered
 * while a decoded profile copy is live and released when it is wiped;
 * the barrier refuses while any remains outstanding. */
int  plat_startup_temp_acquire(void);
int  plat_startup_temp_release(void);
int  plat_startup_temp_outstanding(void);
include/platx/txn.h
/* txn.h — S423/S428/S442: незавершённое не выдаётся за завершённое.
 *
 * Поверх кадрированного журнала (reclog) кладутся записи транзакций:
 * BEGIN <id> <нагрузка>, затем COMMIT <id> или ABORT <id>.
 *
 * ТРИ ИСХОДА, А НЕ ДВА
 * ────────────────────
 * BEGIN без завершающей записи — это НЕ «не выполнено» и НЕ «выполнено».
 * Процесс умер между началом операции и записью её итога; что успел сделать
 * внешний мир, журнал не знает. Такой транзакции присваивается UNKNOWN, и это
 * отдельный исход, который обязан дойти до оператора (S442). Достроить его до
 * «не выполнено» значит повторить операцию, которая, возможно, прошла;
 * до «выполнено» — потерять её.
 *
 * ПОВТОР ЗАПРЕЩЁН (S423)
 * ──────────────────────
 * Транзакция с известным исходом при повторном разборе журнала НЕ
 * применяется заново. Перезапуск не имеет права молча переиграть то, что уже
 * случилось.
 *
 * ДУБЛИКАТ — НЕ ПОВОД ТЕРЯТЬ СОСЕДА (S428)
 * ────────────────────────────────────────
 * Повторный BEGIN с тем же идентификатором отмечается как дубликат, но
 * остальные записи разбираются дальше. Отбрасывать хвост журнала из-за одной
 * повторной записи значит терять непохожие, вполне действительные операции.
 */
#define PLAT_TXN_ID_MAX   32
#define PLAT_TXN_MAX      256

typedef enum {
    PLAT_TXN_UNKNOWN   = 0,  /* начата, итог не записан — крах посередине */
    PLAT_TXN_COMMITTED = 1,
    PLAT_TXN_ABORTED   = 2
} plat_txn_state_t;

typedef struct {
    char             id[PLAT_TXN_ID_MAX];
    plat_txn_state_t state;
    uint32_t         begins;      /* сколько раз начиналась (дубликаты)   */
    uint32_t         ends;        /* сколько раз завершалась              */
} plat_txn_entry_t;

typedef struct {
    plat_txn_entry_t e[PLAT_TXN_MAX];
    size_t           n;
    uint32_t         duplicates;  /* повторные BEGIN с известным id       */
    uint32_t         orphan_ends; /* COMMIT/ABORT без BEGIN               */
    uint32_t         overflow;    /* не поместившиеся id — НЕ ноль        */
} plat_txn_view_t;

struct xio_t;

int plat_txn_begin (const struct xio_t *io, const char *log, const char *id,
                    const void *payload, size_t len);
int plat_txn_commit(const struct xio_t *io, const char *log, const char *id);
int plat_txn_abort (const struct xio_t *io, const char *log, const char *id);

/* Разобрать журнал в исходы. Идемпотентен: сколько раз ни зови — тот же ответ,
 * и ничего не применяет сам. */
int plat_txn_replay(const struct xio_t *io, const char *log,
                    plat_txn_view_t *out);

const plat_txn_entry_t *plat_txn_find(const plat_txn_view_t *v, const char *id);
include/platx/watchdog.h
/* include/platx/watchdog.h — независимый сторож (A1-P09-421..430).
 *
 * ЗАЧЕМ ОТДЕЛЬНЫЙ ПОТОК
 *
 * До этой волны платформенный сторож жил заданием `recovery:tick` на общем
 * планировщике (src/core/sched.c, заведено в platform_init.c). Планировщик
 * там ОДИН поток, и он выполняет задания ПОСЛЕДОВАТЕЛЬНО:
 *
 *     pthread_mutex_unlock(&s->lock);
 *     if (fn) fn(ud);                  <-- задание выполняется здесь
 *     pthread_mutex_lock(&s->lock);
 *
 * На том же потоке стоят `supervisor:tick`, `keyring:gc` и — существенно —
 * задания, заводимые DSL (`src/dsl/dsl_exec.c`: job_loop_step, job_health),
 * то есть код, приходящий из сценария, а не из ядра. Любое из них, зависнув
 * в fn(), останавливает сторожа НАВСЕГДА: не задерживает, а лишает его
 * следующего тика. Сторож, которого останавливает то, за чем он следит, не
 * отличается от отсутствующего.
 *
 * A3 нашёл ровно это в Windows-части (sweep дедлайнов iocp_pool исполняется
 * только из ветки простоя воркера). Linux-часть больна тем же, в более
 * тяжёлой форме: там сторожил хотя бы какой-то воркер, здесь поток ровно
 * один и второго наблюдателя нет.
 *
 * Здесь у сторожа СВОЙ поток. Он не берёт ни один лок наблюдаемого, не стоит
 * в очереди заданий и не зависит от того, жив ли dispatch.
 *
 * ЧЕГО ЭТОТ МОДУЛЬ НЕ ДЕЛАЕТ
 *
 * Он не меняет состояние жизненного цикла и не перезапускает модули
 * (A1-P09-425). Он публикует ФАКТ: «heartbeat владельца X просрочен на N мс,
 * страйк K». Решение остаётся у восстановления — второго движка
 * восстановления здесь не заводится (A1-P09-448).
 *
 * SPDX-License-Identifier: GPL-2.0
 */

#define PLAT_WD_MAX_WATCH  32u
#define PLAT_WD_RING       64u

/* Род факта. «Здоровье не измерено» — ОТДЕЛЬНЫЙ род, а не разновидность
 * «здоров». Это и есть A1-P09-427: снимок, который не сошёлся, ничего не
 * подтверждает, и публиковать по нему бодрый heartbeat значит отвечать
 * «всё хорошо» на вопрос, на который мы не ответили. */
typedef enum {
    PLAT_WD_FACT_SILENT       = 1,  /* heartbeat просрочен                  */
    PLAT_WD_FACT_UNMEASURABLE = 2,  /* наблюдение не сошлось                */
    PLAT_WD_FACT_SELF_DEGRADED= 3,  /* сам сторож не может измерять время   */
    PLAT_WD_FACT_RECOVERED    = 4   /* был SILENT, снова стучит             */
} plat_wd_fact_kind_t;

/* Факт. Только числа и идентификаторы; ни одного указателя — он копируется
 * в кольцо и переживает наблюдаемого. */
typedef struct plat_wd_fact {
    uint32_t kind;
    uint32_t strikes;      /* сколько подряд просрочек насчитано            */
    uint64_t owner_id;
    uint64_t owner_gen;
    uint64_t task_gen;     /* поколение наблюдаемой задачи (A1-P09-428)     */
    uint64_t age_ms;       /* возраст последнего удара; 0 если не измерен   */
    uint64_t seq;          /* порядковый номер публикации                   */
    uint64_t mono_ms;      /* когда опубликован; 0 если часы отказали       */
} plat_wd_fact_t;

/* Исход наблюдения одного владельца. Возвращает наблюдатель. */
typedef enum {
    PLAT_WD_OBS_OK           = 0,  /* стучит, возраст в *age_ms             */
    PLAT_WD_OBS_SILENT       = 1,  /* молчит дольше допуска                 */
    PLAT_WD_OBS_UNMEASURABLE = 2,  /* снимок не сошёлся / часы отказали     */
    PLAT_WD_OBS_GONE         = 3   /* владельца больше нет — снять с учёта  */
} plat_wd_obs_t;

/* Наблюдатель. Обязан быть НЕБЛОКИРУЮЩИМ и не брать локов наблюдаемого:
 * весь смысл отдельного потока пропадает, если сторож встаёт в ту же
 * очередь (A1-P09-422). Штатная реализация читает plat_task через seqlock. */
typedef plat_wd_obs_t (*plat_wd_observe_fn)(void *ud, uint64_t owner_id,
                                            uint64_t owner_gen,
                                            uint64_t *age_ms,
                                            uint64_t *task_gen);

/* Политика. Снимается ОДИН раз при старте и дальше неизменна (A1-P09-424):
 * сторож, у которого интервал можно подвинуть на ходу, не даёт ни одного
 * утверждения о сроке обнаружения. */
typedef struct plat_wd_policy {
    uint32_t interval_ms;   /* период опроса; 0 отвергается                 */
    uint32_t max_strikes;   /* просрочек до факта SILENT; 0 отвергается     */
    uint32_t dedup_repeat;  /* сколько одинаковых фактов подряд публиковать
                             * до подавления; 0 → 1 (A1-P09-426)            */
} plat_wd_policy_t;

/* ── жизненный цикл ───────────────────────────────────────────────────── */

/* Запустить сторожа на СВОЁМ потоке. Повторный старт — ошибка, а не
 * «уже запущено»: два сторожа считают страйки независимо. */
int plat_wd_start(const plat_wd_policy_t *pol,
                  plat_wd_observe_fn obs, void *ud);
void plat_wd_stop(void);
int  plat_wd_running(void);

/* Действующая политика. Пустой результат (-1), если сторож не запущен.
 * Отдельной функции «поменять интервал» здесь НЕТ намеренно. */
int plat_wd_policy_get(plat_wd_policy_t *out);

/* ── подписка ─────────────────────────────────────────────────────────── */

int plat_wd_watch(uint64_t owner_id, uint64_t owner_gen);
int plat_wd_unwatch(uint64_t owner_id, uint64_t owner_gen);
int plat_wd_watch_count(void);

/* ── факты ────────────────────────────────────────────────────────────── */

/* Забрать опубликованные факты. Кольцо ОГРАНИЧЕНО: если потребитель не
 * успевает, теряются СТАРЫЕ факты, а сторож не встаёт (A1-P09-430).
 * Число потерянных возвращает plat_wd_dropped(). */
int      plat_wd_drain(plat_wd_fact_t *out, int max);
uint64_t plat_wd_dropped(void);
uint64_t plat_wd_published(void);

/* Сколько раз сторож прошёл круг. Это и есть проверяемое «сторож жив»:
 * счётчик растёт независимо от того, что происходит с наблюдаемыми. */
uint64_t plat_wd_ticks(void);

/* Часы сторожа для фикстур. NULL — штатные монотонные. */
void plat_wd_set_clock_for_test(int (*fn)(uint64_t *out));

/* Задать политику и наблюдателя БЕЗ запуска потока. Только для фикстур:
 * штатный plat_wd_start делает первый круг сразу, и для детерминированной
 * проверки этот круг — гонка с тестом. */
int plat_wd_arm_for_test(const plat_wd_policy_t *pol,
                         plat_wd_observe_fn obs, void *ud);

/* Один круг вручную, БЕЗ потока: для детерминированных проверок. Возвращает
 * число ОПУБЛИКОВАННЫХ фактов (подавленные повторы не считаются). */
int plat_wd_tick_once(void);

/* Сбросить всю статику. Только для фикстур. */
void plat_wd_reset_for_test(void);
include/platx/watchdog_bind.h
/* include/platx/watchdog_bind.h — продуктовый вход независимого сторожа.
 *
 * Отделён от watchdog.h намеренно: сам сторож не знает типов платформы и
 * проверяется без неё (tests/bounds/t_watchdog.c линкует его с sched.c и
 * ничем больше). Здесь — привязка, и только она тянет plat_task и профиль.
 *
 * SPDX-License-Identifier: GPL-2.0
 */

/* 0 — сторож запущен; -1 — профиль про сторожа не сказал или поток не
 * создался. -1 означает «платформа осталась без наблюдения» и обязан быть
 * сообщён, а не проглочен. */
int  plat_watchdog_bind_start(void);
void plat_watchdog_bind_stop(void);

int  plat_watchdog_bind_watch(plat_owner_t owner);
int  plat_watchdog_bind_unwatch(plat_owner_t owner);

/* Перенести накопленные факты во вход восстановления. Зовётся из задания
 * recovery:tick, то есть там, где живёт ДЕЙСТВИЕ; наблюдение к этому моменту
 * уже произошло на отдельном потоке. */
int  plat_watchdog_bind_pump(int max);
include/platx/wire.h
/* wire.h — S461/S469/S470: версия на проводе и стойкость к понижению.
 *
 * S469  У каждой точки входа обрамления должна быть версия и ОГРАНИЧЕННОЕ
 *       согласование возможностей. Ограниченное — значит: набор битов
 *       известен заранее, чужие биты отбрасываются, и согласованное никогда
 *       не больше того, что предложила каждая сторона.
 *
 * S470  Понижение отвергается. Сторона, однажды говорившая по версии N, не
 *       принимает предложение N-1: это либо чужая подделка, либо откат, о
 *       котором надо узнать, а не согласиться молча. Отказ от понижения —
 *       единственное, что отличает согласование от подчинения.
 *
 * S461  Событие несёт версию своей полезной нагрузки. Подписчик объявляет,
 *       какую понимает. Несовместимое НЕ ДОСТАВЛЯЕТСЯ — отказ происходит до
 *       вызова обработчика, а не внутри него: обработчик, получивший чужой
 *       формат, уже прочитал его своими полями.
 */
/* ── рукопожатие ─────────────────────────────────────────────────────────── */

#define PLAT_WIRE_VERSION_MIN 1u
#define PLAT_WIRE_VERSION_MAX 3u

/* Известные возможности. Чужие биты в предложении отбрасываются. */
#define PLAT_WIRE_FEAT_AEAD     0x0001u
#define PLAT_WIRE_FEAT_COMPRESS 0x0002u
#define PLAT_WIRE_FEAT_MULTIPART 0x0004u
#define PLAT_WIRE_FEAT_KNOWN    0x0007u

typedef struct {
    uint16_t version;     /* согласованная                       */
    uint16_t features;    /* согласованные возможности           */
    uint16_t floor;       /* ниже этой версии не опускаемся      */
    uint8_t  established; /* 1 — рукопожатие состоялось          */
} plat_wire_t;

typedef enum {
    PLAT_WIRE_OK          = 0,
    PLAT_WIRE_TOO_OLD     = 1,  /* ниже минимума                 */
    PLAT_WIRE_TOO_NEW     = 2,  /* выше максимума                */
    PLAT_WIRE_DOWNGRADE   = 3   /* попытка опустить ниже пола    */
} plat_wire_verdict_t;

/* Начать: пол равен floor (0 — минимум протокола). */
void plat_wire_init(plat_wire_t *w, uint16_t floor);

/* Согласовать с предложением той стороны. */
plat_wire_verdict_t plat_wire_negotiate(plat_wire_t *w, uint16_t peer_version,
                                        uint16_t peer_features);

const char *plat_wire_verdict_name(plat_wire_verdict_t v);

/* ── версии событий (S461) ───────────────────────────────────────────────── */

typedef struct {
    uint32_t type;
    uint16_t payload_version;
} plat_event_hdr_t;

/* 1 — доставлять, 0 — нет. Подписчик объявляет минимальную и максимальную
 * версию нагрузки, которую он умеет читать. */
int plat_event_deliverable(const plat_event_hdr_t *e,
                           uint16_t sub_min, uint16_t sub_max);
include/platx/witness_provider.h
/* platx/witness_provider.h — witness_provider_v1 (D053).
 *
 * A witness does not stream facts. It answers a bounded question about state
 * that something else claims: "which tasks exist", "is this page still the
 * one that was measured", "which modules are loaded". Its value is that it
 * looks from somewhere else — an LKM below the process, a hypervisor below
 * the kernel, a firmware measurement made before either existed.
 *
 * Two rules make it worth having:
 *   1. A claim carries what it is a claim ABOUT and where it was made from.
 *      A claim without a subject is an opinion (D086, D088).
 *   2. The consistency of a snapshot is measured, not declared. A coordinator
 *      that lets a provider label a best-effort walk "atomic" has built a
 *      quorum out of one opinion repeated (D064).
 *
 * ABI freeze: WITNESS_PROVIDER_ABI 1 (D053 sealed 2026-09-01)
 */

#define WITNESS_PROVIDER_ABI  1u

#define WITNESS_SUBJECT_MAX   96u
#define WITNESS_CLAIMS_MAX    64u   /* per snapshot, per provider */
#define WITNESS_EVIDENCE_MAX  64u   /* bytes of an evidence reference */

/* ── Consistency of a snapshot (D063) ────────────────────────────────────
 * ATOMIC      one freeze point; nothing changed under the walk, and the
 *             provider can prove it (a stop-the-world token, a generation
 *             that did not move).
 * COORDINATED several providers walked inside one bounded window and the
 *             window is stated. Deltas during the window are reported.
 * BEST_EFFORT the walk was live; boundaries are unknown.
 * OFFLINE     the subject could not be observed at all. Not an empty result:
 *             an empty ATOMIC snapshot claims "nothing exists", an OFFLINE
 *             one claims nothing.
 */
typedef enum witness_consistency {
    WITNESS_CONS_OFFLINE     = 0,
    WITNESS_CONS_BEST_EFFORT = 1,
    WITNESS_CONS_COORDINATED = 2,
    WITNESS_CONS_ATOMIC      = 3,
    WITNESS_CONS_MAX
} witness_consistency_t;

/* ── What a claim is about ───────────────────────────────────────────────── */
typedef enum witness_subject {
    WITNESS_SUBJ_NONE     = 0,
    WITNESS_SUBJ_TASK     = 1,   /* a task/thread the kernel knows */
    WITNESS_SUBJ_MODULE   = 2,   /* a loaded kernel module */
    WITNESS_SUBJ_SOCKET   = 3,   /* an open socket */
    WITNESS_SUBJ_PAGE     = 4,   /* a physical/guest page under protection */
    WITNESS_SUBJ_FILE     = 5,   /* a file identity, not its content */
    WITNESS_SUBJ_MEASURE  = 6,   /* a measurement/baseline entry (IMA, TPM) */
    WITNESS_SUBJ_MAX
} witness_subject_t;

/* ── Evidence reference (D091) ───────────────────────────────────────────
 * A handle, never a blob. SENSE stores references to evidence held by
 * FORENSIC; copying the bytes into the observation plane would make every
 * consumer a custodian of material it cannot seal.
 */
typedef struct witness_evidence_ref {
    uint32_t kind;                       /* forensic store class */
    uint32_t len;                        /* bytes used in `ref` */
    uint8_t  ref[WITNESS_EVIDENCE_MAX];  /* opaque handle */
    uint8_t  digest[32];                 /* SHA-256 of the referenced object */
    uint64_t sealed_at_ns;               /* 0 = not sealed */
} witness_evidence_ref_t;

/* ── One claim ───────────────────────────────────────────────────────────── */
typedef struct witness_claim {
    uint32_t abi_version;
    uint32_t struct_size;

    uint32_t subject;                    /* witness_subject_t */
    uint32_t flags;
    char     subject_id[WITNESS_SUBJECT_MAX];  /* stable subject identity */

    uint64_t observed_at_ns;
    uint64_t epoch;                      /* provider epoch of the observation */
    uint64_t value_hi;                   /* subject-specific measurement */
    uint64_t value_lo;

    uint32_t independence_group;         /* copied from the envelope */
    uint32_t bridge_id;                  /* copied from the envelope (D089) */

    witness_evidence_ref_t evidence;     /* zeroed when there is none */
} witness_claim_t;

#define WITNESS_CLAIM_F_PRESENT   (1u << 0)  /* the subject exists */
#define WITNESS_CLAIM_F_ABSENT    (1u << 1)  /* the subject provably does not */
#define WITNESS_CLAIM_F_HIDDEN    (1u << 2)  /* present here, absent elsewhere */
#define WITNESS_CLAIM_F_MEASURED  (1u << 3)  /* value_* is a measurement */
#define WITNESS_CLAIM_F_SEALED    (1u << 4)  /* evidence.sealed_at_ns is set */

/* ── A snapshot as it was actually taken ─────────────────────────────────── */
typedef struct witness_snapshot {
    uint32_t abi_version;
    uint32_t struct_size;

    uint32_t consistency;      /* witness_consistency_t ACHIEVED, not asked */
    uint32_t n_claims;

    uint64_t window_begin_ns;  /* both zero only for OFFLINE */
    uint64_t window_end_ns;
    uint64_t deltas_during;    /* changes observed inside the window */
    uint64_t freeze_token;     /* non-zero only when the walk was frozen */

    witness_claim_t claims[WITNESS_CLAIMS_MAX];
} witness_snapshot_t;

/* ── The vtable ──────────────────────────────────────────────────────────── */
typedef struct witness_provider_v1 {
    uint32_t abi_version;      /* WITNESS_PROVIDER_ABI */
    uint32_t struct_size;
    void    *ctx;

    int (*describe)(void *ctx, prov_envelope_t *out, prov_status_t *st);
    int (*negotiate)(void *ctx, const prov_negotiate_req_t *req,
                     prov_negotiate_result_t *out);

    /* Best consistency this provider can actually deliver right now. The
     * coordinator asks before it promises, and never promises more. */
    int (*max_consistency)(void *ctx, uint32_t subject,
                           uint32_t *consistency_out, prov_status_t *st);

    /* Take a bounded snapshot of one subject class under a budget. The
     * provider fills `consistency` with what it achieved; claiming more than
     * max_consistency() reported is a conformance failure (D064). */
    int (*snapshot)(void *ctx, uint32_t subject, const prov_budget_t *budget,
                    witness_snapshot_t *out, prov_status_t *st);

    int (*health)(void *ctx, prov_health_t *out, prov_status_t *st);
    int (*selftest)(void *ctx, prov_selftest_t *out, prov_status_t *st);

    void *reserved[8];
} witness_provider_v1_t;

/* Name of a consistency level, for evidence and logs. Never NULL. */
const char *witness_consistency_name(uint32_t c);
const char *witness_subject_name(uint32_t s);
include/platx/zeroize.h
/* include/platx/zeroize.h — ограниченная очистка Core-owned контекстов
 * (A1-P09-431..438).
 *
 * ЧТО ЭТОТ МОДУЛЬ ОБЕЩАЕТ И ЧЕГО НЕ ОБЕЩАЕТ
 *
 * Обещает: перечисленные ЗДЕСЬ буферы будут затёрты неустранимой записью, и
 * порядок будет такой, что новое использование запрещено ДО начала медленной
 * уборки, а не после неё.
 *
 * Не обещает ничего про копии, которых этот модуль не видит: страницы,
 * отданные ядру, буферы аллокатора, срезы в стеке чужих функций, содержимое
 * swap и core-дампа. Их сюда не записывают и «очищенными» не называют
 * (A1-P09-432). Список областей — это ИНВЕНТАРЬ ПОДКОНТРОЛЬНОГО, а не
 * утверждение «все секреты в системе стёрты»; выдать первое за второе —
 * ровно тот способ соврать, от которого карточка 449 предостерегает.
 *
 * ПОЧЕМУ ПОРЯДОК ИМЕННО ТАКОЙ
 *
 *     tamper -> revoke -> drain -> zeroize
 *
 * Отзыв допуска стоит ПЕРВЫМ и он мгновенный. Затирание — медленное: оно
 * идёт по областям и ограничено шагом. Если сначала затирать, а потом
 * закрывать вход, то всё время уборки контекст остаётся доступным, и заявка,
 * пришедшая в середине, прочитает наполовину затёртый буфер. Поэтому вход
 * закрывается одной записью состояния, и только затем начинается уборка.
 *
 * SPDX-License-Identifier: GPL-2.0
 */

#define PLAT_ZX_MAX_REGIONS 32u
#define PLAT_ZX_TAG_MAX     24u

/* Состояние контекста. Порядок значений совпадает с порядком перехода;
 * назад состояние не идёт никогда, кроме явного rearm с НОВЫМ поколением. */
typedef enum {
    PLAT_ZX_LIVE     = 0,  /* обычная работа                                */
    PLAT_ZX_REVOKED  = 1,  /* tamper записан, вход закрыт, уборка не начата  */
    PLAT_ZX_DRAINING = 2,  /* уборка идёт, ограничена шагами                 */
    PLAT_ZX_ZEROIZED = 3,  /* уборка закончена; терминальное                 */
    PLAT_ZX_KILLED   = 4   /* финальный kill-switch (A1-P09-437)             */
} plat_zx_state_t;

/* Причина терминального исхода. Первая записанная НЕ перезаписывается:
 * повторный tamper во время уборки не меняет ответ на вопрос «почему»
 * (A1-P09-436). */
typedef enum {
    PLAT_ZX_REASON_NONE      = 0,
    PLAT_ZX_REASON_TAMPER    = 1,
    PLAT_ZX_REASON_KILL      = 2,
    PLAT_ZX_REASON_SHUTDOWN  = 3
} plat_zx_reason_t;

/* Класс заявки на допуск (A1-P09-437). Только уборка Координатора проходит
 * после kill; всё остальное отвергается. */
typedef enum {
    PLAT_ZX_ADMIT_EFFECT  = 0,  /* обычный эффект ACT                       */
    PLAT_ZX_ADMIT_CLEANUP = 1   /* разрешённая уборка                       */
} plat_zx_admit_class_t;

typedef enum {
    PLAT_ZX_ADMIT_OK      = 0,
    PLAT_ZX_ADMIT_REVOKED = 1,  /* контекст отозван                         */
    PLAT_ZX_ADMIT_KILLED  = 2,  /* сработал kill-switch                     */
    PLAT_ZX_ADMIT_STALE   = 3   /* поколение заявителя не то                */
} plat_zx_admit_t;

/* Исход по ОДНОЙ области. Различие проверенного и непроверенного —
 * содержательное: «я записал нули» и «я перечитал и увидел нули» это разные
 * утверждения, и второе доступно не всегда (A1-P09-438, 449). */
typedef enum {
    PLAT_ZX_OUT_PENDING   = 0,
    PLAT_ZX_OUT_VERIFIED  = 1,  /* затёрто и перечитано                     */
    PLAT_ZX_OUT_WRITTEN   = 2,  /* затёрто, перечитать нечем                */
    PLAT_ZX_OUT_HELD      = 3   /* НЕ затёрто: область удерживается кем-то  */
} plat_zx_outcome_t;

typedef struct plat_zx_region_report {
    char     tag[PLAT_ZX_TAG_MAX];
    size_t   len;
    uint32_t outcome;
} plat_zx_region_report_t;

/* Итог уборки. Ни одного поля вида «успех»: слово SUCCEEDED здесь не
 * употребляется намеренно (A1-P09-438). Есть числа, и по ним видно, что
 * именно проверено, а что только записано или вовсе удержано. */
typedef struct plat_zx_report {
    uint32_t state;
    uint32_t reason;
    uint32_t regions_total;
    uint32_t regions_verified;
    uint32_t regions_written;
    uint32_t regions_held;      /* честный остаток: см. A1-P09-446          */
    uint32_t steps_used;
    uint32_t steps_budget;
    uint32_t tamper_count;      /* включая повторные во время уборки        */
    uint32_t restarts;          /* обязан остаться 0 (A1-P09-436)           */
    uint64_t generation;
} plat_zx_report_t;

/* ── инвентарь (A1-P09-432) ───────────────────────────────────────────── */

/* Внести подконтрольную область. Только память, которой владеет Core и
 * которую он может затереть сам. Всё, что «наверное, тоже копия», сюда не
 * вносится: инвентарь, в котором есть недоказанное, перестаёт быть
 * инвентарём. */
int plat_zx_register(void *addr, size_t len, const char *tag);

/* То же, но с явным указанием, можно ли ПЕРЕЧИТАТЬ область после записи.
 *
 * verifiable == 0 — область записать можно, прочитать обратно нельзя
 * (write-only отображение, буфер устройства, чужая страница). Такая область
 * получает исход WRITTEN, а не VERIFIED, и разница эта содержательная:
 * «я записал нули» и «я перечитал и увидел нули» — разные утверждения, и
 * складывать их в одно число значит приписать отчёту проверку, которой не
 * было (A1-P09-438, 449). plat_zx_register == register_ex с verifiable = 1. */
int plat_zx_register_ex(void *addr, size_t len, const char *tag,
                        int verifiable);
int plat_zx_region_count(void);

/* ── жизненный цикл (A1-P09-431, 435, 436) ────────────────────────────── */

int plat_zx_arm(uint64_t generation);

/* Записать tamper. Вход закрывается ЗДЕСЬ и немедленно; уборка не начата.
 * Повторный вызов во время уборки считается, но не перезапускает её и не
 * переписывает первую причину (A1-P09-436). */
int plat_zx_tamper(plat_zx_reason_t reason);

/* Финальный kill-switch (A1-P09-437). */
int plat_zx_kill(void);

/* Один шаг уборки. Возвращает число обработанных областей, 0 — больше нечего,
 * -1 — уборка не начата. Шаги ограничены: cleanup не может стать бесконечным
 * ни при каком поведении наблюдаемого. */
int plat_zx_drain_step(void);

/* Довести уборку до конца в пределах бюджета шагов. */
int plat_zx_drain_all(void);

/* Заново вооружить контекст. Разрешено ТОЛЬКО из терминального состояния и
 * ТОЛЬКО с поколением строго больше прежнего (A1-P09-435): затёртый
 * контекст не оживает по тому же адресу и по старым токенам. */
int plat_zx_rearm(uint64_t new_generation);

/* ── допуск и отчёт ───────────────────────────────────────────────────── */

plat_zx_admit_t plat_zx_admit(uint64_t generation, plat_zx_admit_class_t cls);

int plat_zx_report(plat_zx_report_t *out);
int plat_zx_regions(plat_zx_region_report_t *out, int max);
plat_zx_state_t plat_zx_state(void);

/* Неустранимая запись нулей поверх области (A1-P09-433). Отдельно
 * экспортирована, чтобы её можно было проверить в оптимизированной сборке. */
void plat_zx_wipe(void *addr, size_t len);

/* Пометить область как удерживаемую: её нельзя затирать, пока владелец не
 * отпустит. Существует ради честности отчёта — удержанная область попадёт в
 * regions_held, а не будет молча пропущена (A1-P09-446). */
int plat_zx_hold(const char *tag, int held);

void plat_zx_reset_for_test(void);
06

crypto

Криптографические примитивы и версионированные сервисы
src/crypto/Доверие и защита10 файлов3 API headers

Crypto предоставляет небольшой, тестируемый набор primitives и versioned services для authenticated encryption, hashing/signature и key handling. Он концентрирует сложные правила nonce, key lifetime, errors и zeroization.

Граница ответственности

  • Consumers не копируют алгоритмы и не зависят от private context layout.
  • Algorithm negotiation отделён от primitives и принадлежит protocol/plugin format.
  • Randomness поступает из проверенного OS source; deterministic seed только test profile.

Устройство подсистемы

  • crypto.aead v1 принимает explicit key handle/material, nonce, associated data, input/output bounds и возвращает typed error.
  • Ed25519 verify/sign wrappers используют canonical encoding и reject noncanonical inputs.
  • Key buffers живут в wipe-aware allocation; ownership transfer обозначен API.
  • Backend/provider может меняться при неизменном service ABI и known-answer suite.

Поток работы

  • Consumer resolve service/version.
  • Validate sizes/key/nonce.
  • Primitive operation без hidden allocation где возможно.
  • Result/error + wipe temporaries + metrics без secret.

Отказ и восстановление

  • Authentication failure не различает полезные oracle details внешнему consumer.
  • OOM/short output не оставляет partial plaintext/ciphertext как success.
  • Nonce policy violation диагностируется до seal.
  • Provider revoke делает handle stale, а не вызывает dangling function table.

Основные возможности

  • Authenticated encryption primitives for payload/storage paths.
  • Ed25519 signing/verification supports manifests/plugins/scripts.
  • Used by RA2C, vault, keyring and protected plugin flows.
Архитектурные детали и инварианты

Overview

Pure-C crypto primitives. No heap allocation. All functions require explicit output_len parameter (INV-CRYPTO-01).

Invariants

INV-CRYPTO-01 | All crypto functions require output_len parameter; return -1 if output buffer too small

Управление и диагностика

Домен не регистрирует собственную корневую команду. Его контракт используется вызывающими подсистемами через API и интерфейсы trust, vault, keyring.

Справочник CLI / crypto →
Состав подсистемы / 10 файлов
Файл / компонентНазначение и граница
src/crypto/chacha20poly1305.cРеализация chacha20poly1305
src/crypto/crypto.cсамодостаточные криптографические примитивы. Реализованы без внешних зависимостей. См. crypto.h для API.
src/crypto/crypto.hсамодостаточные криптографические примитивы. Без внешних зависимостей (no OpenSSL, no libsodium). Реализованы согласно спецификациям: SHA-256 FIPS 180-4 HMAC-SHA256 RFC 2104 HKDF-SHA256 RFC 5869 XChaCha20-Poly1305 (AEAD, nonce 24 байта)
src/crypto/crypto_ed25519.cсамодостаточный Ed25519 (RFC 8032) + SHA-512 (FIPS 180-4). Без внешних зависимостей (no OpenSSL, no libsodium). Реализация EdDSA на кривой edwards25519 адаптирована из TweetNaCl (Bernstein, van Gastel, Janssen, Lange, Schwabe, Smetsers — public domain), SHA-512 — самостоятельная
src/crypto/crypto_native_v2.cDecrypt never exposes unauthenticated bytes after returning, including a provider error after it has already filled part of the output buffer.
src/crypto/crypto_password_kdf.cStable public ABI from upstream argon2.h. This declaration lets a runtime package supply the implementation without copying cryptographic code.
src/crypto/platx_ed25519.cреализация публичного контракта Ed25519 из include/platx/platx_crypto.h. Заголовок `platx/platx_crypto.h` объявлял platx_ed25519_keygen/sign/verify, и на них уже ссылался тест `tests/crypto/t_crypto_ed25519.c`, но НИ ОДНОЙ реализации в дереве не было: `grep` по всем .c давал только объявления.
src/crypto/platx_signer.cPlatX: checkpoint signer/verifier helpers. и отсутствия владельца ключа. Конституция: «не второй crypto stack». Ed25519, SHA, RNG и работа с ключами живут в крипто-модуле платформы; сюда они не переезжают.
src/crypto/platx_signer.hPlatX: checkpoint signer/verifier interface. И SP FORENSIC checkpoint, и MIRAGE ledger checkpoint несут поле `checkpoint_signature[64]` под Ed25519. Сейчас оно нулевое: ключа у этих модулей нет и быть не должно. Нулевая подпись читается как «не подписан» — это честно, но означает, что печать сегмента
src/crypto/sha256.cРеализация sha256
Контракты API / 3 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/services/crypto_v1.h
/* platx/services/crypto_v1.h — domain crypto. Core keeps only hash+sign. */
#define PLAT_CAP_CRYPTO_AEAD  "crypto.aead"
#define PLAT_CRYPTO_AEAD_V1   0x00010000u

typedef struct plat_crypto_aead_v1 {
    uint32_t struct_size;
    int (*seal)(const void *key, size_t klen,
                const void *nonce, size_t nlen,
                const void *pt, size_t ptlen,
                void *ct, size_t *ctlen);
    int (*open)(const void *key, size_t klen,
                const void *nonce, size_t nlen,
                const void *ct, size_t ctlen,
                void *pt, size_t *ptlen);
} plat_crypto_aead_v1_t;
include/platx/services/crypto_v2.h
/* Authenticated metadata is part of the crypto contract, never discarded by
 * a caller-side adapter to v1. All buffers are caller-owned and bounded. */
#define PLAT_CRYPTO_AEAD_V2 0x00020000u
#define PLAT_CRYPTO_AES256_GCM 1u
#define PLAT_CRYPTO_V2_MAX (1024u * 1024u)
#define PLAT_CRYPTO_V2_AAD_MAX 65520u

typedef struct plat_crypto_aead_v2 {
    uint32_t struct_size;
    uint32_t abi_version;
    uint32_t algorithm;
    uint32_t flags;
    int (*random)(void *out, size_t len);
    /* ct = ciphertext || 16-byte tag; nonce is exactly 12 bytes. Buffers
     * must not overlap. On failure output length is zero; authentication or
     * provider failure wipes produced output. Invalid arguments do not touch
     * output bytes. A supplied capacity never means bytes produced. */
    int (*seal)(const uint8_t key[32], const uint8_t nonce[12],
                const void *aad, size_t aad_len, const void *pt, size_t pt_len,
                void *ct, size_t capacity, size_t *written);
    int (*open)(const uint8_t key[32], const uint8_t nonce[12],
                const void *aad, size_t aad_len, const void *ct, size_t ct_len,
                void *pt, size_t capacity, size_t *written);
    void (*wipe)(void *data, size_t len);
} plat_crypto_aead_v2_t;

/* Host-side crypto provider, registered by a host through the existing ABI.
 * Linux uses the optional OpenSSL provider; Windows uses native BCrypt.
 * NULL means this build has no provider. No algorithm fallback is allowed. */
const plat_crypto_aead_v2_t *plat_crypto_native_aead_v2(void);
include/platx/services/crypto_verify_v1.h
/* platx/services/crypto_verify_v1.h — versioned public verification API.
 *
 * ЗАЧЕМ
 *
 * Проверка подписи — единственная крипто-операция, которая нужна
 * потребителям вне крипто-модуля: загрузчик плагинов, архиватор,
 * доказательная плоскость. До сих пор каждый из них подключал
 * внутренний src/crypto/crypto.h и звал crypto_ed25519_verify()
 * напрямую. Это давало им весь крипто-стек целиком — AEAD, HKDF,
 * генерацию ключей, RNG — там, где требовалось одно предложение:
 * «эта подпись над этим сообщением этим ключом — верна или нет».
 *
 * Внутренний заголовок не является контрактом. Он меняется вместе с
 * реализацией: там лежат структуры состояния SHA, enum'ы шифров,
 * сигнатуры, которые никто не обещал сохранять. Модуль, который на
 * него завязан, ломается от правки, не имеющей к нему отношения,
 * и — что хуже — получает доступ к примитивам, которыми ему нечего
 * делать. Загрузчику плагинов не нужен crypto_ed25519_keypair().
 *
 * Этот заголовок — публичная версионированная поверхность: ровно
 * два примитива (SHA-256 и Ed25519 verify), явная версия ABI, и
 * обещание, что сигнатуры не поменяются внутри мажорной версии.
 * Реализация остаётся в крипто-модуле и остаётся единственной —
 * это тонкая обёртка, а не второй крипто-стек.
 *
 * КОНТРАКТ ОТКАЗА (fail-closed)
 *
 * Обе функции возвращают 0 ТОЛЬКО при доказанном успехе. Любая
 * иная ситуация — NULL-аргумент, недопустимая длина, битый ключ,
 * несовпадение подписи — это -1. Верификатор, который отвечает
 * «валидно» при отсутствии входных данных, превращает непроверенное
 * в проверенное; это худший из возможных дефектов в этом файле.
 *
 * SPDX-License-Identifier: GPL-2.0
 */

/* ── Версия ──────────────────────────────────────────────────────────────
 * Формат: major(16) | minor(16). Мажор меняется только при несовместимом
 * изменении сигнатур; минор — при добавлении функций в конец файла.  */
#define PLATX_CRYPTO_VERIFY_V1          0x00010000u
#define PLATX_CRYPTO_VERIFY_VERSION_MAJOR  1
#define PLATX_CRYPTO_VERIFY_VERSION_MINOR  0

/* ── Замороженные размеры ────────────────────────────────────────────────
 * Ed25519 (RFC 8032) и SHA-256 (FIPS 180-4). Эти числа — часть ABI:
 * они уже зашиты в формат .platx_sig и в записи checkpoint.            */
#define PLATX_ED25519_SIG_LEN       64
#define PLATX_ED25519_PUBKEY_LEN    32
#define PLATX_SHA256_DIGEST_LEN     32

/* Версия реализации, с которой собран вызывающий бинарь.
 * Возвращает PLATX_CRYPTO_VERIFY_V1. Позволяет плагину, загруженному
 * из отдельного ELF, убедиться, что говорит с той же поверхностью. */
uint32_t platx_crypto_verify_abi(void);

/* SHA-256 над буфером — одна операция, без потокового контекста.
 *
 * data может быть NULL только при len == 0 (хеш пустого сообщения).
 * out обязателен.
 *
 * Возврат:  0 — дайджест записан в out
 *          -1 — недопустимые аргументы; содержимое out не определено */
int platx_crypto_sha256(const void *data, size_t len,
                        uint8_t out[PLATX_SHA256_DIGEST_LEN]);

/* Проверка detached-подписи Ed25519 над сообщением msg.
 *
 * sig    — 64 байта (R || S)
 * msg    — сообщение; может быть NULL только при msglen == 0
 * pubkey — 32 байта открытого ключа
 *
 * Возврат:  0 — подпись математически верна для этой пары (msg, pubkey)
 *          -1 — подпись неверна, ключ не раскодируется, или аргументы
 *               недопустимы
 *
 * Ноль означает ровно одно: подпись верна. Он НЕ означает, что ключ
 * кому-то доверяют — политика доверия живёт выше, в trust store
 * вызывающего. Разделение намеренное: математика и политика — разные
 * вопросы, и склеивать их в один код возврата значит потерять оба. */
int platx_crypto_ed25519_verify(const uint8_t sig[PLATX_ED25519_SIG_LEN],
                                const void *msg, size_t msglen,
                                const uint8_t pubkey[PLATX_ED25519_PUBKEY_LEN]);
07

dbg

Лаборатория отладки процессов, памяти и исполнения
src/dbg/Наблюдение и исследование30 файлов0 API headers

DBG — research tooling для изучения process state, maps, registers, memory, disassembly, trace и controlled injection. Его промышленная обязанность — быть полностью отделённым от поддерживаемых profiles и не расширять trusted Core.

Граница ответственности

  • Только explicit research/full binary и operator authorization.
  • Target context/generation проверяется до ptrace/process_vm operations.
  • Injection never masquerades as normal plugin/hook operation.

Устройство подсистемы

  • Debug session имеет owner, target context, attach state, acquired OS handles, stop deadline и evidence output.
  • Disassembler/patch/inject backends оформляются operations с dry-run/report.
  • Detach идемпотентно восстанавливает target state и освобождает resources даже после partial failure.

Поток работы

  • Operator selects target context.
  • Policy/privilege preflight → session attach.
  • Read-only inspect либо separately armed mutation.
  • Detach/target exit → cleanup/report.

Отказ и восстановление

  • Target exit/PID reuse → stale context, session terminates.
  • Partial write/injection не объявляется success; evidence содержит bytes/ranges.
  • Debugger crash cleanup обеспечивается OS + Core reaper/child isolation.
  • Unsupported architecture rejects before decode/patch.

Основные возможности

  • Process attach and register/map inspection.
  • Disassembly, memory operations and tracing helpers.
  • Injection-oriented experiments kept outside server/Core acceptance profiles.

Управление и диагностика

Корневые команды: dbg. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / dbg →
Состав подсистемы / 30 файлов
Файл / компонентНазначение и граница
src/dbg/cmd_dbg.cnamespace "dbg": интеграция dbg_forge в платформу memfd. Маршрутизирует команды вида dbg dasm [args] — дизассемблер (x86/arm/arm64) dbg debug [args] — отладчик ptrace dbg trace [args] — трассировщик syscall/инструкций dbg analyze [args] — анализатор виртуальной памяти (/proc)
src/dbg/dbg_core.cслияние: args.c, common.c, dispatch.c, elf.c, log.c, output.c, proc.c, session.c
src/dbg/dbg_debug.cРеализация dbg / debug
src/dbg/dbg_disasm.cвстроенный дизассемблер x86/x86-64/arm/arm64 (без capstone). Заменяет прежнюю реализацию на capstone: платформа собирается без внешних библиотек. Декодеры ниже, затем API df_disasm, затем команды dasm.
src/dbg/dbg_inject.cмодуль inject (инъекция кода и перехват функций), из inj/.
src/dbg/dbg_inject_ext.incdbg_inject_ext.inc — фазы 1–4 инструментации. Включается из dbg_inject.c; не компилировать отдельно.
src/dbg/dbg_inject_ftrace.incdbg_inject_ftrace.inc — ftrace LKM ioctl-команды. Включается из dbg_inject.c; не компилировать отдельно.
src/dbg/dbg_inject_smc.incdbg_inject_smc.inc — self-modifying code (smc-*). Включается из dbg_inject.c; не компилировать отдельно.
src/dbg/dbg_inject_uring.incdbg_inject_uring.inc — uring-steal / uring-control / uring-signal. Включается из dbg_inject.c; не компилировать отдельно.
src/dbg/dbg_lab.cDBG Lab v1: корневая таблица диспетчера подсистемы lab. Регистрирует df_lab_command (группа "lab") в глобальной таблице dbg. Подгруппы: target, session, gadget. Остальные группы (chain, heap, spray, crash) возвращают UNSUPPORTED + версию до реализации в последующих этапах.
src/dbg/dbg_lab.hDBG Lab v1: сессии, гаджеты, chain (Этап 1: target + gadget). Lab — фасад над существующим dbg-ядром (dbgforge / src/dbg). - Только x86-64 LE ELF64 — цель приёмки v1. AArch64 — UNSUPPORTED + версия. - generation >= 1; generation == 0 или stale — отказ. - Не включаем внутренности mesh/ra2c/xim/hook.
src/dbg/dbg_lab_chain.cDBG Lab Этап 2: Chain Engine. Реализует: dbg lab chain new/goal/constraint/solve/verify/explain/export Bounded symbolic solver (read-only). - Solver read-only: никаких write-операций в процесс. dbg.mutate = N/A. - Timeout default 1000ms, hard 30000ms. Исчерпание → TIMEOUT + partial.
src/dbg/dbg_lab_chain.hDBG Lab Этап 2: Chain Engine types and API. Solver только читает ELF; никаких write-операций в процесс.
src/dbg/dbg_lab_crash.cDBG Lab v1: crash triage from ELF + Linux core dump. Разбирает core dump: PT_NOTE (prstatus/siginfo/prpsinfo), /proc/maps copy. Строит backtrace module+offset без абсолютных VA. Signature стабильна между прогонами: "signame|module+offset|access|type". taint/minimize → DF_ERR_UNSUP (нет runner в v1).
src/dbg/dbg_lab_crash.hDBG Lab v1: Crash triage from ELF + core dump. Этап 4. Разбирает Linux ELF core (PT_NOTE: prstatus/siginfo/maps). Строит backtrace как module+offset (без ASLR-адресов). Signature: стабильная строка без абсолютных VA — совпадает на двух прогонах. taint/minimize: UNSUPPORTED (нет runner в v1).
src/dbg/dbg_lab_evidence.cDBG Lab v1: Evidence Bundle save/load/compare. Формат файла .pxde: JSON-объект, последнее поле — "integrity_sha256". Integrity = SHA-256(того же файла, но integrity_sha256 = "0000...0000").
src/dbg/dbg_lab_evidence.hDBG Lab v1: Evidence Bundle (.pxde) API. Формат: самодостаточный JSON с полем integrity_sha256. Integrity: SHA-256 файла с integrity_sha256="0000...0000" (64 нуля).
src/dbg/dbg_lab_gadget.cDBG Lab v1: Gadget Engine на df_disasm. Реализует: dbg lab gadget scan/show/query/rebase/export lab_gadget_scan_elf(), lab_sem_parse(), lab_sem_match() - Декодирование через df_disasm_one(), не байтовые сигнатуры. - Overlapping scan: каждый байт в RX PT_LOAD — потенциальное начало.
src/dbg/dbg_lab_heap.cDBG Lab v1: Heap timeline, UAF candidates, Spray metrics. Этап 3. Реализация: - in-memory event timeline с generation-tracking по адресу - UAF-детектор: use в [free, reuse) → кандидат с правилом + confidence - Spray metrics: density, monotonicity, reuse_rate
src/dbg/dbg_lab_heap.hDBG Lab v1: Heap timeline, UAF candidates, Spray metrics. Этап 3. Timeline хранится в памяти. Live-трассировка (trace start) требует dbg.attach; до реализации возвращает UNSUPPORTED + версию. - generation >= 1; generation == 0 — невалиден. - Адрес после free — то же поколение; после reuse — новое поколение.
src/dbg/dbg_lab_target.cDBG Lab v1: управление сессиями и анализ ELF-цели. Реализует: dbg lab target open/attach/core/info/close dbg lab session list/save/load Сессии хранятся в $XDG_RUNTIME_DIR/dbg_forge/lab/.sess в формате key=value. Активная сессия — файл .../lab/active.
src/dbg/dbg_mem.cслияние: mem.c, mem_core.c, mem_dump.c, mem_info.c, mem_search.c, mem_util.c, mem_viz.c
src/dbg/dbg_probe.cCompiled only when PLATX_DEBUG is defined. Probes are named breakpoint-like insertion points that can fire a callback when hit — useful for white-box testing and fuzzing without attaching a real debugger.
src/dbg/dbg_rop.cbounded, read-only ROP inventory for x86-64 ELF images. This intentionally scans only executable PT_LOAD segments and recognizes a small, auditable set of instruction-complete gadgets. It never attaches to a process and never writes the target. File offsets and ELF virtual
src/dbg/dbg_trace.cслияние: syscalls.c, trace.c
src/dbg/dbgforge.hвстроенный отладчик (бывш. dbg_forge), единый заголовок. dbg не рабочий путь, не XIO, не C2.
src/dbg/hades_if.hC interface to Hades eBPF/r0r3 ring-buffer subsystem. Hades provides a lock-free ring buffer shared between BPF (ring0) and userspace (ring3) for low-latency event delivery without a syscall per event. The BPF side maps the buffer via bpf_ringbuf_output(); the
src/dbg/inject_if.hпубличный vtable подсистемы dbg_inject. Публикуется через platform_provide(INJECT_IF_NAME, INJECT_IF_VERSION, ...) так что elf, hades, DSL-манифесты и ms-скрипты могут вызывать операции инъекции без прямой линковки с dbg_inject.c. Параллельно определяем ms_script_if_t — vtable скриптового движка,
src/dbg/poe_if.hC interface to POEngine (Platform Obfuscation Engine). POEngine provides shellcode obfuscation, payload encryption and binary protection via level8_crypto. The C++ implementation lives in external/poengine/ and is exposed through drm_poengine_bridge.cpp
src/dbg/smc.hSelf-Modifying Code primitives with atomic patch/swap. Pure-C header (POSIX + GCC/Clang __atomic builtins), no platform SDK deps. Linux x86-64. Include from any module that needs SMC/trampoline support. Compile with -Wall -Wextra; the static-inline functions emit no code unless
Контракты API / 0 заголовков

Отдельный заголовок из include/platx/ не закреплён за этим доменом в справочнике. Внутренние границы перечислены в составе файлов; управление и публичные связи описаны в соседних подсистемах.

08

drm

Контроль защищённого содержимого и жизненного цикла расширений
src/drm/Доверие и защита10 файлов0 API headers

DRM управляет lifecycle защищённых plugin/material assets: verify, protect, activate, rotate, deactivate и controlled destruction. Он объединяет crypto/vault/plugin/POE, но не должен создавать альтернативный plugin manager.

Граница ответственности

  • Plugin lifecycle остаётся у plugin module; DRM добавляет protection policy/envelope.
  • Self-destruct означает cryptographic revoke/wipe managed data, не неограниченное удаление filesystem.
  • POE/anti-debug не считаются trust boundary.

Устройство подсистемы

  • Protected record содержит plugin identity/version, signer, encrypted envelope, key reference, activation state, deadline и owner.
  • Activation transaction проверяет trust, раскрывает material в bounded wipe buffer, передаёт plugin verifier/loader и стирает temporary.
  • Rotation создаёт новое envelope поколение и атомарно переключает reference.
  • Timeout task через Core Task API инициирует policy event, не detached thread.

Поток работы

  • Protect request → plugin verify → encrypt/store metadata.
  • Activate → authz/key resolve/decrypt → plugin load.
  • Rotate/deactivate → revoke publication → unload → wipe.
  • Status/audit показывают identity/state без secret.

Отказ и восстановление

  • Key unavailable → no decrypt/no partial activation.
  • Rotation crash → old либо new generation remains valid, не half reference.
  • Timeout during use follows explicit grace/force policy.

Основные возможности

  • Protected plugin registry and activation lifecycle.
  • AES-GCM-backed protected material and key rotation.
  • Self-protection/timeout/destruct controls integrate POE.
Архитектурные детали и инварианты

Назначение

Аудит операций DRM/KMS: MODESET, GEM_OPEN, GEM_MMAP, PRIME, AUTH. Используется

для обнаружения скрытых GPU-пассировок и несанкционированных режимов экрана.

Реализация

Чтение состояния через /sys/kernel/debug/dri/<N>/state (debugfs, read-only).

Разбор текстового вывода: обнаружение ключевых слов crtc[, gem, prime.

Статический буфер s_dbg_buf[8192] (§SEC-3).

Управление и диагностика

Корневые команды: drm. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / drm →
Состав подсистемы / 10 файлов
Файл / компонентНазначение и граница
src/drm/drm.hDRM-protected plugin subsystem. In-place image vault for INTERNAL plugins and the main-binary stamp (`platx-self`, one exclusive page — not an on-disk ELF rewrite): INACTIVE — live mmap wiped + PROT_NONE; ciphertext is AES-256-GCM (optional POE XOR layers 1–7 applied before seal).
src/drm/drm_audit.cМониторинг через /sys/kernel/debug/dri//state (debugfs) — read-only.
src/drm/drm_cmd.cCLI-команды DRM-подсистемы. drm protect [--level=cfg|vm|data|code|anti|runtime|ai|crypto|all] drm activate drm deactivate drm status drm info drm rotate-key drm self-destruct drm set-timeout
src/drm/drm_crypto.cAES-256-GCM seal/unseal for the plugin image vault.
src/drm/drm_manager.cреестр защищённых плагинов + управление состояниями. Хранит до DRM_MAX_PLUGINS защищённых плагинов. Регистрируется как подсистема "drm" через subsys_ops_t. Перехватывает dispatch-вызовы через trampoline-таблицу. Состояния: INACTIVE (зашифрован) ↔ ACTIVE (расшифрован).
src/drm/drm_obfuscate.cобёртка над POEngine-трансформациями. Тонкий слой, вызывающий poe_obfuscate / poe_deobfuscate из poe.h. Добавляет логирование и проверки.
src/drm/drm_protect.cDRM protect/activate/deactivate with full rollback. STAB-114: Every error path rolls back to a consistent state. If wipe (a) snapshot error -> no shutdown yet; return, no rollback needed (b) seal error -> no shutdown yet; free snap_buf, return (c) wipe error -> shutdown WAS called; rollback includes load_state
src/drm/drm_self.cfirst-slice protection of the main binary (not modules). What is protected: one exclusive page `.platx_self` inside platx/memfd, sealed with the existing DRM vault (AES-256-GCM) after the crypto wave starts. The on-disk ELF is never rewritten (no first-boot chicken-egg).
src/drm/drm_vault.ccapture VMA prot, wipe live pages, restore them.
src/drm/drm_vault.hin-place wipe / restore of a plugin mmap.
Контракты API / 0 заголовков

Отдельный заголовок из include/platx/ не закреплён за этим доменом в справочнике. Внутренние границы перечислены в составе файлов; управление и публичные связи описаны в соседних подсистемах.

09

dsl

Декларативные манифесты, размещение и управление конфигурацией
src/dsl/Исполнение и расширения12 файлов3 API headers

DSL описывает желаемое состояние deployment и превращает декларацию в проверенный transaction plan. Он отвечает за parse/model/validate/plan/apply/rollback, но делегирует фактические lifecycle, transport, security и storage операции их capabilities.

Граница ответственности

  • Grammar bounded и версионирована; неизвестные mandatory fields отклоняются.
  • DSL executor не вызывает subsystem private hooks.
  • Secrets представлены references, не literal values в model/log.

Устройство подсистемы

  • Parser создаёт source-located AST; semantic model нормализует components/dependencies/recovery/isolation/config.
  • Validator разрешает descriptors/services/version/profile и строит graph intents.
  • Planner генерирует ordered operations и rollback compensation с idempotency keys.
  • Executor вызывает typed platform operations, сохраняет correlation и outcome каждого шага.

Поток работы

  • Text/signature → parse AST.
  • Normalize/validate → desired graph.
  • Dry-run plan → operator approval/policy.
  • Apply transaction → lifecycle/capabilities → commit or rollback.

Отказ и восстановление

  • Syntax/semantic errors не делают side effects.
  • Failure шага запускает compensations только для committed steps.
  • Concurrent apply одного target serializes/idempotency rejects.

Основные возможности

  • Parses a constrained YAML-like grammar into manifest model.
  • Lifecycle executor applies deploy/undeploy/reload transactions.
  • Supports validation/signing and operational diagnostics.
Архитектурные детали и инварианты

Overview

Stack-based bytecode VM for evaluating security rules at runtime. Rules are compiled to bytecode before deployment (INV-DSL-01). Stack overflow disables the rule rather than crashing (INV-DSL-02).

Invariants

INV-DSL-01 | Undefined variable → compile-time error, never runtime

INV-DSL-02 | VM stack overflow (>64 deep) → rule disabled, no crash

Components

dsl_vm.c | Stack-based bytecode VM; OP_PUSH/POP/CMP/AND/OR/NOT/JMP/HALT

dsl_route.c | Route rules: route TRAFFIC if CONDITION to TARGET

dsl_sensor.c | Sensor rules: sensor FIELD > THRESHOLD → ACTION

dsl_fs.c | Filesystem watch: watch PATH → ACTION

Static Dimensions (§SEC-3)

- Max rules: 128 (g_rules BSS)

- Max instructions per rule: 256

Управление и диагностика

Корневые команды: dsl, deploy. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / dsl →
Состав подсистемы / 12 файлов
Файл / компонентНазначение и граница
src/dsl/cmd_dsl.cnamespace "dsl": управление жизненным циклом манифестов. Полностью: deploy/validate/list/status/undeploy/stop/start/restart/reload/ set/get/export/import/logs
src/dsl/dsl.hдекларативный манифест оркестрации. YAML → дерево → схема/граф → {{ }} → deploy → dispatch → registry Невалидный манифест, цикл depends_on, висячая зависимость, неразрешённая {{ }} или сбой обязательного секрета — деплой не поднимается. Частично применённого экземпляра в реестре нет. Неизвестный command не рвёт сессию:
src/dsl/dsl_api.cреализация C-API поверх ядра dsl.h.
src/dsl/dsl_api.hстабильный C-API для интеграции DSL с внешними системами. Тонкая обёртка над dsl.h: внешний C-API не тянет внутренности движка.
src/dsl/dsl_exec.cисполнитель манифеста, вычисление condition, реестр деплойментов. Исполнение шага (решение пользователя — «через диспетчер платформы»): - подставляем {{ }} в параметры; - строим командную строку " k=v ..."; - если namespace зарегистрирован в консоли (cmd_exists —
src/dsl/dsl_fs.cfilesystem watch DSL: `watch PATH → ACTION`
src/dsl/dsl_io.hthe DSL tree's one door for typed I/O. Private to src/dsl. Deliberately NOT in dsl.h: that header is included from outside this tree (script binds dsl.* through it), and a door belongs to the tree that owns the descriptors, not to everyone who can name it.
src/dsl/dsl_manifest.cмодель манифеста поверх YAML-дерева: загрузка, валидация, подстановки {{ }}, топологическая сортировка steps.
src/dsl/dsl_route.cРеализация dsl / route
src/dsl/dsl_sensor.csensor DSL: `sensor FIELD > THRESHOLD → ACTION`
src/dsl/dsl_vm.cРеализация dsl / vm
src/dsl/dsl_yaml.cпарсер подмножества YAML (по отступам) → дерево dsl_node_t. - вложенные map по отступам: key: value / key:\n - последовательности: - scalar / - key: val (+ deeper keys) - flow-последовательности: [a, "b", c] - блочные скаляры: key: | (+ более отступленные строки)
Контракты API / 3 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/dsl_fs_action.h
/* platx/dsl_fs_action.h — DSL typed FS actions with rollback (INT-098).
 * A micro-transaction over VFS/FUSE/FSX operations for DSL scripts.
 * MAX 8 ops per transaction.
 */
#define PLAT_DSL_FS_TXN_MAX_OPS  8
#define PLAT_DSL_FS_REASON       128
#define PLAT_DSL_FS_PATH_MAX     256

typedef enum {
    PLAT_DSL_FS_OP_VFS_PUT      = 1,
    PLAT_DSL_FS_OP_VFS_REMOVE   = 2,
    PLAT_DSL_FS_OP_FUSE_MOUNT   = 3,
    PLAT_DSL_FS_OP_FUSE_UMOUNT  = 4,
    PLAT_DSL_FS_OP_FSX_MOUNT    = 5,
    PLAT_DSL_FS_OP_FSX_UNMOUNT  = 6,
} plat_dsl_fs_op_kind_t;

typedef struct {
    plat_dsl_fs_op_kind_t kind;
    char arg0[PLAT_DSL_FS_PATH_MAX];   /* path / mountpoint / session */
    char arg1[PLAT_DSL_FS_PATH_MAX];   /* source / id */
    uint32_t flags;
    /* snapshot for rollback */
    int  done;
    char undo_id[128];   /* mount_id / vfs_path for undo */
} plat_dsl_fs_op_t;

typedef struct {
    plat_dsl_fs_op_t ops[PLAT_DSL_FS_TXN_MAX_OPS];
    int  n_ops;
    int  committed;
    char reason[PLAT_DSL_FS_REASON];
} plat_dsl_fs_txn_t;

int plat_dsl_fs_txn_begin (plat_dsl_fs_txn_t *txn);
int plat_dsl_fs_txn_add   (plat_dsl_fs_txn_t *txn, const plat_dsl_fs_op_t *op);
int plat_dsl_fs_txn_commit(plat_dsl_fs_txn_t *txn);
/* Undo all committed ops in reverse order. */
int plat_dsl_fs_txn_rollback(plat_dsl_fs_txn_t *txn);
include/platx/dsl_route_txn.h
/* platx/dsl_route_txn.h — DSL typed config transaction for routes/limits (INT-107). */
#define PLAT_ROUTE_TXN_MAX  8
#define PLAT_ROUTE_KEY_MAX  64
#define PLAT_ROUTE_VAL_MAX  128

typedef enum {
    PLAT_ROUTE_OP_SET    = 1,
    PLAT_ROUTE_OP_DELETE = 2,
    PLAT_ROUTE_OP_LIMIT  = 3,
} plat_route_op_kind_t;

typedef struct {
    plat_route_op_kind_t kind;
    char  key[PLAT_ROUTE_KEY_MAX];
    char  value[PLAT_ROUTE_VAL_MAX];
    int   done;
} plat_route_op_t;

typedef struct {
    plat_route_op_t ops[PLAT_ROUTE_TXN_MAX];
    int  n_ops;
    int  committed;
    char reason[128];
} plat_route_txn_t;

int plat_route_txn_begin   (plat_route_txn_t *txn);
int plat_route_txn_add     (plat_route_txn_t *txn, const plat_route_op_t *op);
int plat_route_txn_commit  (plat_route_txn_t *txn);
int plat_route_txn_rollback(plat_route_txn_t *txn);
include/platx/dsl_sensor_txn.h
/* platx/dsl_sensor_txn.h — INT-129: DSL sensor profile transaction with rollback. */

#define PLAT_SENSOR_TXN_OPS_MAX  8
#define PLAT_SENSOR_NAME_MAX    64
#define PLAT_SENSOR_VALUE_MAX  128
typedef enum {
    PLAT_SENSOR_OP_ENABLE    = 1,
    PLAT_SENSOR_OP_DISABLE   = 2,
    PLAT_SENSOR_OP_CONFIGURE = 3
} plat_sensor_op_kind_t;
typedef struct {
    plat_sensor_op_kind_t kind;
    char                  sensor_name[PLAT_SENSOR_NAME_MAX];
    char                  value[PLAT_SENSOR_VALUE_MAX];
    int                   done;
    char                  prev_value[PLAT_SENSOR_VALUE_MAX];
} plat_sensor_op_t;
typedef struct {
    plat_sensor_op_t ops[PLAT_SENSOR_TXN_OPS_MAX];
    int              n_ops;
    int              committed;
    char             reason[128];
} plat_sensor_txn_t;
void plat_sensor_txn_begin(plat_sensor_txn_t *txn);
int  plat_sensor_txn_add(plat_sensor_txn_t *txn, const plat_sensor_op_t *op);
int  plat_sensor_txn_commit(plat_sensor_txn_t *txn);
int  plat_sensor_txn_rollback(plat_sensor_txn_t *txn);
10

ebpf

eBPF, HADES, сенсоры и провайдеры наблюдения ядра
src/ebpf/Наблюдение и исследование135 файлов11 API headers

eBPF domain управляет userspace feature probe/load/attach/map/events для BPF-side programs. Он отделяет kernel-dependent sensors/transports от Core и предоставляет проверяемые adapters без обязательной libbpf зависимости базовых профилей.

Граница ответственности

  • BPF programs живут отдельно от userspace source lists.
  • Covert/research transports никогда не становятся default transport.

Устройство подсистемы

  • Feature probe фиксирует kernel/BTF/syscall/program/map/helper/privilege support.
  • Loader backend: raw bpf syscall для простых объектов; optional libbpf/tool для ELF/CO-RE до собственного verifier.
  • Object owner scope отслеживает program/map/link/ring fds и pin paths.
  • Event bridge валидирует record version/size и применяет backpressure/drop metrics.

Поток работы

  • Profile requests sensor/program.
  • Probe → verify object/requirements → load maps/program.
  • Attach link → health READY → events.
  • Stop/failure → detach links → close/unpin owned objects.

Отказ и восстановление

  • Нет vmlinux.h/BTF → explicit unsupported, без fake capability.
  • Verifier/load/attach partial failure rollback всех fds/pins.
  • Ring flood bounded, critical control path не блокируется.
  • Kernel upgrade invalidates compatibility and requires reload/reprobe.

Основные возможности

  • Load/attach/introspect BPF programs and maps.
  • Feature probing, BTF/tracing/LSM/XDP support and FSX integration.
  • Research transports over ARP/ICMP/TCP options/VLAN/QUIC/DNS/DoH and others.
Архитектурные детали и инварианты

Событие: строгий 72-байтный контракт

hades_event_t (ebpf/hades/hades_event_schema.h) — единственная бинарная форма

на границе ядро↔userspace: ровно 72 байта (_Static_assert), stdint-only, без

строк-длин, без JSON, без указателей. Отличать от plat_hades_event_t (ядро,

INT-124) — толстая audit/MBus-запись со строками и JSON; на проводе не использовать.

Codec little-endian, чистый, ничего не аллоцирует. Полная спека —

docs/specs/hades_event_schema.md.

Threat scoring (детерминированно)

PTRACE→TL3, KILL→TL2, CONNECT/EXEC/FORK→TL1, FILE_OPEN→TL0; HADES_EVF_LSM_DENY

поднимает уровень на 1 (cap TL4). Никакой модели/LLM в этом пути (канон C25).

Управление и диагностика

Корневые команды: ebpf, bpf, hades. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / ebpf →
Состав подсистемы / 135 файлов
Файл / компонентНазначение и граница
src/ebpf/cmd_ebpf.cEBPFMonitor: загрузка, подключение и мониторинг eBPF-программ. Namespace: "ebpf" (псевдоним "bpf"). Подсистема работает НАПРЯМУЮ через системный вызов bpf(2), perf_event_open(2) и Netlink — без libbpf и bpftool. • собственный минимальный ELF-загрузчик BPF-объектов (секции, maps,
src/ebpf/covert_raw.cuserspace raw-socket fallbacks for 8 covert transports. When XDP/TC programs are not loaded (missing CAP_BPF, old kernel, or absent BPF objects) each transport falls back to a plain socket implementation. All state is file-static; one instance per transport.
src/ebpf/covert_raw.hraw-socket fallback API for the 10 eBPF covert transports. Used by the RA2C eBPF transport provider when XDP/TC programs are not loaded or when CAP_BPF / CAP_NET_ADMIN are unavailable. Each transport degrades gracefully to a raw-socket userspace implementation.
src/ebpf/ebpf_fsx.cEBPFMonitor listens to FSX filesystem events on mbus. Correlation only: PID + FS path + process name. No hiding, no C2.
src/ebpf/ebpf_fsx.hEBPFMonitor subscription to FSX EventBus topics.
src/ebpf/ebpf_loader.cЗагружает .bpf.o объекты через bpf(2) syscall напрямую.
src/ebpf/ebpf_loader.hОбъявления типов и интерфейсов
src/ebpf/ebpf_probe.ceBPF capability detection and feature probing.
src/ebpf/ebpf_version.cРеализация ebpf / version
src/ebpf/ebpf_version.hОбъявления типов и интерфейсов
src/ebpf/ebpf_xdp.cминимальный XDP-инструментарий поверх bpf() (без libbpf). Собирает XDP-программу-счётчик из массива struct bpf_insn прямо в C, создаёт ARRAY-map, загружает программу и прикрепляет к интерфейсу через helper'ов (иначе верификатор аннулирует указатели на пакет).
src/ebpf/ebpf_xdp.hминимальный переиспользуемый XDP-инструментарий поверх bpf(). Общий слой для учебных транспортных eBPF-модулей (icmp_ebpf, dns_ebpf). Не зависит от libbpf/clang: программы собираются как массивы struct bpf_insn прямо в C и загружаются syscall'ом bpf() — как в EBPFMonitor (cmd_ebpf.c).
src/ebpf/ebpf_xio.hlazy XIO accessors for the top-level eBPF CLI/transport. File I/O via ebpf_file_io(); sockets via ebpf_net_io(); process via ebpf_proc_io(). fopen идёт через open+fdopen, чтобы fd оставался на vtable. Нет libc fallback.
src/ebpf/hades/btf/btf_compat.hдоопределение видов BTF, отсутствующих в старых сравнительно недавно. На машинах с более старым пакетом и сборка btf_parser.c/btf_resolver.c падает с error: 'BTF_KIND_ENUM64' undeclared error: invalid application of 'sizeof' to incomplete type 'struct btf_enum64'
src/ebpf/hades/btf/btf_gen.cРеализация btf / gen
src/ebpf/hades/btf/btf_parser.cРеализация btf / parser
src/ebpf/hades/btf/btf_resolver.cРеализация btf / resolver
src/ebpf/hades/c2_detection/c2_behavior.cРеализация c2 / behavior
src/ebpf/hades/c2_detection/c2_dga.cРеализация c2 / dga
src/ebpf/hades/c2_detection/c2_dns.cDNS tunnelling and Domain Generation Algorithm detection Two independent detection surfaces: 1. Tunnel detection — per-query heuristics (entropy, length, charset, subdomain depth) and per-session volume tracking. 2. DGA scoring — n-gram frequency analysis against an English bigram
src/ebpf/hades/c2_detection/c2_http.cРеализация c2 / http
src/ebpf/hades/c2_detection/c2_ioc.cРеализация c2 / ioc
src/ebpf/hades/c2_detection/c2_malware.cРеализация c2 / malware
src/ebpf/hades/c2_detection/c2_mitre.cРеализация c2 / mitre
src/ebpf/hades/c2_detection/c2_network.cРеализация c2 / network
src/ebpf/hades/c2_detection/c2_signatures.cC2 framework fingerprinting via JA3 and beacon timing Identifies known C2 frameworks by matching TLS ClientHello JA3 hashes and by analysing the statistical regularity of connection timing (beacon detection via Chi-squared test on inter-arrival intervals).
src/ebpf/hades/c2_detection/c2_tls.cTLS fingerprinting (JA3) and SNI/ALPN analysis 1. JA3 computation — MD5 of the normalised ClientHello parameters. 2. SNI heuristics — DGA pattern, IP-as-SNI, known C2 domains. 3. ALPN heuristics — non-standard protocol values used by implants. 4. Global TLS statistics.
src/ebpf/hades/c2_detection/c2_tunnels.cРеализация c2 / tunnels
src/ebpf/hades/cmd/cmd_btf.cРеализация cmd / btf
src/ebpf/hades/cmd/cmd_core.cРеализация cmd / core
src/ebpf/hades/cmd/cmd_filters.cРеализация cmd / filters
src/ebpf/hades/cmd/cmd_firewall.cРеализация cmd / firewall
src/ebpf/hades/cmd/cmd_hades.cкоманда «hades» в консоли платформы. Файл, которого не было. `src/core/cmd_autoreg.h` ищет его через __has_include и, не найдя, разворачивает REG_hades() в пустоту — поэтому в platx не было даже команды hades, хотя весь userspace Hades лежал в дереве (W2-F005). Отсутствие одного файла делало невидимым целый слой, и заметить
src/ebpf/hades/cmd/cmd_hades.hточка входа CLI Hades для платформы. Два потребителя, одна функция: - продукт: cmd_hades.c регистрирует команду «hades» в консоли платформы; - отдельная утилита: cmd_core.c собирается с -DHADES_CLI_MAIN и получает main(), который зовёт то же самое.
src/ebpf/hades/cmd/cmd_lsm.cРеализация cmd / lsm
src/ebpf/hades/cmd/cmd_research.cРеализация cmd / research
src/ebpf/hades/cmd/cmd_sensors.cРеализация cmd / sensors
src/ebpf/hades/cmd/cmd_waf.cРеализация cmd / waf
src/ebpf/hades/container/container_detect.cContainer and Kubernetes runtime detection Detects which container runtime (if any) is hosting a given PID by inspecting /proc//cgroup and /proc//mounts. Results are cached in a circular LRU of 256 entries. Supported runtimes: Docker, containerd (k8s.io), Podman, LXC, Kata Containers,
src/ebpf/hades/container/container_ns.cNamespace monitoring and container lifecycle tracking. Works alongside the BPF sensors to provide full container context. Polls /proc//ns/* symlinks every 500 ms to detect namespace changes not caught by BPF, and uses inotify on /proc to detect new processes efficiently.
src/ebpf/hades/container/k8s_detect.cРеализация k8s / detect
src/ebpf/hades/core/ebpf_api.cC-API implementation, subsystem registration, REST router. - All ebpf_if_t function pointers (via manager_ / fw_ / waf_ functions) - subsys_ops_t registration with the PLATX platform subsystem registry - Minimal HTTP-style REST router for /api/ebpf/... endpoints
src/ebpf/hades/core/ebpf_bpf_fs.cРеализация ebpf / bpf / fs
src/ebpf/hades/core/ebpf_compat.cРеализация ebpf / compat
src/ebpf/hades/core/ebpf_config.cRuntime configuration management for the hades eBPF subsystem. Manages per-sensor and global configuration, persisted as JSON to /var/lib/hades/config.json. Uses a hand-rolled recursive-descent JSON parser so there is no dependency on cJSON or any other external library.
src/ebpf/hades/core/ebpf_config.hPublic API for hades runtime configuration.
src/ebpf/hades/core/ebpf_dispatcher.cRing-buffer dispatcher: reads BPF events, formats JSON, publishes to the mbus event bus. A single Core-owned background task watches all sensor ring buffers via epoll. Each ring buffer is mmap'd according to the Linux BPF ring-buffer layout. For every event record the dispatcher calls dispatch_event(), which formats
src/ebpf/hades/core/ebpf_jit.cРеализация ebpf / jit
src/ebpf/hades/core/ebpf_loader.cРеализация ebpf / loader
src/ebpf/hades/core/ebpf_manager.cSensor lifecycle manager. Responsible for loading, attaching, and unloading BPF sensor objects. metrics output consumed by the REST API and subsystem registry. BPF object loading uses raw bpf(2) syscalls so that the manager has no dependency on libbpf. Map fds are obtained via BPF_OBJ_GET for maps that
src/ebpf/hades/core/ebpf_map.cРеализация ebpf / map
src/ebpf/hades/core/r0r3_ringbuf.cRing0→Ring3 userspace transport reader. Реализация без libbpf: используем raw bpf(2) syscall + mmap + epoll. Это учебная часть: показывает, как ядро экспортирует события через BPF_MAP_TYPE_RINGBUF без каких-либо внешних библиотек. [consumer page] mmap offset 0 → u64 consumer_pos
src/ebpf/hades/core/r0r3_ringbuf.hpublic API for the Ring0→Ring3 transport layer
src/ebpf/hades/filters/filter_anomaly.cStatistical anomaly detection via per-process baselines Uses Welford's online algorithm for numerically stable mean/variance tracking without requiring historical windows in memory. Anomalies are flagged when a new observation's z-score exceeds a caller-supplied
src/ebpf/hades/filters/filter_correlation.cEvent correlation and behavioral chain detection - Open-addressing hash table of proc_timeline_t keyed by PID. Table size: CORR_HT_SIZE (power of two, currently 512). - corr_feed() inserts events into the owning PID's circular buffer. - corr_check() iterates every occupied slot and for each timeline
src/ebpf/hades/filters/filter_correlation.hEvent correlation and behavioral chain detection Maintains a per-process event timeline and matches multi-step attack sequences (TTPs) expressed as ordered pattern steps with timing constraints. Typical call sequence: // periodic (e.g. every 1 s): correlation_match_t matches[8];
src/ebpf/hades/filters/filter_exclusion.cРеализация filter / exclusion
src/ebpf/hades/filters/filter_signatures.cDetection signature engine implementation 15 detection rules covering network C2, process abuse, file manipulation, and container escape TTPs.
src/ebpf/hades/filters/filter_signatures.hDetection signature engine Defines the public API for hades signature-based event detection. Consumers call sigs_init() once, then sigs_check() per event.
src/ebpf/hades/filters/filter_whitelist.cРеализация filter / whitelist
src/ebpf/hades/firewall/fw_geoip.cРеализация fw / geoip
src/ebpf/hades/firewall/fw_geoip.hGeoIP-политика файрвола: интерфейс для потребителей. `geoip_policy_t` здесь НЕ объявлен намеренно. Он определён внутри fw_geoip.c, а рабочий экземпляр — статический: собрать его снаружи нельзя. CLI это и не мог: он звал geoip_policy_add_cc(NULL, cc), и от разыменования
src/ebpf/hades/firewall/fw_ips.c── BPF syscall wrappers ────────────────────────────────────────────────────
src/ebpf/hades/firewall/fw_l4.cРеализация fw / l4
src/ebpf/hades/firewall/fw_l4.hправила L3/L4 файрвола: типы и интерфейс. ЕДИНСТВЕННОЕ объявление. До этого заголовка типы жили в двух местах: в fw_l4.c и своей копией в cmd/cmd_firewall.c. Копии совпадали поле в поле — но их совпадение не проверялось ничем, а рядом, в том же слое, точно такая
src/ebpf/hades/firewall/fw_rules.c── BPF syscall wrappers ────────────────────────────────────────────────────
src/ebpf/hades/firewall/fw_rules.hUserspace firewall rule types and text-format parser Rule text format (key=value pairs, space-separated): proto=tcp|udp|icmp|any sport= source port (optional) dport= destination port (optional) action=allow|drop|reject
src/ebpf/hades/firewall/fw_stats.cРеализация fw / stats
src/ebpf/hades/firewall/fw_stats.hвывод счётчиков файрвола. Объявления вынесены сюда, чтобы CLI не держал их своей копией: копия молча расходится, а компоновщик о ней не знает (W2-F007).
src/ebpf/hades/firewall/geoip_loader.c── BPF syscall wrapper ─────────────────────────────────────────────────────
src/ebpf/hades/fuzz/fuzz_btf.cAFL++/libFuzzer harness for the BTF/CO-RE parser. afl-clang-fast -fsanitize=address,undefined \ -o fuzz/fuzz_btf fuzz/fuzz_btf.c tools/btf_core.c \ -I tools/ -lelf -lz fuzz/corpus_btf/ — copy /sys/kernel/btf/vmlinux there if available: cp /sys/kernel/btf/vmlinux fuzz/corpus_btf/vmlinux.seed
src/ebpf/hades/fuzz/fuzz_dns.cAFL++/libFuzzer harness for the DNS wire-format parser. Exercises dns_parse_query() from c2_detection/c2_dns.c and the underlying label-chasing logic. The harness specifically targets: - Pointer loops (compression pointer cycles) - Malformed label lengths (> 63 without pointer bit set)
src/ebpf/hades/fuzz/fuzz_waf.cAFL++/libFuzzer harness for all WAF detection modules. afl-clang-fast -fsanitize=address,undefined \ -o fuzz/fuzz_waf fuzz/fuzz_waf.c \ waf/waf_sqli.c waf/waf_xss.c waf/waf_path_traversal.c \ waf/waf_cmd_injection.c waf/waf_csrf.c \ waf/waf_ratelimit.c waf/waf_ua.c \
src/ebpf/hades/hades_mod.cthin platx door for the hades sensor domain. Invariant: this file does not load BPF, does not create threads, and is not a supervisor. Restart is plat_recovery_choose/apply → plat_lifecycle_request only — and only after start owns sensors. No vmlinux / no load → optional door, idle OK or DEGRADED, process up.
src/ebpf/hades/hades_mod.hsensors.hades as a platx module door. Not a supervisor. Hades is a domain, not Core: not the working path, not C2, not an XIO backend. BPF load/attach stays in the hades tree. This file is the edge.
src/ebpf/hades/hades_optional.hcompile-time and runtime gate for the Hades eBPF plane. The main product build never includes this file. It is included only by optional Hades targets, smoke tests, and any future HAD1–HAD6 tasks once those are explicitly started by the integrator. This header does NOT include platx/module.h or platx/recovery.h. Those
src/ebpf/hades/hooks/hook_fentry.cРеализация hook / fentry
src/ebpf/hades/hooks/kprobe_tcp.cРеализация kprobe / tcp
src/ebpf/hades/hooks/tracepoint_sched.cРеализация tracepoint / sched
src/ebpf/hades/hooks/uprobe_ssl.cРеализация uprobe / ssl
src/ebpf/hades/include/core/r0r3_ringbuf.hpublic API for the Ring0→Ring3 transport layer
src/ebpf/hades/include/ebpf.hОбъявления типов и интерфейсов
src/ebpf/hades/include/ebpf_btf.hОбъявления типов и интерфейсов
src/ebpf/hades/include/ebpf_compat.hОбъявления типов и интерфейсов
src/ebpf/hades/include/ebpf_container.hОбъявления типов и интерфейсов
src/ebpf/hades/include/ebpf_firewall.hОбъявления типов и интерфейсов
src/ebpf/hades/include/ebpf_loader.hОбъявления типов и интерфейсов
src/ebpf/hades/include/ebpf_lsm.hОбъявления типов и интерфейсов
src/ebpf/hades/include/ebpf_platform.hEBPFMonitor public API + platform integration types Defines the ebpf_if_t C-API surface, sensor lifecycle types, mbus topic constants, and minimal platform stubs when the PLATX SDK is not present.
src/ebpf/hades/include/ebpf_research.hОбъявления типов и интерфейсов
src/ebpf/hades/include/ebpf_sensor.hОбъявления типов и интерфейсов
src/ebpf/hades/include/ebpf_tls_multi.hОбъявления типов и интерфейсов
src/ebpf/hades/include/ebpf_waf.hОбъявления типов и интерфейсов
src/ebpf/hades/include/hades.hОбъявления типов и интерфейсов
src/ebpf/hades/include/hades_xio.hlazy XIO accessors for the hades eBPF agent. File/mmap/dir I/O via hades_file_io(); sockets via hades_net_io(); fork/waitpid/kill via hades_proc_io(). fopen идёт через open+fdopen, чтобы fd оставался на vtable. Нет libc fallback.
src/ebpf/hades/include/vmlinux.hОбъявления типов и интерфейсов
src/ebpf/hades/maps/map_hash.cРеализация map / hash
src/ebpf/hades/maps/map_percpu.cРеализация map / percpu
src/ebpf/hades/maps/map_queue.cРеализация map / queue
src/ebpf/hades/maps/map_ringbuf.cРеализация map / ringbuf
src/ebpf/hades/platform/ebpf_alert_bus.cРеализация ebpf / alert / bus
src/ebpf/hades/platform/ebpf_alert_bus.hОбъявления типов и интерфейсов
src/ebpf/hades/platform/ebpf_lkm_fallback.cРеализация ebpf / lkm / fallback
src/ebpf/hades/platform/ebpf_platx.cРеализация ebpf / platx
src/ebpf/hades/sensors/hades_sensors.cHAD0: sensor registry and loader wiring. Called from hades_mod.c only. - probe() never loads a BPF object; safe to call any time. - Each owned sensor has an exclusive ebpf_loader_ctx_t. - fini() releases every fd unconditionally; idempotent. - STREAM sensors (TTY / keyring / pam) excluded.
src/ebpf/hades/sensors/hades_sensors.hHAD0: sensor registry and loader interface. Sole interface between hades_mod.c and the BPF sensor subsystem. hades_mod.c calls hades_sensors_init() in try_own and hades_sensors_fini() in stop/destroy. No platx/module.h. No Core dependency. No STREAM sensors.
src/ebpf/hades/sensors/sensor_file/sensor_file.bpf.cHAD2: VFS file access sensor. tracepoint/syscalls/sys_enter_unlinkat — unlink (stable syscall ABI) Using the syscall tracepoint for unlink avoids vfs_unlink signature path_filter map: inode -> 1 suppresses high-noise paths (e.g., /proc reads by the monitoring daemon itself). Populated from userspace.
src/ebpf/hades/sensors/sensor_network/sensor_network.bpf.cHAD3: network connection sensor. Both IPv4 (AF_INET=2) and IPv6 (AF_INET6=10) handled. AF_UNIX excluded (not a network hop). tcp_conns map: sock* -> owner_pid, tracks open connections. W3-33: the close hook is now present. Without it every connect and accept added a key that nothing ever removed, so after 8192 live
src/ebpf/hades/sensors/sensor_payloads.hполезная нагрузка трёх штатных сенсоров, объявленная До этого каждая .bpf.c описывала свою структуру у себя. Пока читатель был только один — сам сенсор, — это ничего не стоило. Волна 2 добавляет потребителя (адаптер к sense_provider_v1), и два описания одного и того же
src/ebpf/hades/sensors/sensor_process/sensor_process.bpf.cHAD1: process lifecycle sensor. sched_process_fork — new process created (copy_process done) sched_process_exec — execve completed successfully sched_process_exit — process / thread about to exit Events written to the "events" ring buffer (sensors_common.h macro).
src/ebpf/hades/sensors/sensors_common.hобщие типы и вспомогательные макросы для всех standalone BPF-сенсоров. Каждый сенсор компилируется самостоятельно, загружается через ebpf-load и выводит события в ring buffer с именем "events". Формат события: [sensor_event_hdr][payload]
src/ebpf/hades/system/system_audit.cLinux audit subsystem bridge for hades. Opens an AF_NETLINK/NETLINK_AUDIT socket, receives kernel audit records, parses their key=value payload, and publishes structured events to the hades message bus under the topic "hades/audit/event". Audit record types handled:
src/ebpf/hades/system/system_user.c/etc user account and privilege change monitor for hades. Uses inotify to watch critical identity and privilege configuration files for modifications. On each IN_MODIFY or IN_CREATE event the affected file is re-read, diffed against a cached snapshot, and changed lines are
src/ebpf/hades/tests/test_ringbuf.cUnit tests for the BPF ring-buffer consumer protocol Tests the drain logic that ebpf_dispatcher.c uses, running entirely in userspace with heap-allocated fake ring buffers. No BPF syscalls, no root. gcc -O2 -std=c11 -Wall -Wextra -D_GNU_SOURCE \ -o build/test_ringbuf tests/test_ringbuf.c
src/ebpf/hades/tools/agent1_hades_helper.c- A1-005 live HADES sensor runner (libbpf) Build: gcc -O2 -std=c11 -Wall -D_GNU_SOURCE -I/usr/include/bpf \ -o agent1_hades_helper agent1_hades_helper.c -lbpf -lpthread
src/ebpf/hades/tools/agent1_sp_lsm_live.cA1-009: live BPF LSM deny/audit/unload evidence.
src/ebpf/hades/tools/btf_core.cBTF-парсер + CO-RE движок. Полностью самостоятельный модуль: только libc, Linux uapi.
src/ebpf/hades/tools/btf_core.hМинимальный BTF-парсер + CO-RE движок. Compile Once – Run Everywhere: при загрузке BPF-объекта читает секции .BTF и .BTF.ext, находит CO-RE-релокации и применяет их к массиву инструкций — патчит смещения полей и размеры типов относительно BTF работающего ядра (/sys/kernel/btf/vmlinux).
src/ebpf/hades/tools/cmd_ebpf.cEBPFMonitor: загрузка, подключение и мониторинг eBPF-программ. Namespace: "ebpf" (псевдоним "bpf"). Подсистема работает НАПРЯМУЮ через системный вызов bpf(2), perf_event_open(2) и Netlink — без libbpf и bpftool. • собственный минимальный ELF-загрузчик BPF-объектов (секции, maps,
src/ebpf/hades/tools/ebpf_load.cebpf-load — универсальный консольный загрузчик eBPF-объектов. Автоматически определяет тип программы из SEC()-аннотации и подключает kprobe / kretprobe / kprobe.multi / kretprobe.multi uprobe / uretprobe / uprobe.multi / uretprobe.multi tracepoint / raw_tracepoint / raw_tp
src/ebpf/hades/tools/gen_vmlinux.cРеализация gen / vmlinux
src/ebpf/hades/tools/hades_ctl.cControl-plane CLI for the hades security daemon. Communicates with hadesд via a Unix-domain socket at /var/run/hades/ctl.sock using a simple JSON-RPC protocol. Falls back to direct BPF map access via /sys/fs/bpf/hades/ when the daemon is not running. gcc -O2 -Wall -Wextra -o hades_ctl tools/hades_ctl.c -lbpf
src/ebpf/hades/tools/test_firewall.cUnit tests for userspace firewall and WAF components 1. fw_rule_parse — full rule (TCP src/dst/dport/action) 2. fw_rule_parse — partial rule (UDP, no dst/port) 3. fw_rule_parse — invalid input → returns -1 4. ips_check_ip — no match on fresh state 5. WAF SQLi — "SELECT * FROM users WHERE id=1 OR 1=1--" → blocked
src/ebpf/hades/tools/test_loader.cTest harness for BTF loading and CO-RE metadata Uses the libbpf public API for BTF kernel loading / type inspection and a locally-defined btf_core_relo_kind_str() for CO-RE relocation kind names (mirrors the eb_core_relo_kind_name() in tools/btf_core.c).
src/ebpf/hades/tools/test_sensors.cHades sensor integration tests (no libbpf) Replaces the old test_sensors.c which depended on libbpf. Uses only raw Linux BPF syscalls (bpf(2)). 1. BPF_MAP_CREATE: BPF_MAP_TYPE_RINGBUF (needs CAP_BPF) 2. mmap ring-buffer (needs CAP_BPF)
src/ebpf/hades/tools/test_waf.cWAF fuzzing / regression tests XSS : 30 true positives, 10 false positives PathTrav: 20 true positives, 5 false positives CMDi : 20 true positives, 5 false positives Exit 1 on any miss. gcc -O2 -std=c11 -Wall \
src/ebpf/hades/waf/waf_cmd_injection.cOS/shell command injection detection for hades WAF 1. Shell metacharacter heuristic (fast pre-filter). 2. Pattern scan against known injection payloads. Compile: gcc -O2 -std=c11 -Wall -I/home/claude/hades/waf -c waf_cmd_injection.c
src/ebpf/hades/waf/waf_csrf.cCSRF protection for the hades WAF 2. Origin / Referer header validation against trusted-domain list. 3. CSRF token validation: HMAC-SHA256(session_id||timestamp) encoded as 64 lower-case hex digits; token sourced from X-CSRF-Token header or "_csrf" body parameter.
src/ebpf/hades/waf/waf_path_traversal.cPath traversal / directory traversal detection 1. Raw pattern scan (../, ..\, encoded variants). 2. Normalize the URI path and check whether it escapes the web root. Compile: gcc -O2 -std=c11 -Wall -I/home/claude/hades/waf -c waf_path_traversal.c
src/ebpf/hades/waf/waf_ratelimit.cPer-IP sliding-window rate limiter for the hades WAF • 4096-entry open-addressing hash table (power-of-two, FNV-1a hash). • No malloc in the hot path — all state is in static storage. • Per-bucket atomic_flag spinlock for thread safety. • Three independent sliding windows: per-second, per-minute, per-hour.
src/ebpf/hades/waf/waf_rules.cРеализация waf / rules
src/ebpf/hades/waf/waf_rules.hWAF rule engine types for the hades security framework Defines the core rule structures, request/result types, and public API for the in-process Web Application Firewall.
src/ebpf/hades/waf/waf_sqli.cSQL Injection detection for the hades WAF Detection strategy: 1. Raw case-insensitive pattern matching on the raw field value. 2. URL-decoded version of the field (handles %27, %3B, + etc.). 3. Both URI query string and request body are scanned. Compile: gcc -O2 -std=c11 -Wall -I/home/claude/hades/waf -c waf_sqli.c
src/ebpf/hades/waf/waf_ua.cUser-Agent analysis and blocking for the hades WAF Detection strategy: • Case-insensitive substring scan across 120 suspicious UA patterns. • Empty / absent User-Agent rejection (configurable). • Known-good bot allowlist (Googlebot, Bingbot, Slurp, DuckDuckBot, etc.)
src/ebpf/hades/waf/waf_xss.cCross-Site Scripting (XSS) detection for the hades WAF 1. Raw pattern scan (case-insensitive). 2. HTML entity decoding (&#NNN; and &#xHH;) then re-scan. 3. URL percent-decoding then re-scan. Compile: gcc -O2 -std=c11 -Wall -I/home/claude/hades/waf -c waf_xss.c
Контракты API / 11 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/ebpf_cap_loader.h
/* platx/ebpf_cap_loader.h — INT-123: Generic eBPF loader as Hades cap provider. */

#define PLAT_EBPF_CAP_NAME_MAX  64
#define PLAT_EBPF_PATH_MAX     256
#define PLAT_EBPF_REASON_MAX   128
typedef enum {
    PLAT_EBPF_OK = 0,
    PLAT_EBPF_ERR_ARGS,
    PLAT_EBPF_ERR_DENIED,
    PLAT_EBPF_ERR_LOAD,
    PLAT_EBPF_ERR_NOLINK
} plat_ebpf_status_t;
typedef struct {
    plat_ebpf_status_t status;
    int                prog_fd;
    char               reason[PLAT_EBPF_REASON_MAX];
} plat_ebpf_load_result_t;
/* lease_id=0 skips capability gate. prog_type is BPF_PROG_TYPE_* integer. */
int plat_ebpf_cap_load(const char *cap_name, const char *path,
                        int prog_type, plat_context_handle_t ctx,
                        uint64_t lease_id, plat_ebpf_load_result_t *out);
include/platx/hades_adapter.h
/* platx/hades_adapter.h — Hades sensors as sense_provider_v1 (D066-D071).
 *
 * One adapter type, three instances: process, file, network. Each one turns the
 * raw records of a Hades sensor into normalized SENSE records with a stable
 * entity identity, and does its accounting through the provider SDK so that
 * loss is a number and a gap rather than a shorter stream.
 *
 * The transport is a function pointer on purpose. On a privileged host it is a
 * ring-buffer reader; in a test it is a fixture. Everything this file is
 * responsible for — identity, normalization, sequencing, loss — is the same in
 * both cases, and is therefore testable without CAP_BPF. What genuinely needs
 * the kernel (attaching the programs, the real ring) is the live-attach card
 * set D080-D084, and it is not claimed here.
 *
 * Identity is the part worth reading twice:
 *
 *   process  boot_id : pidns : pid : start_time : exec_generation
 *            PID reuse is the normal case on a busy host, not an edge case. A
 *            key without start_time silently merges two processes; a key
 *            without exec_generation merges the program before an exec with
 *            the program after it, which is precisely the transition an
 *            attacker uses.
 *
 *   file     boot_id : mntns : dev : ino : generation
 *            The path is NOT part of the identity. A path is a claim about a
 *            file at a moment — rename, bind-mount and hardlink all break it —
 *            and it travels as a hint next to the identity, never as it.
 *
 *   socket   boot_id : netns : proto : tuple : cookie
 *            The tuple alone is reused within seconds; the cookie is what makes
 *            two connections with the same tuple two connections.
 *
 * Имена функций начинаются с prov_, а не с hades_, и это не косметика.
 * Профильный firewall (tools/release_gate.py, PROFILE_DENY) запрещает
 * поставляемому профилю server экспортировать `^hades_`: там не должно быть
 * доменного движка Hades — ни загрузчика BPF, ни модуля. Адаптер же
 * принадлежит фабрике: он переводит уже готовые записи в SENSE и никакого
 * ядра не трогает. Сначала он назывался hades_* и профиль честно покраснел на
 * тринадцати символах; правильный ответ — назвать вещи своими именами, а не
 * ослабить запрет (та же развилка, что в находке A3-F003).
 */

#define HADES_ADAPTER_ABI 1u

typedef enum hades_adapter_kind {
    HADES_ADAPT_NONE    = 0,
    HADES_ADAPT_PROCESS = 1,
    HADES_ADAPT_FILE    = 2,
    HADES_ADAPT_NETWORK = 3,
    HADES_ADAPT_MAX
} hades_adapter_kind_t;

/* ── the transport ───────────────────────────────────────────────────────
 * Pull exactly one raw record.
 *   1  a record was written into `rec`
 *   0  nothing to read AND coverage is intact (a genuinely idle sensor)
 *  -1  could not look: the caller turns this into a typed UNAVAILABLE
 * `*lost_out` reports records the transport knows it lost since the previous
 * call. Reporting loss and returning 0 in the same call is legal and normal.
 */
typedef int (*hades_pull_fn)(void *ctx, void *rec, uint32_t rec_size,
                             uint64_t *lost_out);

/* Start time of a process, in nanoseconds since boot, or 0 when unknown.
 * Injected so that identity can be tested without /proc. */
typedef uint64_t (*hades_starttime_fn)(void *ctx, uint32_t pid);

/* ── post-hoc identity resolution ────────────────────────────────────────
 * ABI v1 of the sensor records carries a path but no dev/ino, and a tuple but
 * no socket cookie. Both are resolvable from userspace after the fact, and
 * both resolutions are RACY: by the time the adapter looks, the path may point
 * at a different file and the socket may be closed.
 *
 * That race is why the result is declared SYNTHESIZED rather than DIRECT, and
 * why a failed resolution falls back to the weaker key instead of dropping the
 * record. What must never happen is the third option — resolving post-hoc and
 * presenting the result as if the sensor had reported it.
 *
 * Returning 0 means "could not resolve"; the adapter then counts it.
 */
typedef int (*hades_file_ident_fn)(void *ctx, const char *path,
                                   uint64_t *dev_out, uint64_t *ino_out);
typedef int (*hades_sock_ident_fn)(void *ctx, uint32_t proto,
                                   const uint8_t saddr[16], uint16_t sport,
                                   const uint8_t daddr[16], uint16_t dport,
                                   uint64_t *cookie_out);

typedef struct hades_adapter_cfg {
    uint32_t kind;                 /* hades_adapter_kind_t */
    const char *provider_id;       /* stable id; also the SENSE source id */
    const char *capability;        /* published capability name */

    uint64_t epoch;                /* provider epoch; 0 is refused */
    uint64_t generation;           /* provider generation; 0 is refused */
    uint64_t boot_id_hash;
    uint64_t pidns, mntns, netns;  /* the namespaces this instance observes */

    uint32_t window;               /* unacked ceiling; 0 is refused */

    hades_pull_fn      pull;
    void              *pull_ctx;
    hades_starttime_fn starttime;  /* NULL: /proc is read */
    void              *starttime_ctx;
    hades_file_ident_fn file_ident; /* NULL: stat(2) on the reported path */
    void               *file_ident_ctx;
    hades_sock_ident_fn sock_ident; /* NULL: /proc/net/{tcp,udp} lookup */
    void               *sock_ident_ctx;
} hades_adapter_cfg_t;

/* Encoding scratch. Deliberately not sense_schema.h's SENSE_RECORD_MAX
 * (65520): one normalized record from these three sensors never approaches it,
 * and a 64 KiB buffer per adapter instance would be a per-provider cost paid
 * for a case that cannot happen. A record that does not fit is refused and
 * counted, not truncated. */
#define HADES_ADAPTER_SCRATCH 2048u

/* Bounded identity table: pid → (start_time, exec_generation). Overflow is
 * counted and degrades quality; it never silently reuses a slot's identity. */
#define HADES_IDENT_SLOTS 256u

typedef struct hades_ident_slot {
    uint32_t pid;
    uint32_t exec_gen;
    uint64_t start_time;
    uint64_t last_seen_ns;
    int      in_use;
} hades_ident_slot_t;

typedef struct hades_adapter {
    hades_adapter_cfg_t cfg;
    sense_provider_v1_t vtable;
    prov_emitter_t      em;

    int      opened;
    uint64_t stream_id;
    uint32_t state;                /* prov_state_t */

    hades_ident_slot_t idents[HADES_IDENT_SLOTS];
    uint64_t ident_overflows;      /* identity table recycled a slot */
    uint64_t ident_unresolved;     /* dev:ino or cookie could not be resolved */
    uint64_t normalize_refused;    /* records that could not be encoded */
    uint64_t records_in;

    uint8_t  scratch[HADES_ADAPTER_SCRATCH];
} hades_adapter_t;

/* ── identity (D069-D071), pure and testable ─────────────────────────────── */
uint64_t prov_hades_ident_process(uint64_t boot_id, uint64_t pidns, uint32_t pid,
                             uint64_t start_time, uint32_t exec_gen);
uint64_t prov_hades_ident_file(uint64_t boot_id, uint64_t mntns, uint64_t dev,
                          uint64_t ino, uint64_t gen);
uint64_t prov_hades_ident_socket(uint64_t boot_id, uint64_t netns, uint32_t proto,
                            const uint8_t saddr[16], uint16_t sport,
                            const uint8_t daddr[16], uint16_t dport,
                            uint64_t cookie);

/* The default resolvers, exposed so a test can drive the real ones rather than
 * only their injected stand-ins. Both return 1 on success and 0 otherwise. */
int prov_hades_resolve_file_ident(void *ctx, const char *path,
                             uint64_t *dev_out, uint64_t *ino_out);
int prov_hades_resolve_sock_ident(void *ctx, uint32_t proto,
                             const uint8_t saddr[16], uint16_t sport,
                             const uint8_t daddr[16], uint16_t dport,
                             uint64_t *cookie_out);

/* ── lifecycle ───────────────────────────────────────────────────────────── */
int prov_hades_adapter_init(hades_adapter_t *a, const hades_adapter_cfg_t *cfg,
                       prov_status_t *st);
sense_provider_v1_t *prov_hades_adapter_provider(hades_adapter_t *a);

/* Register the three standard adapters with the fabric under one call, so a
 * profile cannot publish two of the three and call the set complete. */
int prov_hades_adapters_register_all(hades_adapter_t *proc_a, hades_adapter_t *file_a,
                                hades_adapter_t *net_a, prov_status_t *st);
include/platx/hades_alert_msx.h
/* platx/hades_alert_msx.h — INT-128: Hades alerts → MSX queue with quota/dedup. */

#define PLAT_HADES_ALERT_ID_MAX      32
#define PLAT_HADES_ALERT_KIND_MAX    32
#define PLAT_HADES_ALERT_DETAIL_MAX 256
#define PLAT_HADES_ALERT_QUOTA_MAX   32
#define PLAT_HADES_ALERT_WINDOW_SEC  60
#define PLAT_HADES_ALERT_DEDUP_SLOTS 64
typedef enum {
    PLAT_HADES_SEV_INFO = 0,
    PLAT_HADES_SEV_LOW,
    PLAT_HADES_SEV_MEDIUM,
    PLAT_HADES_SEV_HIGH,
    PLAT_HADES_SEV_CRITICAL
} plat_hades_severity_t;
typedef enum {
    PLAT_HADES_ALERT_OK = 0,
    PLAT_HADES_ALERT_ERR_ARGS,
    PLAT_HADES_ALERT_ERR_NOLINK,
    PLAT_HADES_ALERT_ERR_QUOTA,
    PLAT_HADES_ALERT_ERR_DEDUP
} plat_hades_alert_status_t;
typedef struct {
    char                  alert_id[PLAT_HADES_ALERT_ID_MAX];
    char                  kind[PLAT_HADES_ALERT_KIND_MAX];
    plat_hades_severity_t severity;
    uint64_t              ctx_id;
    char                  detail[PLAT_HADES_ALERT_DETAIL_MAX];
} plat_hades_alert_t;
typedef struct {
    plat_hades_alert_status_t status;
    int                       queued;
    char                      reason[128];
} plat_hades_alert_result_t;
int  plat_hades_alert_msx_push(const plat_hades_alert_t *alert,
                                plat_hades_alert_result_t *out);
void plat_hades_alert_msx_reset(void);
int  plat_hades_alert_msx_quota_used(void);
include/platx/hades_bpf_owner.h
/* platx/hades_bpf_owner.h — INT-122: BPF program ownership within Hades. */

#define PLAT_HADES_BPF_PROG_MAX   64
#define PLAT_HADES_BPF_SLOTS_MAX   8
typedef struct {
    int      bpf_fd;
    uint64_t owner_ctx_id;
    uint64_t owner_ctx_gen;
    char     prog_name[PLAT_HADES_BPF_PROG_MAX];
    int      live;
} plat_hades_bpf_owner_t;
int plat_hades_bpf_owner_claim(int bpf_fd, const char *prog_name,
                                plat_context_handle_t ctx,
                                plat_hades_bpf_owner_t *out);
int plat_hades_bpf_owner_release(int bpf_fd);
int plat_hades_bpf_owner_check(int bpf_fd, plat_context_handle_t ctx);
include/platx/hades_capsule.h
/* platx/hades_capsule.h — HAD0: the six Hades sensor contracts.
 *
 * This header fixes the FORM of a Hades sensor capsule before any attach code
 * exists. HAD0 is a contract, not an attachment: there is no try_own here, no
 * BPF program is loaded, and nothing is emitted. It declares the types a
 * capsule manifest is made of — source classes, the semantic quality ladder,
 * the coverage epoch, the rule-requirements form, and privacy + cost budgets —
 * so that HAD1+ implement one agreed shape instead of inventing five.
 *
 * Deliberately self-contained: it includes only <stdint.h>/<stddef.h>. It does
 * NOT include any eBPF program body, any src/ebpf/hades sensor source, or
 * libbpf — Core never pulls a BPF object in through this header.
 *
 * Two buses (TZ_PLATX_HADES §2): dense per-observation records (an exec, a
 * connect) ride the DOMAIN ring, never plat_event. Only the rare lifecycle
 * facts (attach/detach/error/…) are plat_event, and their ids live in
 * platx/event.h (0x1500–0x1505). This header does not emit either one.
 */

#define PLAT_HADES_ID_MAX        64
#define PLAT_HADES_SCHEMAS_MAX   16
#define PLAT_HADES_FIELDS_MAX    32
#define PLAT_HADES_TAG_MAX       40
#define PLAT_HADES_HASH_HEX      65   /* 32-byte hash as hex + NUL */

/* ── §3.2 Four source classes ─────────────────────────────────────────────
 * Not every sensor emits an event stream. A metric put on the STREAM "just in
 * case" is a contract violation: a rule that wants a counter demands AGGREGATE,
 * not one event per increment. */
typedef enum plat_hades_source_class {
    PLAT_HADES_CLASS_NONE      = 0,  /* zeroed manifest field — not a class */
    PLAT_HADES_CLASS_STREAM    = 1,  /* pointwise: exec, connect, open → domain ring */
    PLAT_HADES_CLASS_AGGREGATE = 2,  /* counters/hist/top-N → maps + rare snapshot */
    PLAT_HADES_CLASS_SNAPSHOT  = 3,  /* state at an instant → bounded blob + seq */
    PLAT_HADES_CLASS_HEALTH    = 4   /* coverage/integrity → health + rare plat_event */
} plat_hades_source_class_t;

/* ── §3.3 Semantic Quality Ladder ─────────────────────────────────────────
 * A fallback must name BOTH the hook AND the quality. A kprobe may not quietly
 * claim the same proof a tracepoint/LSM gives. Ordered so higher = stronger and
 * a zeroed field is UNAVAILABLE: "the source is absent; a rule may not pretend
 * it saw anything." */
typedef enum plat_hades_quality {
    PLAT_HADES_Q_UNAVAILABLE = 0,  /* no source; never "seen" */
    PLAT_HADES_Q_PARTIAL     = 1,  /* some fields / a weaker vantage */
    PLAT_HADES_Q_COMPATIBLE  = 2,  /* different hook, manifest-equivalent result */
    PLAT_HADES_Q_EXACT       = 3   /* the declared point gives the required semantics */
} plat_hades_quality_t;

/* ── §3.6 Privacy classes (per schema field) ──────────────────────────────*/
typedef enum plat_hades_privacy {
    PLAT_HADES_PRIV_METADATA          = 0,  /* allowed in an observe profile */
    PLAT_HADES_PRIV_SENSITIVE         = 1,  /* only if the profile explicitly took it */
    PLAT_HADES_PRIV_SECRET_BEARING    = 2,  /* argv/env/path/packet body — opt-in */
    PLAT_HADES_PRIV_FORBIDDEN_DEFAULT = 3   /* not in an MVP profile; preview must shout */
} plat_hades_privacy_t;

/* ── §3.5 Rule requirement outcome ─────────────────────────────────────────
 * A rule declares its needs BEFORE running. If any is unmet the outcome is
 * UNSATISFIED — never a silent skip and never "clean". UNSATISFIED = 0 so a
 * zeroed result is never mistaken for a satisfied rule. */
typedef enum plat_hades_rule_status {
    PLAT_HADES_RULE_UNSATISFIED = 0,  /* requirement unmet — not proven, not clean */
    PLAT_HADES_RULE_SATISFIED   = 1
} plat_hades_rule_status_t;

/* ── §3.4 Coverage / observation epoch ────────────────────────────────────
 * An epoch is an interval with constant: capsule + profile generation, source
 * set, adaptive mode, and semantic floor. Change any one → a NEW epoch, a rare
 * plat_event, and a gap for Evidence. Automatic reconcile without a new epoch
 * is forbidden. */
typedef struct plat_hades_epoch {
    uint64_t epoch_id;
    uint32_t capsule_generation;
    uint32_t profile_generation;
    uint32_t source_set_hash;   /* which sources are live this epoch */
    uint32_t adaptive_mode;     /* FULL/FILTERED/AGGREGATE/HEALTH_ONLY (§12) */
    plat_hades_quality_t semantic_floor;
} plat_hades_epoch_t;

/* ── §3.6 Cost SLO (circuit breaker, §13) ──────────────────────────────────
 * Exceeding a budget is never a silent drop: it forces a declared adaptive
 * transition (new epoch + DEGRADED) or a DEGRADED / keep-old profile. */
typedef struct plat_hades_slo {
    uint32_t max_cpu_pct;
    uint64_t max_map_bytes;
    uint64_t max_ring_bytes;
    uint32_t max_events_per_sec;
    uint64_t max_payload_bytes_per_sec;
    uint32_t max_load_ms;        /* verifier/load time ceiling */
} plat_hades_slo_t;

/* One schema field: its name and the strictest thing it may carry. */
typedef struct plat_hades_field {
    char                 name[PLAT_HADES_ID_MAX];
    plat_hades_privacy_t privacy;
} plat_hades_field_t;

/* One event schema in the capsule. */
typedef struct plat_hades_schema {
    char                      kind[PLAT_HADES_ID_MAX];   /* e.g. "exec" */
    plat_hades_source_class_t source_class;
    plat_hades_quality_t      declared_quality;          /* best this schema claims */
    uint32_t                  schema_ver;
    plat_hades_privacy_t      privacy_max;               /* strictest field class */
    unsigned                  n_fields;
    plat_hades_field_t        fields[PLAT_HADES_FIELDS_MAX];
} plat_hades_schema_t;

/* ── §3.1 The signed capsule manifest ─────────────────────────────────────
 * A signed package of one Observation Epoch — not "a pile of .o in a folder".
 * A capsule fixes a generation: a changed .o or schema is a new capsule_id or
 * a new hash → a new generation. A silent object swap is forbidden. */
typedef struct plat_hades_manifest {
    char                capsule_id[PLAT_HADES_ID_MAX];
    char                profile_id[PLAT_HADES_ID_MAX];
    uint32_t            generation;

    plat_hades_epoch_t  epoch;
    plat_hades_slo_t    slo;

    unsigned            n_schemas;
    plat_hades_schema_t schemas[PLAT_HADES_SCHEMAS_MAX];

    /* attach points + fallbacks are named (strings), not opened here. */
    char                attach_point[PLAT_HADES_ID_MAX];   /* declared point */
    char                fallback_point[PLAT_HADES_ID_MAX]; /* named fallback, or "" */

    /* Expected program tags and content hashes — a load that does not match
     * REFUSEs (verified by preflight in HAD1, not here). */
    char                expected_tag[PLAT_HADES_TAG_MAX];
    char                object_hash[PLAT_HADES_HASH_HEX];
    char                btf_hash[PLAT_HADES_HASH_HEX];
    char                schema_hash[PLAT_HADES_HASH_HEX];

    int                 signature_present;  /* default appliance: signature required */
    int                 lab_unsigned_ok;    /* only if the manifest explicitly says so */
} plat_hades_manifest_t;

/* ── §3.5 Rule Requirements Contract (the FORM only) ───────────────────────
 * Declared before the run. HAD0 fixes the form; the first live detector is not
 * HAD0 (no earlier than HAD3, and never without an UNSATISFIED zub). This is a
 * pure predicate — it evaluates no arbitrary text, runs no Rego. */
typedef struct plat_hades_rule_req {
    uint32_t             need_kinds_mask;   /* required event kinds */
    uint32_t             min_schema_ver;    /* else a foreign layout */
    plat_hades_quality_t min_quality;       /* PARTIAL cannot close an EXACT rule */
    uint32_t             max_loss_ppm;      /* else "did not happen" on a drop */
    uint32_t             min_lineage_depth; /* a parent rule with no tree → UNSATISFIED */
    uint32_t             min_independent;   /* one STREAM alone is not PROVEN */
} plat_hades_rule_req_t;

/* Bodies are static inline: HAD0's file list is the header alone (no .c), and
 * a contract you can call is better than one you can only cite. */

/* 1 for the four MVP-admissible source classes; 0 for NONE/unknown. */
int plat_hades_source_class_ok(plat_hades_source_class_t c); /* inline interface */

/*
 * Evaluate the requirement form against what a source actually offered. A rule
 * that declared NO quality (min_quality == UNAVAILABLE) is UNSATISFIED by
 * construction — it may not apply. A source below the required quality, or a
 * schema older than required, or too much loss, or too little lineage /
 * independence, is UNSATISFIED too. SATISFIED only when every declared floor is
 * met. No side effects, no arbitrary text evaluated.
 */
plat_hades_rule_status_t plat_hades_rule_eval(
    const plat_hades_rule_req_t *req,
    plat_hades_quality_t         observed_quality,
    uint32_t                     observed_schema_ver,
    uint32_t                     observed_loss_ppm,
    uint32_t                     observed_lineage_depth,
    uint32_t                     observed_independent); /* inline interface */

/* ── string helpers (out-of-range → "UNKNOWN", never a positive name) ─────*/
const char *plat_hades_source_class_str(plat_hades_source_class_t c); /* inline interface */

const char *plat_hades_quality_str(plat_hades_quality_t q); /* inline interface */

const char *plat_hades_privacy_str(plat_hades_privacy_t p); /* inline interface */

const char *plat_hades_rule_status_str(plat_hades_rule_status_t s); /* inline interface */
include/platx/hades_context_corr.h
/* platx/hades_context_corr.h — INT-125: Hades + Fabric Context correlation. */

typedef struct {
    uint64_t              hades_ctx_id;
    pid_t                 pid;
    uint64_t              birth;
    uint64_t              ns_inum;
    plat_corr_id_t        corr_id;
    plat_context_handle_t ctx_handle; /* {0,0} if not found */
} plat_hades_ctx_corr_t;
/* Resolve pid+birth+ns_inum → context handle + corr_id. 0/-1. */
int plat_hades_ctx_corr_resolve(pid_t pid, uint64_t birth, uint64_t ns_inum,
                                 plat_hades_ctx_corr_t *out);
include/platx/hades_event_schema.h
/* platx/hades_event_schema.h — INT-124: Versioned Hades event → Audit/MBus. */

#define PLAT_HADES_EVENT_VERSION     1
#define PLAT_HADES_EVENT_KIND_MAX   32
#define PLAT_HADES_EVENT_DETAIL_MAX 256
#define PLAT_HADES_EVENT_TOPIC      "hades.event"
typedef struct {
    uint32_t       version;
    char           kind[PLAT_HADES_EVENT_KIND_MAX];
    pid_t          pid;
    uint64_t       ctx_id;
    uint32_t       ctx_gen;
    plat_corr_id_t corr_id;
    char           detail[PLAT_HADES_EVENT_DETAIL_MAX];
    uint64_t       event_ts_ms;
} plat_hades_event_t;
/* Emit to audit (AUDIT_NOTICE) + MBus. Returns 0/-1. */
int plat_hades_event_emit(const plat_hades_event_t *ev);
/* Format complete UTF-8 JSON. Returns bytes written (excluding NUL), or -1
 * for invalid/unterminated fields or insufficient space. On failure, buf[0]
 * is cleared when buf != NULL and n > 0. */
int plat_hades_event_format(const plat_hades_event_t *ev, char *buf, size_t n);
include/platx/hades_sensor_abi.h
/* platx/hades_sensor_abi.h — the Hades sensor wire ABI, stated once (D072).
 *
 * Until now the layout of what a sensor writes into its ring buffer existed in
 * two places at once: `struct sensor_hdr` in the private sensors_common.h, and
 * whatever the consumer believed. A consumer that wants to read those bytes had
 * to include a private BPF-side header — which is how a public adapter ends up
 * depending on the internals of the domain it adapts.
 *
 * This header is that layout, and nothing else:
 *   - no includes at all, so it is valid both for clang -target bpf (where
 *     stdint.h is not what you want) and for ordinary userspace;
 *   - fixed-width builtin types, identical on LP64 and on BPF;
 *   - _Static_assert on every size, so a field added in the middle breaks the
 *     build instead of silently shifting every consumer's view.
 *
 * Frozen: HADES_SENSOR_ABI_VERSION 1. Changing a struct here is an ABI break
 * and needs a new version, not an edit.
 */
#define HADES_SENSOR_ABI_VERSION 1

#define HADES_MAX_COMM   16
#define HADES_MAX_PATH  256
#define HADES_MAX_ARGS  512

/* Event types, as written into hades_sensor_hdr.type. The values are the ones
 * the sensors already emit (enum sensor_type); they are repeated here so a
 * consumer does not need the private enum. */
#define HADES_EV_PROCESS_EXEC   1u
#define HADES_EV_PROCESS_EXIT   2u
#define HADES_EV_PROCESS_FORK   3u
#define HADES_EV_FILE_OPEN     10u
#define HADES_EV_FILE_WRITE    11u
#define HADES_EV_FILE_UNLINK   12u
#define HADES_EV_FILE_RENAME   13u
#define HADES_EV_NET_CONNECT   20u
#define HADES_EV_NET_ACCEPT    23u
#define HADES_EV_NET_SEND      24u
#define HADES_EV_NET_CLOSE    111u

/* ── common header (48 bytes, frozen) ────────────────────────────────────
 * `cpu` and the per-CPU emitted counter together are the source sequence: the
 * header carries no sequence field of its own, and adding one would break the
 * freeze. A consumer that tracks (cpu, counter) sees loss without it.
 */
struct hades_sensor_hdr {
    unsigned long long ts;        /* bpf_ktime_get_ns() */
    unsigned int  pid;            /* host PID */
    unsigned int  tid;            /* host TID */
    unsigned int  uid;            /* effective UID */
    unsigned int  gid;            /* effective GID */
    unsigned int  ppid;           /* parent PID */
    unsigned short type;          /* HADES_EV_* */
    unsigned short cpu;           /* processor id */
    char comm[HADES_MAX_COMM];
};

_Static_assert(sizeof(struct hades_sensor_hdr) == 48,
               "hades_sensor_hdr is frozen at 48 bytes");

/* ── process lifecycle (HAD1) ────────────────────────────────────────────── */
struct hades_proc_event {
    struct hades_sensor_hdr hdr;
    unsigned int ppid;
    unsigned char type;           /* 0=fork 1=exec 2=exit */
    signed int   exit_code;       /* waitpid-style; type == 2 only */
    char filename[HADES_MAX_PATH];
    char argv[HADES_MAX_ARGS];
};

/* ── file activity (HAD2) ────────────────────────────────────────────────── */
struct hades_file_event {
    struct hades_sensor_hdr hdr;
    unsigned char op;             /* 0=open 2=write 3=unlink */
    unsigned int  flags;          /* f_flags, open only */
    signed long long ret;         /* bytes written, or negative errno */
    char path[HADES_MAX_PATH];
};

/* ── network activity (HAD3) ─────────────────────────────────────────────── */
struct hades_net_event {
    struct hades_sensor_hdr hdr;
    unsigned char type;           /* 0=connect 1=accept 2=close 3=udp_send */
    unsigned char af;             /* 2=AF_INET 10=AF_INET6 */
    unsigned char saddr[16];      /* v4 uses the first four bytes */
    unsigned char daddr[16];
    unsigned short sport;         /* host byte order */
    unsigned short dport;
    unsigned long long bytes;
};

/* ── Имена в bpffs ────────────────────────────────────────────────────────
 *
 * Кольцо сенсора публикуется в bpffs под своим путём; карта счётчиков
 * продюсера — рядом, под тем же путём с этим суффиксом. Договорённость лежит
 * ЗДЕСЬ, в общем ABI-заголовке, а не двумя строковыми литералами в
 * hades_sensors.c и hades_live_bridge.c: закрепляет одна сторона, открывает
 * другая, и разойтись молча они не должны.
 *
 * Зачем счётчики вообще публиковать: отказ bpf_ringbuf_reserve() не оставляет
 * в кольце ни байта — записи не появилось. Потребитель, как бы аккуратно он ни
 * читал, узнать о переполнении не может; знает только продюсер, в этой карте
 * (W2-F006).
 */
#define HADES_PIN_STATS_SUFFIX "_stats"

_Static_assert(sizeof(struct hades_proc_event) == 832, "proc event layout");
_Static_assert(sizeof(struct hades_file_event) == 320, "file event layout");
_Static_assert(sizeof(struct hades_net_event)  ==  96, "net event layout");
include/platx/hades_sensor_profile.h
/* platx/hades_sensor_profile.h — INT-121: Hades sensor profile (one active slot). */

#define PLAT_HADES_SENSOR_NAME_MAX   48
#define PLAT_HADES_SENSOR_MAX        16
#define PLAT_HADES_PROFILE_NAME_MAX  64
typedef struct {
    char     sensor_name[PLAT_HADES_SENSOR_NAME_MAX];
    int      enabled;
    uint32_t flags;
} plat_hades_sensor_entry_t;
typedef struct {
    char                      name[PLAT_HADES_PROFILE_NAME_MAX];
    plat_hades_sensor_entry_t sensors[PLAT_HADES_SENSOR_MAX];
    int                       n_sensors;
    uint64_t                  loaded_at_ms;
} plat_hades_sensor_profile_t;
int plat_hades_profile_load(const plat_hades_sensor_profile_t *p);
int plat_hades_profile_get(plat_hades_sensor_profile_t *out);
int plat_hades_profile_sensor_enable(const char *sensor_name, int enable);
include/platx/hades_transport.h
/* platx/hades_transport.h — where an adapter's raw records come from.
 *
 * Two transports, and the difference between them is the honest part:
 *
 *   live    the kernel ring of an owned Hades sensor. It needs CAP_BPF, a
 *           loaded object and a bound reader. When any of that is missing the
 *           transport REFUSES with a typed reason — it never degrades into an
 *           idle stream, because an idle stream means "the host is quiet" and
 *           that is precisely the claim it cannot make.
 *
 *   replay  a file of recorded raw records in the frozen wire layout
 *           (platx/hades_sensor_abi.h). This is what makes the fabric usable
 *           on a host without CAP_BPF, and it is deliberately NOT dressed up
 *           as live: every record it produces is marked by the adapter's
 *           envelope as replayed, and the CLI says which transport is in use.
 */

typedef enum hades_transport_kind {
    HADES_TRANSPORT_NONE   = 0,
    HADES_TRANSPORT_REPLAY = 1,
    HADES_TRANSPORT_LIVE   = 2
} hades_transport_kind_t;

typedef struct hades_transport {
    uint32_t kind;
    void    *fh;             /* FILE* for replay */
    uint32_t rec_size;
    uint64_t delivered;
    uint64_t short_reads;    /* truncated tail: counted, never padded */
    /* Измеряются ли потери продюсера (W2-F006).
     *
     * lost_out — число, и числом нельзя сказать «не знаю». Пока моста не было,
     * это не мешало: replay-транспорт знает свои потери точно. У живого
     * кольца иначе — переполнение видит только продюсер, в своей карте, и
     * если её нет, честный ответ не «0 потерь», а «потери не измеряются».
     * Ноль вместо этого читается как «потерь не было» и удостоверяет то, чего
     * никто не проверял.
     *
     * 1 = счётчик продюсера привязан, lost_out полон;
     * 0 = не привязан, lost_out учитывает только потери потребителя. */
    unsigned char loss_measured;

    void    *live;           /* bridge state, owned by the bridge */
    int (*live_pull)(void *live, void *rec, uint32_t rec_size,
                     uint64_t *lost_out);
    void (*live_close)(void *live);
} hades_transport_t;

/* Open a replay transport over `path`. The record size is decided by the
 * adapter kind, and a file whose size is not a whole number of records is
 * refused: a half record is not a record. */
int prov_hades_transport_replay(hades_transport_t *t, uint32_t adapter_kind,
                           const char *path, prov_status_t *st);

/* ── The live bridge is a registered extension, not a #ifdef ─────────────
 * Reading an owned sensor's ring means linking the Hades engine, and the
 * Defensive server profile is forbidden from containing it: the profile
 * firewall denies `^hades_` there, and that denial is correct — a defensive
 * agent must not carry a BPF loader. So the bridge cannot simply be compiled
 * into every profile.
 *
 * It is therefore a hook. A profile that legitimately owns Hades links a
 * translation unit that calls prov_hades_live_register() at start; a profile
 * that does not gets NULL and the honest refusal below. No profile changes
 * behaviour by accident, and no #ifdef decides at compile time what a deploy
 * decides at link time.
 *
 * The bridge is expected to: open the sensor's `events` ring, drain it into a
 * bounded queue and answer pull() from that queue, reporting ring loss through
 * `lost_out` — the same contract the replay transport already satisfies.
 */
typedef int (*prov_hades_live_open_fn)(void *bridge_ctx, uint32_t adapter_kind,
                                       const char *pin_path,
                                       hades_transport_t *t,
                                       prov_status_t *st);

void prov_hades_live_register(prov_hades_live_open_fn fn, void *bridge_ctx);
int  prov_hades_live_available(void);

/* The live transport. Without a registered bridge it returns
 * PROV_ERR_UNAVAILABLE / PROV_UNAVAIL_KERNEL naming what is missing; with one,
 * it validates that the bridge really produced a reader before calling the
 * transport live. */
int prov_hades_transport_live(hades_transport_t *t, uint32_t adapter_kind,
                              const char *pin_path, prov_status_t *st);

void prov_hades_transport_close(hades_transport_t *t);

/* The pull function to hand to hades_adapter_cfg_t.pull. */
int prov_hades_transport_pull(void *ctx, void *rec, uint32_t rec_size,
                         uint64_t *lost_out);

const char *prov_hades_transport_name(uint32_t kind);
include/platx/services/hades_v1.h
/* platx/services/hades_v1.h — публичный контракт реестра, не реализация.
 * Модули require/provide этот vtable. Core не включает src/ebpf/hades.
 * Наличие vtable = живая cap. Это не eBPF API и не путь в src/.
 */
/* sensors.hades / PLAT_CAP_HADES — имя в реестре. Не CLI-глагол hades. */
#define PLAT_HADES_V1   0x00010000u

struct plat_hades_v1 {
    uint32_t struct_size;          /* sizeof; иначе чужая раскладка — не вызывать */
    int (*health)(void);           /* 0 = жив; иначе cap нет, полусостояние запрещено */
    int (*version)(uint32_t *out); /* *out = PLAT_HADES_V1; не версия ядра/eBPF */
};
11

edr

Наблюдение за конечным узлом и координация разрешённого реагирования
src/edr/Наблюдение и исследование16 файлов15 API headers

EDR соединяет сигналы защиты с контекстом процесса и объекта. Контур различает наблюдение, аналитическую оценку и разрешённую реакцию; источник события и качество наблюдения сохраняются. Состояние провайдера и исход операции отражаются в CLI отдельно.

Граница ответственности

  • EDR соединяет сигналы защиты с контекстом процесса и объекта. Контур различает наблюдение, аналитическую оценку и разрешённую реакцию; источник события и качество наблюдения сохраняются. Состояние провайдера и исход операции отражаются в CLI отдельно.

Устройство подсистемы

  • адаптер CLI домена EDR к консоли платформы. Логики нет: она в edr_cli.c и печатает в FILE*. Вывод собирается в поток в памяти и отдаётся консоли одним куском — иначе пришлось бы держать второй набор форматирования, и две реализации одной команды однажды разошлись бы
  • CLI домена EDR. Логика и печать в FILE*. ЧТО ЭТОТ CLI ПОКАЗЫВАЕТ. Инцидент без его сомнений бесполезен и опасен: по «severity 85» нельзя понять, на чём стоит вывод, чего НЕ наблюдалось и что противоречит. Поэтому `incident explain` печатает рядом гипотезы с
  • finding → hypothesis → incident; counter-forensic;
  • Central-роль EDR (EXF-X-05…X-11). Только сводки.

Управление и диагностика

Корневые команды: edr. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / edr →
Состав подсистемы / 16 файлов
Файл / компонентНазначение и граница
src/edr/cmd_edr.cадаптер CLI домена EDR к консоли платформы. Логики нет: она в edr_cli.c и печатает в FILE*. Вывод собирается в поток в памяти и отдаётся консоли одним куском — иначе пришлось бы держать второй набор форматирования, и две реализации одной команды однажды разошлись бы
src/edr/edr_cli.cCLI домена EDR. Логика и печать в FILE*. ЧТО ЭТОТ CLI ПОКАЗЫВАЕТ. Инцидент без его сомнений бесполезен и опасен: по «severity 85» нельзя понять, на чём стоит вывод, чего НЕ наблюдалось и что противоречит. Поэтому `incident explain` печатает рядом гипотезы с
src/edr/edr_correlator.cfinding → hypothesis → incident; counter-forensic;
src/edr/edr_federation.cCentral-роль EDR (EXF-X-05…X-11). Только сводки.
src/edr/edr_fleet_hunt.cfan-out подписанного рецепта по флоту (EXF-X-17).
src/edr/edr_hunt.cкомпиляция и bounded исполнение рецептов hunt. Здесь нет ни одного вызова, который читает мир: всё чтение делает провайдер хоста через vtable. Поэтому «hunt только наблюдает» — свойство компоновки, а не обещание: у этого файла нет символов для I/O.
src/edr/edr_hypothesis.cнабор гипотез, целочисленный posterior, gate вердикта. Арифметика — только целые: exp считается собственной процедурой в фиксированной точке, чтобы два build'а (и два узла флота) получали один и тот же posterior из одних ячеек. libm exp() этого не обещает.
src/edr/edr_incident.cинцидент как последовательность неизменяемых ревизий. Инвариант, ради которого написано всё ниже: у инцидента нет «текущего состояния», которое можно перезаписать. Есть последняя ревизия, а любое изменение — копия последней с одним отличием, номером на единицу больше
src/edr/edr_pack.cжизненный цикл policy pack: транзакционная активация с поколением и откатом, coverage contract, replay-обязательность, drift.
src/edr/edr_projection.cподписанные bounded-проекции узла (XDR, EXF-X-04/05/07).
src/edr/edr_reason.cимена причин и уровней наблюдения. Таблица полная: тест t_edr_reason перебирает все коды и требует имя ≠ "?".
src/edr/edr_recorder.cflight recorder с pre-trigger окном и авто-откатом глубокого профиля; TL-постура; запрос prevention compile.
src/edr/edr_response.cчерновик Plan IR без полномочия; лестница; автономия. шагов. Этого достаточно, чтобы platx_planir_admit и Action Coordinator отказали — а значит, «EDR не может действовать сам» доказывается отказом действующего кода, а не этим комментарием.
src/edr/edr_saga.cfleet plan / node re-check / saga / partition (EXF-X-12…16).
src/edr/edr_storyline.cslab-проекция графа сущностей: ingest без I/O, CAUSES только из явного parent, расхождение свидетелей как запись, синтетика отдельно.
src/edr/edrctl_main.cавтономный CLI домена EDR. Домен в поставляемый профиль не входит, поэтому глагол `edr` внутри platx появится по решению владельца. До тех пор CLI обязан быть запускаемым: «CLI есть» без исполнения — утверждение без проверки. Состояние живёт в памяти процесса — сценарий из нескольких команд идёт через `-f `.
Контракты API / 15 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/edr_correlator.h
/* platx/edr_correlator.h — коррелятор: finding → hypothesis → incident.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §4.5 (EXF-D-01…07).
 *
 * EDR не содержит детекторов первого уровня. Первичные findings приходят от
 * SENSE, PXSIG-контента и NDR; здесь они превращаются в ячейки набора гипотез
 * и ревизии инцидента. Что коррелятор делает САМ — это findings класса
 * counter-forensic (EXF-D-05), вычисляемые из фактов о наблюдателе без
 * нового сенсора, и Reality Quorum (EXF-D-06): расхождение свидетелей разных
 * уровней — OBSERVER_DISCREPANCY с обоими показаниями, не «большинство».
 *
 * Правила, проверяемые тестом:
 *  - finding с verdict INDETERMINATE (coverage was blind) не поднимает
 *    инцидент выше TRIAGED без второго независимого основания (EXF-D-04);
 *  - NDR-гипотеза с requires_host_evidence без host-основания остаётся
 *    гипотезой; NDR + host по одной сущности — CORROBORATED (EXF-D-07);
 *  - потеря зрения ПОВЫШАЕТ TL и severity, не понижает число алертов.
 */

#define EDR_CORR_SETS_MAX 64u

typedef enum edr_finding_domain { EDR_DOM_NONE = 0, EDR_DOM_HOST = 1, EDR_DOM_NETWORK, EDR_DOM_SANDBOX,
    EDR_DOM_INTEGRITY, EDR_DOM_OBSERVER, EDR_DOM_MAX } edr_finding_domain_t;

/* Верdict детектора — как в sense_finding.h; COMPROMISE невыразим. */
typedef enum edr_fverdict { EDR_FV_ANOMALY = 0, EDR_FV_POLICY_MATCH = 1, EDR_FV_INDETERMINATE = 2 } edr_fverdict_t;

/* Классы counter-forensic findings (EXF-D-05) — закрытый список. */
typedef enum edr_cf_class {
    EDR_CF_NONE = 0,
    EDR_CF_SENSOR_DETACHED = 1, EDR_CF_SEQUENCE_BREAK, EDR_CF_OBSERVER_DISCREPANCY,
    EDR_CF_EXEC_CHANGED_AFTER_MEASURE, EDR_CF_CLOCK_ROLLBACK, EDR_CF_POLICY_GEN_ROLLBACK,
    EDR_CF_AUDIT_CHANNEL_LOST, EDR_CF_CONTROL_CHANNEL_LOST, EDR_CF_EVIDENCE_TAMPER,
    EDR_CF_FLIGHT_RECORDER_STARVED, EDR_CF_COORDINATED_BLINDING,
    EDR_CF_MAX
} edr_cf_class_t;

typedef struct edr_finding_in {
    uint64_t finding_id;            /* детерминированный id детектора         */
    uint32_t domain;                /* edr_finding_domain_t                   */
    uint32_t verdict;               /* edr_fverdict_t                         */
    uint32_t confidence;            /* 0..3                                   */
    uint32_t severity;              /* 0..100                                 */
    uint32_t observation_level;
    uint32_t independence_group;
    uint32_t source_id;
    uint32_t requires_host_evidence;/* NDR: гипотеза, не атака                */
    uint64_t entity_id, entity_gen;
    uint32_t entity_kind;
    uint32_t hypothesis_role;       /* edr_hyp_role_t, к которой относится    */
    int32_t  llr_milli;
    uint32_t coverage_blind;        /* контракт видимости был не удовлетворён */
    uint64_t t_ns, wall_ns;
} edr_finding_in_t;

typedef struct edr_corr_set {
    int in_use;
    uint64_t incident_id;
    uint32_t attack_idx, benign_idx, dq_idx, resid_idx;
    uint32_t host_basis, network_basis, corroborated;
    uint32_t indeterminate_only;    /* инцидент стоит только на INDETERMINATE  */
    uint32_t _pad;
    edr_hypothesis_set_t set;
} edr_corr_set_t;

typedef struct edr_correlator {
    edr_incidents_t *incidents;
    edr_posture_t   *posture;
    uint32_t next_set, _pad;
    uint64_t findings_in, cf_findings, discrepancies, corroborated, held_at_triaged, refused;
    edr_corr_set_t sets[EDR_CORR_SETS_MAX];
} edr_correlator_t;

int  edr_corr_init(edr_correlator_t *c, edr_incidents_t *incidents, edr_posture_t *posture);
/* Приём finding: открывает или пополняет инцидент по (entity_id, gen),
 * добавляет ячейку в набор, двигает состояние по правилам EXF-D-04/07. */
int  edr_corr_finding(edr_correlator_t *c, const edr_finding_in_t *f, uint64_t *incident_out);
/* Counter-forensic finding из факта о наблюдателе: severity/TL растут. */
int  edr_corr_counter_forensic(edr_correlator_t *c, uint32_t cf_class, uint64_t entity_id,
                               uint64_t entity_gen, uint32_t level, uint64_t t_ns, uint64_t *incident_out);
/* Reality Quorum: показания свидетелей о subject (present/absent, уровень,
 * группа). Согласны — OK; расходятся — OBSERVER_DISCREPANCY finding. */
typedef struct edr_quorum_claim { uint32_t present, observation_level, independence_group, source_id; } edr_quorum_claim_t;
int  edr_corr_quorum(edr_correlator_t *c, uint64_t entity_id, uint64_t entity_gen,
                     const edr_quorum_claim_t *claims, uint32_t n, uint64_t t_ns, uint64_t *incident_out);
const edr_corr_set_t *edr_corr_set(const edr_correlator_t *c, uint64_t incident_id);
const char *edr_cf_class_name(uint32_t k);
include/platx/edr_federation.h
/* platx/edr_federation.h — Central-роль EDR: приём проекций, per-node
 * cursor и GAP, entity resolution с классом, fleet-гипотезы,
 * COORDINATED_BLINDING, cross-node prior (opt-in).
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §5.3–5.4 (EXF-X-05…X-11).
 *
 * Central владеет только сводками (EXF-X-03): здесь нет ни сырых ячеек, ни
 * байт улик — только то, что вошло в edr_projection_t. Узел, от которого
 * проекций нет N нс, — GAP на карте покрытия, не «здоровый и тихий»
 * (EXF-X-06, R1). Сопоставление сущностей хранит класс (SAME / PROBABLE(k) /
 * AMBIGUOUS / DISTINCT); склеить без класса невозможно (EXF-X-08).
 * Fleet-гипотезы ссылаются на проекции узловых наборов (EXF-X-09); набор
 * без обязательных пяти членов — SCHEMA. Cross-node prior выключен по
 * умолчанию и включается только подписанной policy с k-анонимностью
 * (EXF-X-11).
 */

#define EDR_FED_NODES_MAX     64u
#define EDR_FED_PROJ_LIVE     16u    /* последних проекций на узел             */
#define EDR_FED_LINKS_MAX     256u
#define EDR_FED_ENTITIES_MAX  512u
#define EDR_FLEET_HYPS        5u
#define EDR_FLEET_CELLS_MAX   64u

typedef enum edr_fed_node_state { EDR_FN_UNKNOWN = 0, EDR_FN_OK = 1, EDR_FN_GAP_SILENT,
    EDR_FN_GAP_SEQUENCE, EDR_FN_REFUSED_SCHEMA, EDR_FN_NOLINK, EDR_FN_MAX } edr_fed_node_state_t;

typedef struct edr_fed_node {
    int      in_use;
    uint64_t node_id;
    uint32_t tenant_id, expected_schema;
    uint64_t cursor;                 /* последний принятый sequence            */
    uint64_t last_seen_ns;
    uint64_t received, gaps, refused_schema, refused_sig, refused_tenant, refused_stale;
    uint32_t state, blinding_class;  /* последний BLINDING_REPORT              */
    uint64_t blinding_t_ns;
    uint64_t policy_generation;      /* заявленное узлом                        */
    uint32_t n_live, head;
    edr_projection_t live[EDR_FED_PROJ_LIVE];
} edr_fed_node_t;

/* Сущность в федеративном индексе — только из ENTITY_DIGEST проекций. */
typedef struct edr_fed_entity {
    int in_use;
    uint64_t node_id;
    edr_proj_entity_t e;
} edr_fed_entity_t;

typedef enum edr_link_class { EDR_LINK_NONE = 0, EDR_LINK_SAME = 1, EDR_LINK_PROBABLE,
    EDR_LINK_AMBIGUOUS, EDR_LINK_DISTINCT, EDR_LINK_MAX } edr_link_class_t;
typedef struct edr_fed_link {
    int in_use;
    uint32_t a, b;                   /* индексы сущностей                        */
    uint32_t klass, k;               /* k — сколько признаков совпало (PROBABLE) */
    uint32_t conflict_mask;          /* какие признаки противоречат (AMBIGUOUS)  */
} edr_fed_link_t;
#define EDR_MATCH_DIGEST   (1u<<0)
#define EDR_MATCH_NAME     (1u<<1)
#define EDR_MATCH_PARENT   (1u<<2)
#define EDR_MATCH_GEN      (1u<<3)
#define EDR_MATCH_KIND     (1u<<4)

/* Fleet Hypothesis Arena — обязательные члены (EXF-X-09). */
typedef enum edr_fleet_role {
    EDR_FH_NONE = 0,
    EDR_FH_ONE_OPERATOR_N_NODES = 1,
    EDR_FH_INDEPENDENT_COMMON_TOOL,
    EDR_FH_LEGIT_ADMIN_OPERATION,
    EDR_FH_FALSE_CORRELATION_PROXY_NAT,
    EDR_FH_RESIDUAL,
    EDR_FH_MAX
} edr_fleet_role_t;
typedef struct edr_fleet_cell {
    uint64_t node_id, incident_id, hypothesis_set_id;  /* ссылка на ПРОЕКЦИЮ     */
    uint32_t node_role;              /* роль узловой гипотезы                  */
    uint32_t posterior_det_milli, grade, fleet_hyp;    /* к какой fleet-гипотезе */
    int32_t  llr_milli;
    uint32_t _pad;
} edr_fleet_cell_t;
typedef struct edr_fleet_hypset {
    uint64_t set_id;
    uint32_t n_hyps, n_cells;
    uint32_t roles[EDR_FLEET_HYPS];
    int32_t  prior_milli[EDR_FLEET_HYPS];
    uint32_t posterior_milli[EDR_FLEET_HYPS];
    uint32_t nodes_referenced;
    uint32_t _pad;
    edr_fleet_cell_t cells[EDR_FLEET_CELLS_MAX];
} edr_fleet_hypset_t;

typedef struct edr_fed_finding {     /* fleet-finding (COORDINATED_BLINDING)   */
    uint32_t cf_class, priority;     /* priority 100 — выше любого одиночного   */
    uint32_t n_nodes, _pad;
    uint64_t t_first_ns, t_last_ns;
    uint64_t nodes[16];
} edr_fed_finding_t;

typedef struct edr_federation {
    uint64_t central_id;
    uint32_t tenant_id;
    uint32_t prior_enabled;          /* cross-node prior: 0 по умолчанию         */
    uint64_t prior_policy_token; uint32_t prior_k_anon, _pad;
    uint64_t ingested, refused_total, gaps_total, discrepancies, blinding_findings, prior_refused;
    uint32_t n_nodes, n_entities, n_links, _pad2;
    edr_fed_node_t   nodes[EDR_FED_NODES_MAX];
    edr_fed_entity_t entities[EDR_FED_ENTITIES_MAX];
    edr_fed_link_t   links[EDR_FED_LINKS_MAX];
} edr_federation_t;

int  edr_fed_init(edr_federation_t *f, uint64_t central_id, uint32_t tenant_id);
int  edr_fed_node_register(edr_federation_t *f, uint64_t node_id, uint32_t tenant_id, uint32_t expected_schema);
/* Приём кадра проекции: подпись узла обязательна; схема ≠ ожидаемой —
 * REFUSED_SCHEMA (не разбор); чужой tenant — отказ; sequence-скачок — GAP
 * (кадр принимается, разрыв посчитан); sequence ≤ cursor — STALE/DUPLICATE. */
int  edr_fed_ingest(edr_federation_t *f, const uint8_t *wire, size_t len,
                    edr_proj_verify_fn verify, void *ctx, uint64_t now_ns);
/* Карта покрытия флота: узел без проекций дольше silence_ns — GAP_SILENT. */
int  edr_fed_coverage(edr_federation_t *f, uint64_t now_ns, uint64_t silence_ns,
                      uint32_t *ok_out, uint32_t *gap_out, uint32_t *unknown_out);
const edr_fed_node_t *edr_fed_node(const edr_federation_t *f, uint64_t node_id);
/* Entity resolution: класс связи двух сущностей индекса; PROBABLE требует
 * k ≥ 2 совпадений без конфликта; конфликт → AMBIGUOUS. Запись в links. */
int  edr_fed_resolve(edr_federation_t *f, uint32_t a, uint32_t b, edr_fed_link_t *out);
int  edr_fed_entity_find(const edr_federation_t *f, uint64_t node_id, uint64_t entity_id, uint64_t gen);
/* Fleet Hypothesis Arena. */
int  edr_fleet_hypset_init(edr_fleet_hypset_t *s, uint64_t set_id);
int  edr_fleet_hypset_validate(const edr_fleet_hypset_t *s);
/* Ячейка только из HYPOTHESIS-проекции узла, принятой федерацией. */
int  edr_fleet_cell_add(edr_fleet_hypset_t *s, const edr_federation_t *f, uint64_t node_id,
                        uint64_t hypothesis_set_id, uint32_t node_role, uint32_t fleet_hyp, int32_t llr_milli);
int  edr_fleet_hypset_recompute(edr_fleet_hypset_t *s);
/* COORDINATED_BLINDING: ≥ min_nodes узлов с BLINDING_REPORT (sensor/audit/
 * control) или молчанием в окне window_ns → fleet-finding priority 100. */
int  edr_fed_blinding(edr_federation_t *f, uint64_t now_ns, uint64_t window_ns, uint32_t min_nodes,
                      edr_fed_finding_t *out);
/* Cross-node prior: выключен; включение — токен policy ≠ 0 и k_anon ≥ 5. */
int  edr_fed_prior_enable(edr_federation_t *f, uint64_t policy_token, uint32_t k_anon);
int  edr_fed_prior_query(edr_federation_t *f, uint32_t node_role, uint32_t *tp_milli_out, uint32_t *n_out);
const char *edr_link_class_name(uint32_t k);
const char *edr_fleet_role_name(uint32_t r);
const char *edr_fed_node_state_name(uint32_t s);
include/platx/edr_fleet_hunt.h
/* platx/edr_fleet_hunt.h — fleet hunt: тот же подписанный рецепт §4.6 с
 * fan-out по scope, per-node бюджетом и сводкой с ОБЯЗАТЕЛЬНЫМ столбцом
 * «узлы, которые не ответили / отказали / ответили PARTIAL» (EXF-X-17).
 * Агрегат без этого столбца — дефект SCHEMA, не сводка. */

#define EDR_FLEET_HUNT_NODES_MAX 64u

typedef enum edr_hunt_scope { EDR_HS_NONE = 0, EDR_HS_FLEET = 1, EDR_HS_SELECTION, EDR_HS_COMPOSITE, EDR_HS_MAX } edr_hunt_scope_t;
typedef enum edr_fh_node_status { EDR_FHN_PENDING = 0, EDR_FHN_OK = 1, EDR_FHN_PARTIAL, EDR_FHN_REFUSED,
    EDR_FHN_NOT_RESPONDED, EDR_FHN_MAX } edr_fh_node_status_t;

typedef struct edr_fleet_hunt_node {
    uint64_t node_id;
    uint32_t status, stop_reason;      /* edr_fh_node_status_t / edr_reason_t   */
    uint32_t executed, n_steps;
    uint64_t events, bytes;
    uint32_t observability, _pad;
} edr_fleet_hunt_node_t;

typedef struct edr_fleet_hunt {
    uint8_t  recipe_digest[32];
    uint32_t scope, n_nodes;
    edr_hunt_budget_t per_node_budget;
    uint64_t issued_ns, deadline_ns;
    edr_fleet_hunt_node_t nodes[EDR_FLEET_HUNT_NODES_MAX];
} edr_fleet_hunt_t;

typedef struct edr_fleet_hunt_summary {
    uint32_t n_nodes, ok, partial, refused, not_responded, pending;
    uint64_t events_total;
    uint32_t has_nonresponse_column;   /* 1 — столбец присутствует (обязателен) */
    uint32_t _pad;
} edr_fleet_hunt_summary_t;

/* fan-out: рецепт обязан быть скомпилирован и подписан; scope SELECTION
 * требует непустой список узлов. */
int  edr_fleet_hunt_init(edr_fleet_hunt_t *h, const edr_hunt_recipe_t *r, uint32_t scope,
                         const uint64_t *nodes, uint32_t n, const edr_hunt_budget_t *per_node,
                         uint64_t issued_ns, uint64_t deadline_ns);
/* Результат узла (с его edr_hunt_result_t); после deadline неответившие —
 * NOT_RESPONDED, не «ok». */
int  edr_fleet_hunt_node_result(edr_fleet_hunt_t *h, uint64_t node_id, const edr_hunt_result_t *res);
int  edr_fleet_hunt_close(edr_fleet_hunt_t *h, uint64_t now_ns, uint32_t *not_responded_out);
int  edr_fleet_hunt_summary(const edr_fleet_hunt_t *h, edr_fleet_hunt_summary_t *out);
/* Проверка сводки перед публикацией: без столбца неответивших — SCHEMA. */
int  edr_fleet_hunt_summary_validate(const edr_fleet_hunt_summary_t *s);
const char *edr_fh_node_status_name(uint32_t s);
include/platx/edr_host.h
/* platx/edr_host.h — CLI домена EDR (host-часть).
 *
 * Та же граница, что у NDR/PXSIG/SANDBOX: ядро EDR (src/edr/edr_*.c) не
 * делает I/O, не читает часы и не выделяет память; чтение файлов и печать
 * живут здесь и в src/edr/{edr_cli,cmd_edr,edrctl_main}.c. Логика печатает
 * в FILE*, чтобы её можно было прогнать без консоли; cmd_edr.c — тонкий
 * адаптер к console_printf; edrctl_main.c — автономный бинарь.
 *
 * Инварианты (ТЗ §10.2):
 *  EXF-L-01 usage начинается с глагола; --json — тот же путь, что текст;
 *  EXF-L-02 команда чтения не меняет состояние; intent без authority
 *           печатает REFUSED с причиной, не «ok»;
 *  EXF-L-03 CLI-бюджет 10/с (TL3+ — 2/с), lockout 30 с, отказ считается;
 *  EXF-L-04 секреты и байты улик не печатаются — только id и digest'ы.
 *
 * Время: домен часов не читает, поэтому CLI получает его ТОЛЬКО через
 * `--now <ns>`; без него команды, которым нужно время, отвечают
 * TIME_UNTRUSTED, а не берут clock_gettime с заднего хода.
 */
int  edr_cli(int argc, char **argv, FILE *out, FILE *err);
int  edr_cli_script(const char *path, FILE *out, FILE *err);
void edr_register_console(void);
/* Сброс состояния процесса (тесты). */
void edr_cli_reset(void);
include/platx/edr_hunt.h
/* platx/edr_hunt.h — hunt: подписанный bounded read-only рецепт.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §4.6 (EXF-U-01…07).
 *
 * Рецепт — DAG типизированных операций из ЗАКРЫТОГО списка, каждая с
 * effect_class OBSERVE по построению: другой класс здесь невыразим, а
 * шаг, объявивший иное, отвергается при компиляции. Произвольный shell,
 * строка osquery или VQL не являются рецептом — их негде записать.
 *
 * Исполнение идёт через vtable провайдера наблюдений, который даёт хост:
 * hunt не читает /proc и не открывает файлы сам. Каждый вызов возвращает
 * потраченные события/байты; бюджет проверяется до вызова и после; при
 * исчерпании — остановка с кодом и пометка PARTIAL, никакой деградации
 * без счётчика (EXF-U-01, R2).
 *
 * Ретроспектива: прогон несёт as_known_then — генерацию intel и отсечку
 * времени; найденное помечено RETROSPECTIVE и не смешивается с тем, что
 * было известно тогда (EXF-U-02).
 *
 * Возврат сенсоров в штатный режим — узел рецепта: ARM без RESTORE
 * компиляцию не проходит (EXF-U-07).
 */

#define EDR_HUNT_ABI        1u
#define EDR_HUNT_STEPS_MAX  16u
#define EDR_HUNT_NAME_MAX   32u
#define EDR_HUNT_ARGS       4u
#define EDR_HUNT_MEASURES_MAX 16u

/* Закрытый список операций; все — OBSERVE. Расширение = новая версия ABI. */
typedef enum edr_hunt_op {
    EDR_HOP_NONE = 0,
    EDR_HOP_ENTITY_QUERY = 1,   /* сущности по kind/маске                   */
    EDR_HOP_PROCESS_ANCESTRY,   /* цепочка SPAWNED                          */
    EDR_HOP_STORYLINE_NEIGHBORS,
    EDR_HOP_TIMELINE_RANGE,     /* SENSE timeline [t0,t1]                   */
    EDR_HOP_WITNESS_SNAPSHOT,   /* bounded snapshot одного subject           */
    EDR_HOP_INTEL_MATCH,        /* сверка с snapshot PXSIG generation        */
    EDR_HOP_EVIDENCE_VERIFY,    /* fx_verify handle                          */
    EDR_HOP_COVERAGE,           /* coverage snapshot                         */
    EDR_HOP_RECORDER_ARM,       /* глубокий профиль на сущность, с deadline  */
    EDR_HOP_RECORDER_RESTORE,   /* возврат сенсоров — обязательная пара ARM  */
    EDR_HOP_MAX
} edr_hunt_op_t;

typedef enum edr_hunt_effect { EDR_HEFF_OBSERVE = 1, EDR_HEFF_OTHER = 2 } edr_hunt_effect_t;

typedef struct edr_hunt_step {
    uint32_t step_id;           /* 1..MAX                                   */
    uint32_t op;                /* edr_hunt_op_t                            */
    uint32_t effect_class;      /* обязано быть OBSERVE                     */
    uint32_t prereq_mask;       /* биты (step_id-1)                         */
    uint64_t args[EDR_HUNT_ARGS];
    uint32_t privacy_class;     /* ≥ PII требует lease при run              */
    uint32_t _pad;
} edr_hunt_step_t;

typedef struct edr_hunt_budget {
    uint32_t max_steps, max_events;
    uint64_t max_bytes, deadline_ns;   /* deadline — абсолютный monotonic   */
    uint32_t max_privacy_reads;
    uint32_t observability_cap;        /* суммарная «заметность» ≤           */
} edr_hunt_budget_t;

typedef struct edr_hunt_recipe {
    uint32_t abi_version, struct_size;
    char     name[EDR_HUNT_NAME_MAX];
    uint32_t n_steps, _pad;
    edr_hunt_step_t steps[EDR_HUNT_STEPS_MAX];
    edr_hunt_budget_t budget;
    uint32_t observability_cost;       /* заявленная заметность противнику   */
    uint32_t signer_key_id;
    uint8_t  digest[32];               /* над каноническим wire без подписи  */
    uint8_t  signature[64];
} edr_hunt_recipe_t;

/* Провайдер наблюдений от хоста. Каждая функция читает; возвращает
 * edr_reason_t и заполняет расход. Отсутствие функции = UNSUPPORTED. */
typedef struct edr_hunt_io {
    uint64_t events, bytes;
    uint32_t privacy_reads, observability;
} edr_hunt_io_t;
typedef struct edr_hunt_provider {
    void *ctx;
    int (*run)(void *ctx, const edr_hunt_step_t *st, uint64_t lease_id, edr_hunt_io_t *io);
    /* проверка подписи рецепта: 0 — верна, -1 — нет */
    int (*verify_sig)(void *ctx, uint32_t key_id, const uint8_t digest[32], const uint8_t sig[64]);
} edr_hunt_provider_t;

typedef enum edr_hunt_status { EDR_HST_NONE = 0, EDR_HST_OK = 1, EDR_HST_SKIPPED_PREREQ,
    EDR_HST_REFUSED, EDR_HST_BUDGET, EDR_HST_UNSUPPORTED, EDR_HST_PRIVACY, EDR_HST_MAX } edr_hunt_status_t;

typedef struct edr_hunt_result {
    uint32_t n_steps, executed;
    uint32_t status[EDR_HUNT_STEPS_MAX];
    uint32_t rc[EDR_HUNT_STEPS_MAX];
    edr_hunt_io_t total;
    uint32_t partial;                  /* 1 — остановлен по бюджету/отказу   */
    uint32_t stop_reason;              /* edr_reason_t                       */
    uint64_t as_known_then_ns;         /* отсечка знания для retro           */
    uint64_t intel_generation;
    uint32_t retrospective;            /* 1 — результаты помечены RETRO     */
    uint32_t restored;                 /* 1 — RESTORE выполнен после ARM    */
} edr_hunt_result_t;

/* Кандидат измерения для ранжирования (EXF-U-04/05/06). Всё в тысячных. */
typedef struct edr_measure {
    uint32_t id;
    uint32_t eig_milli, evsi_milli, cost_milli, observability_milli;
    uint32_t perishability_half_life_s; /* 0 — не портится                   */
    uint32_t _pad;
} edr_measure_t;

int  edr_hunt_recipe_init(edr_hunt_recipe_t *r, const char *name, const edr_hunt_budget_t *b);
int  edr_hunt_step_add(edr_hunt_recipe_t *r, uint32_t op, uint32_t prereq_mask,
                       const uint64_t args[EDR_HUNT_ARGS], uint32_t privacy_class);
/* Компиляция: закрытый список, OBSERVE, DAG без цикла, ARM↔RESTORE пара,
 * бюджет ненулевой. Считает digest. */
int  edr_hunt_compile(edr_hunt_recipe_t *r);
int  edr_hunt_wire(const edr_hunt_recipe_t *r, uint8_t *out, size_t cap);  /* без подписи */
/* Запуск: подпись обязательна (нет verify_sig или отказ → REFUSED); lease для
 * privacy ≥ PII; бюджет; as_known_then/intel_generation для retro. */
int  edr_hunt_run(const edr_hunt_recipe_t *r, const edr_hunt_provider_t *p,
                  uint64_t lease_id, uint64_t now_ns, uint64_t as_known_then_ns,
                  uint64_t intel_generation, edr_hunt_result_t *out);
/* Ранжирование измерений: детерминированное, целочисленное; tie — по id.
 * Возвращает число записанных индексов. */
int  edr_measure_rank(const edr_measure_t *m, uint32_t n, uint32_t mirage_active,
                      uint32_t *order_out, size_t cap);
int32_t edr_measure_score(const edr_measure_t *m, uint32_t mirage_active);
const char *edr_hunt_op_name(uint32_t op);
const char *edr_hunt_status_name(uint32_t s);
include/platx/edr_hypothesis.h
/* platx/edr_hypothesis.h — набор гипотез вместо поля verdict.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §4.3 (EXF-H-01…07), правила R3, R4, R6.
 *
 * Инцидент не хранит вердикт. Он хранит НАБОР конкурирующих гипотез и
 * матрицу «свидетельство × гипотеза» (ячейки). Вердикт — проекция набора
 * через детерминированный gate, и он пересчитывается, а не запоминается:
 * запомненный вердикт переживает свидетельства, на которых стоял.
 *
 * Три вещи, которые набор не позволяет сделать:
 *  1. закрыть инцидент без BENIGN_EXPLANATION, DATA_QUALITY и RESIDUAL в
 *     наборе — иначе «атака» побеждает у пустого стола (EXF-H-02);
 *  2. засчитать одно свидетельство дважды через две ячейки с одной
 *     independence_group (EXF-H-04);
 *  3. принять вердикт, у которого решающая ячейка заполнена моделью
 *     (INFERRED) или слабейший свидетель ниже требуемого уровня (EXF-H-03,
 *     R6). Такие ячейки участвуют только в posterior_display.
 *
 * Posterior считается в тысячных (0..1000) целочисленно, чтобы результат был
 * одинаков на любом build: float в gate — источник «почти» (C17).
 */

#define EDR_HYP_ABI        1u
#define EDR_HYPS_MAX       8u
#define EDR_HYP_CELLS_MAX  64u

typedef enum edr_epistemic { EDR_EP_NONE = 0, EDR_EP_OBSERVED = 1, EDR_EP_INFERRED,
    EDR_EP_GAP, EDR_EP_UNOBSERVED, EDR_EP_UNOBSERVABLE, EDR_EP_MAX } edr_epistemic_t;

typedef enum edr_hyp_role { EDR_HYP_NONE = 0, EDR_HYP_ATTACK = 1, EDR_HYP_BENIGN,
    EDR_HYP_DATA_QUALITY, EDR_HYP_RESIDUAL, EDR_HYP_ROLE_MAX } edr_hyp_role_t;

/* grade — качество основания набора; закрытая шкала */
typedef enum edr_grade { EDR_GRADE_NONE = 0, EDR_GRADE_LOW, EDR_GRADE_MODERATE,
    EDR_GRADE_HIGH, EDR_GRADE_MAX } edr_grade_t;

typedef struct edr_hyp_cell {
    uint64_t evidence_id;          /* finding / observation / witness claim   */
    uint32_t hypothesis;           /* индекс гипотезы 0..n_hyp-1              */
    uint32_t independence_group;   /* 0 — не заявлена (считается уникальной)  */
    uint32_t epistemic;            /* edr_epistemic_t                         */
    int32_t  llr_milli;            /* log-likelihood ratio ×1000, знак = стан */
    uint32_t diagnosticity_milli;  /* 0..1000                                 */
    uint32_t observation_level;    /* edr_obs_level_t                         */
} edr_hyp_cell_t;

typedef struct edr_hypothesis_set {
    uint32_t abi_version, struct_size;
    uint64_t set_id, incident_id;
    uint32_t n_hyp, n_cells, revision;
    uint32_t _pad;
    uint32_t roles[EDR_HYPS_MAX];
    int32_t  prior_milli[EDR_HYPS_MAX];          /* сумма = 1000                */
    /* вычисляемые проекцией: */
    uint32_t posterior_det_milli[EDR_HYPS_MAX];  /* только не-INFERRED ячейки   */
    uint32_t posterior_disp_milli[EDR_HYPS_MAX]; /* все ячейки                  */
    uint32_t grade[EDR_HYPS_MAX];
    uint32_t weakest_level[EDR_HYPS_MAX];
    edr_hyp_cell_t cells[EDR_HYP_CELLS_MAX];
} edr_hypothesis_set_t;

typedef struct edr_verdict_gate {
    uint32_t min_posterior_milli;   /* победитель ≥                            */
    uint32_t min_grade;             /* edr_grade_t                             */
    uint32_t max_competitor_milli;  /* лучший конкурент <                      */
    uint32_t min_witness_level;     /* edr_obs_level_t слабейшего свидетеля ≥  */
} edr_verdict_gate_t;

typedef enum edr_verdict_outcome { EDR_VERD_NONE = 0, EDR_VERD_ACCEPTED = 1,
    EDR_VERD_INCONCLUSIVE, EDR_VERD_INSUFFICIENT, EDR_VERD_MAX } edr_verdict_outcome_t;

#define EDR_MISS_POSTERIOR    (1u<<0)
#define EDR_MISS_GRADE        (1u<<1)
#define EDR_MISS_COMPETITOR   (1u<<2)
#define EDR_MISS_INFERRED     (1u<<3)  /* решающая ячейка от модели           */
#define EDR_MISS_WITNESS      (1u<<4)  /* слабейший свидетель ниже gate       */
#define EDR_MISS_NO_CELLS     (1u<<5)
#define EDR_MISS_SCHEMA       (1u<<6)  /* набор без обязательных ролей        */

typedef struct edr_verdict {          /* проекция; никогда не хранится как факт */
    uint32_t outcome;                 /* edr_verdict_outcome_t                */
    uint32_t winner;                  /* индекс гипотезы при ACCEPTED         */
    uint32_t winner_posterior_milli;
    uint32_t display_posterior_milli; /* с INFERRED — для оператора            */
    uint32_t weakest_level;
    uint32_t missing_mask;            /* чего именно не хватает                */
    uint32_t decisive_cells;          /* сколько ячеек оказались решающими     */
    uint32_t _pad;
} edr_verdict_t;

/* Запись ревизии гипотезы для FORENSIC (класс HYPOTHESIS_REVISION). */
typedef struct edr_hyp_revision {
    uint64_t set_id, incident_id;
    uint32_t revision, hypothesis;
    uint32_t posterior_before_milli, posterior_after_milli;
    uint32_t reason;                  /* edr_hyp_reason_t                     */
    uint32_t cell_evidence_hi;        /* старшие 32 бита evidence_id ячейки   */
    uint64_t evidence_id;
} edr_hyp_revision_t;

typedef enum edr_hyp_reason { EDR_HYPR_NONE = 0, EDR_HYPR_CELL_ADDED, EDR_HYPR_CELL_REFUTED,
    EDR_HYPR_PRIOR_CHANGED, EDR_HYPR_HYP_ADDED, EDR_HYPR_MAX } edr_hyp_reason_t;

typedef int (*edr_hyp_hook_t)(const edr_hyp_revision_t *rev, void *ud);

int  edr_hypset_init(edr_hypothesis_set_t *s, uint64_t set_id, uint64_t incident_id);
/* Добавить гипотезу с ролью и prior (тысячные). Сумма prior проверяется при
 * validate; RESIDUAL — ровно один. */
int  edr_hypset_add(edr_hypothesis_set_t *s, uint32_t role, int32_t prior_milli, uint32_t *idx_out);
/* EXF-H-02: без BENIGN, DATA_QUALITY и ровно одного RESIDUAL набор — SCHEMA. */
int  edr_hypset_validate(const edr_hypothesis_set_t *s);
/* Ячейка. epistemic обязателен; INFERRED помечается и не входит в gate.
 * hook (если задан) получает запись ревизии до/после. */
int  edr_hypset_cell(edr_hypothesis_set_t *s, const edr_hyp_cell_t *c,
                     edr_hyp_hook_t hook, void *ud);
/* Опровержение: помечает ячейки свидетельства как refuted (llr → 0, epistemic
 * сохраняется), пишет ревизию. */
int  edr_hypset_refute(edr_hypothesis_set_t *s, uint64_t evidence_id,
                       edr_hyp_hook_t hook, void *ud);
/* Пересчёт posterior_det/disp, grade, weakest_level. Детерминирован. */
int  edr_hypset_recompute(edr_hypothesis_set_t *s);
/* Проекция вердикта через gate. Всегда заполняет out; возврат — edr_reason_t
 * (OK при ACCEPTED, INCONCLUSIVE/INSUFFICIENT_EVIDENCE/SCHEMA иначе). */
int  edr_verdict_project(const edr_hypothesis_set_t *s, const edr_verdict_gate_t *g,
                         edr_verdict_t *out);
/* Диагностичность свидетельства по набору: 0 — совместимо со всеми (не двигает
 * расследование, EXF-H-05); растёт с разбросом llr между гипотезами. */
uint32_t edr_hypset_diagnosticity(const edr_hypothesis_set_t *s, uint64_t evidence_id);
void edr_verdict_gate_defaults(edr_verdict_gate_t *g);
/* Канонический wire набора (для package): фиксированная длина. */
#define EDR_HYPSET_WIRE_BYTES (4u+4u+8u+8u+4u+4u+4u + EDR_HYPS_MAX*(4u+4u) + EDR_HYP_CELLS_MAX*(8u+4u+4u+4u+4u+4u+4u))
int  edr_hypset_wire(const edr_hypothesis_set_t *s, uint8_t *out, size_t cap);
int  edr_hypset_unwire(const uint8_t *in, size_t len, edr_hypothesis_set_t *out);
#define EDR_VERDICT_WIRE_BYTES (7u*4u + 4u*4u)
int  edr_verdict_wire(const edr_verdict_t *v, const edr_verdict_gate_t *g, uint8_t *out, size_t cap);
int  edr_verdict_unwire(const uint8_t *in, size_t len, edr_verdict_t *v, edr_verdict_gate_t *g);
const char *edr_hyp_role_name(uint32_t r);
const char *edr_epistemic_name(uint32_t e);
const char *edr_verdict_outcome_name(uint32_t o);
include/platx/edr_incident.h
/* platx/edr_incident.h — инцидент EDR: неизменяемые ревизии, машина состояний.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §4.2 (EXF-I-01…06), правила R1, R2, R5.
 *
 * Почему ревизии, а не поля. Поле «state», которое перезаписывается, не
 * отвечает на вопрос «почему инцидент оказался здесь»: история стёрта самим
 * актом изменения. Здесь каждое изменение — новая ревизия с reason_code,
 * ссылкой на digest предыдущей и каноническим wire-образом, из которого
 * инцидент восстанавливается без обращения к живому состоянию (EXF-01).
 *
 * Почему таблица bounded и статическая. Ёмкость — из профиля, не из кучи
 * (MIL-1). Переполнение — отказ с кодом и счётчиком, никогда тихое
 * переиспользование id (R2). Старые ревизии вытесняются из live-окна только
 * после того, как hook (FORENSIC) получил их копию.
 *
 * Здесь нет I/O, нет malloc, нет часов: время приходит аргументом, чтобы
 * CLOCK_ROLLBACK был выразимым состоянием, а не ошибкой библиотеки.
 */

#define EDR_INCIDENT_ABI            1u
#define EDR_INCIDENTS_MAX           256u   /* профиль; MIL-1                 */
#define EDR_INCIDENT_FINDINGS_MAX   64u
#define EDR_INCIDENT_ENTITIES_MAX   16u
#define EDR_INCIDENT_HANDLES_MAX    32u
#define EDR_INCIDENT_GAPS_MAX       16u
#define EDR_INCIDENT_REVISIONS_LIVE 16u

typedef enum edr_incident_state {
    EDR_INC_NONE = 0,
    EDR_INC_NEW = 1, EDR_INC_TRIAGED, EDR_INC_INVESTIGATING,
    EDR_INC_CONTAINING, EDR_INC_CONTAINED, EDR_INC_RECOVERING,
    EDR_INC_RESOLVED, EDR_INC_FALSE_POSITIVE,
    EDR_INC_INSUFFICIENT_EVIDENCE, EDR_INC_INCONCLUSIVE,
    EDR_INC_MAX
} edr_incident_state_t;

/* Причины перехода — закрытый список, попадает в ревизию и в цепочку. */
typedef enum edr_inc_reason {
    EDR_INCR_NONE = 0,
    EDR_INCR_OPENED, EDR_INCR_FINDING_ATTACHED, EDR_INCR_ENTITY_ATTACHED,
    EDR_INCR_HANDLE_ATTACHED, EDR_INCR_GAP_ATTACHED, EDR_INCR_SCORES,
    EDR_INCR_COVERAGE, EDR_INCR_UNCERTAINTY, EDR_INCR_TRIAGE,
    EDR_INCR_INVESTIGATE, EDR_INCR_PLAN_ADMITTED, EDR_INCR_CONTAIN_START,
    EDR_INCR_CONTAIN_VERIFIED, EDR_INCR_RECOVER_START, EDR_INCR_RECOVERED,
    EDR_INCR_OPERATOR_DISPOSITION, EDR_INCR_GATE_INCONCLUSIVE,
    EDR_INCR_GATE_INSUFFICIENT, EDR_INCR_MERGED_INTO, EDR_INCR_MERGE_ABSORB,
    EDR_INCR_SPLIT, EDR_INCR_RECONCILE, EDR_INCR_MAX
} edr_inc_reason_t;

#define EDR_UNC_STALE_EVIDENCE     (1u<<0)
#define EDR_UNC_CONTRADICTION      (1u<<1)
#define EDR_UNC_PROVIDER_LOSS      (1u<<2)
#define EDR_UNC_CLOCK_ROLLBACK     (1u<<3)
#define EDR_UNC_PRIVACY_BUDGET     (1u<<4)
#define EDR_UNC_EVICTION           (1u<<5)
#define EDR_UNC_OBSERVER_DISCREP   (1u<<6)
#define EDR_UNC_SYNTHETIC_TOUCH    (1u<<7)
#define EDR_UNC_EVIDENCE_BYPASSED  (1u<<8)
#define EDR_UNC_OPEN_TXN           (1u<<9)   /* reconcile: незавершённая txn */

typedef enum edr_entity_kind { EDR_ENT_NONE = 0, EDR_ENT_PROCESS, EDR_ENT_FILE,
    EDR_ENT_SOCKET, EDR_ENT_IDENTITY, EDR_ENT_MODULE, EDR_ENT_HOST,
    EDR_ENT_ARTIFACT, EDR_ENT_SERVICE, EDR_ENT_MAX } edr_entity_kind_t;

typedef struct edr_entity_ref {
    uint32_t kind;              /* edr_entity_kind_t                       */
    uint32_t observation_level; /* edr_obs_level_t                         */
    uint64_t entity_id;         /* SENSE entity id                         */
    uint64_t generation;        /* birth/generation ИЗ ФАКТА; 0 — отказ    */
} edr_entity_ref_t;

typedef struct edr_coverage_snapshot {
    uint64_t taken_ns;
    uint32_t sources_ok, sources_degraded, sources_blind, gaps;
    uint32_t weakest_level;     /* edr_obs_level_t                         */
    uint32_t freshness_ms;
    uint32_t loss_ppm;
    uint32_t _pad;
} edr_coverage_snapshot_t;

typedef struct edr_incident_revision {
    uint32_t abi_version, struct_size;
    uint64_t incident_id;
    uint32_t revision_no;
    uint32_t state;             /* edr_incident_state_t                    */
    uint32_t reason_code;       /* edr_inc_reason_t                        */
    uint32_t severity;          /* 0-100                                   */
    uint32_t confidence;        /* sense_confidence_t: 0..3                */
    uint32_t potential_impact;  /* 0-100, отдельно от severity             */
    uint32_t uncertainty_mask;  /* EDR_UNC_*                               */
    uint32_t scope_node;        /* хэш/id узла                             */
    uint32_t n_findings, n_entities, n_handles, n_gaps;
    uint64_t hypothesis_set_id;
    uint64_t policy_generation, pack_generation;
    uint64_t monotonic_ns, wall_ns_claim;
    uint64_t merged_from_id;    /* 0 — не результат merge                  */
    uint32_t pre_merge_findings;/* сколько findings было до merge          */
    uint32_t disposition;       /* edr_disposition_t                       */
    uint8_t  response_plan_digest[32];
    uint64_t finding_ids[EDR_INCIDENT_FINDINGS_MAX];
    edr_entity_ref_t entities[EDR_INCIDENT_ENTITIES_MAX];
    witness_evidence_ref_t handles[EDR_INCIDENT_HANDLES_MAX];
    uint64_t gap_record_ids[EDR_INCIDENT_GAPS_MAX];
    edr_coverage_snapshot_t coverage;
    uint8_t  prev_revision_digest[32];
    uint8_t  attck[8];
} edr_incident_revision_t;

typedef enum edr_disposition { EDR_DISP_NONE = 0, EDR_DISP_TRUE_POSITIVE,
    EDR_DISP_FALSE_POSITIVE, EDR_DISP_BENIGN, EDR_DISP_INCONCLUSIVE,
    EDR_DISP_MAX } edr_disposition_t;

/* Hook: FORENSIC получает каждую ревизию ДО того, как она может быть
 * вытеснена из live-окна. Возврат ≠ 0 означает, что sink потерян: ревизия
 * всё равно принимается (инцидент не может «не измениться»), но таблица
 * поднимает sink_lost и бит EDR_UNC_STALE_EVIDENCE не ставится — вместо
 * этого счётчик sink_failures виден снаружи (R2). */
typedef int (*edr_revision_hook_t)(const edr_incident_revision_t *rev, void *ud);

typedef struct edr_incident_slot {
    int      in_use;
    uint64_t incident_id;
    uint32_t n_live, head;       /* кольцо ревизий                          */
    uint32_t evicted_revisions;
    uint32_t _pad;
    edr_incident_revision_t rev[EDR_INCIDENT_REVISIONS_LIVE];
} edr_incident_slot_t;

typedef struct edr_incidents {
    uint32_t abi_version;
    uint32_t n_in_use;
    uint64_t next_id;
    /* счётчики достижения пределов — R2 */
    uint64_t opened, refused_full, illegal_transitions, evicted_findings,
             evicted_revisions, sink_failures, duplicates_refused, clock_rollbacks;
    edr_revision_hook_t hook;
    void    *hook_ud;
    edr_incident_slot_t slots[EDR_INCIDENTS_MAX];
} edr_incidents_t;

/* Канонический wire ревизии: big-endian, без padding, фиксированные массивы.
 * Размер — константа, чтобы буфер был bounded и digest не зависел от build. */
#define EDR_INC_WIRE_HDR      (4*4 + 8 + 4*4 + 4*4 + 4*4 + 8*3 + 8*2 + 8 + 4 + 4 + 32)
#define EDR_INC_WIRE_ENTITY   (4+4+8+8)
#define EDR_INC_WIRE_HANDLE   (4+4+WITNESS_EVIDENCE_MAX+32+8)
#define EDR_INC_WIRE_COVER    (8+4*7)
#define EDR_INC_WIRE_BYTES    (EDR_INC_WIRE_HDR \
        + 8u*EDR_INCIDENT_FINDINGS_MAX \
        + EDR_INC_WIRE_ENTITY*EDR_INCIDENT_ENTITIES_MAX \
        + EDR_INC_WIRE_HANDLE*EDR_INCIDENT_HANDLES_MAX \
        + 8u*EDR_INCIDENT_GAPS_MAX + EDR_INC_WIRE_COVER + 32u + 8u)

int  edr_incidents_init(edr_incidents_t *t, uint64_t first_id,
                        edr_revision_hook_t hook, void *ud);
int  edr_incident_open(edr_incidents_t *t, uint32_t scope_node,
                       uint64_t hypothesis_set_id, uint64_t policy_gen,
                       uint64_t pack_gen, uint64_t now_ns, uint64_t wall_ns,
                       uint64_t *id_out);
int  edr_incident_state_legal(uint32_t from, uint32_t to);
int  edr_incident_transition(edr_incidents_t *t, uint64_t id, uint32_t to,
                             uint32_t reason, uint64_t now_ns, uint64_t wall_ns);
int  edr_incident_attach_finding(edr_incidents_t *t, uint64_t id,
                                 uint64_t finding_id, uint64_t now_ns, uint64_t wall_ns);
int  edr_incident_attach_entity(edr_incidents_t *t, uint64_t id,
                                const edr_entity_ref_t *e, uint64_t now_ns, uint64_t wall_ns);
int  edr_incident_attach_handle(edr_incidents_t *t, uint64_t id,
                                const witness_evidence_ref_t *h, uint64_t now_ns, uint64_t wall_ns);
int  edr_incident_attach_gap(edr_incidents_t *t, uint64_t id, uint64_t gap_record_id,
                             uint64_t now_ns, uint64_t wall_ns);
int  edr_incident_set_scores(edr_incidents_t *t, uint64_t id, uint32_t severity,
                             uint32_t confidence, uint32_t impact,
                             uint64_t now_ns, uint64_t wall_ns);
int  edr_incident_set_coverage(edr_incidents_t *t, uint64_t id,
                               const edr_coverage_snapshot_t *c,
                               uint64_t now_ns, uint64_t wall_ns);
int  edr_incident_add_uncertainty(edr_incidents_t *t, uint64_t id, uint32_t bits,
                                  uint64_t now_ns, uint64_t wall_ns);
int  edr_incident_set_plan(edr_incidents_t *t, uint64_t id, const uint8_t digest[32],
                           uint64_t now_ns, uint64_t wall_ns);
int  edr_incident_merge(edr_incidents_t *t, uint64_t dst, uint64_t src,
                        uint64_t now_ns, uint64_t wall_ns);
int  edr_incident_split(edr_incidents_t *t, uint64_t id, uint64_t now_ns,
                        uint64_t wall_ns, uint64_t *new_id_out);
int  edr_incident_disposition(edr_incidents_t *t, uint64_t id, uint32_t disp,
                              uint64_t now_ns, uint64_t wall_ns);
const edr_incident_revision_t *edr_incident_latest(const edr_incidents_t *t, uint64_t id);
int  edr_incident_revision_get(const edr_incidents_t *t, uint64_t id,
                               uint32_t revision_no, edr_incident_revision_t *out);
/* Канонический wire и digest. Возврат — байты либо -1. */
int  edr_incident_revision_wire(const edr_incident_revision_t *r, uint8_t *out, size_t cap);
int  edr_incident_revision_unwire(const uint8_t *in, size_t len, edr_incident_revision_t *out);
int  edr_incident_revision_digest(const edr_incident_revision_t *r, uint8_t out[32]);
/* Восстановление из последовательности wire-ревизий (EXF-01): проверяет
 * непрерывность revision_no и prev_revision_digest. Возврат — edr_reason_t. */
int  edr_incident_replay(const uint8_t *const *wires, const size_t *lens, uint32_t n,
                         edr_incident_revision_t *latest_out);
const char *edr_incident_state_name(uint32_t s);
const char *edr_inc_reason_name(uint32_t r);
include/platx/edr_pack.h
/* platx/edr_pack.h — policy packs: подписанное содержимое, транзакционная
 * активация, coverage contract, shadow/canary, drift.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §8 (EXF-K-01…08).
 *
 * Источник содержимого — PXSIG; здесь — потребитель: правило корреляции
 * приходит уже подписанным пакетом, а домен EDR владеет только его
 * ЖИЗНЕННЫМ ЦИКЛОМ: validate → compile → shadow → compare → canary → commit,
 * с поколением и откатом. Второго каталога правил нет.
 *
 * Три отказа, ради которых файл существует:
 *  - правило без replay не активируется, даже если инцидент подтверждён
 *    (EXF-K-03);
 *  - правило, чей coverage contract не удовлетворён, — INACTIVE с причиной,
 *    а не «работает, потому что не срабатывает» (EXF-K-05);
 *  - правило со 100 % FP за период само предлагает себя к отключению
 *    артефактом DRAFT (EXF-K-06).
 */

#define EDR_PACK_ABI       1u
#define EDR_PACK_RULES_MAX 32u
#define EDR_PACK_GENS_KEEP 2u   /* текущее + предыдущее для rollback         */
#define EDR_RULE_WINDOW_EVENTS_MAX 65536u
#define EDR_RULE_WINDOW_SECS_MAX   300u
#define EDR_RULE_DEPTH_MAX         8u

typedef enum edr_rule_kind { EDR_RK_NONE = 0, EDR_RK_SEQUENCE = 1, EDR_RK_CORRELATION,
    EDR_RK_HUNT, EDR_RK_RESPONSE_TEMPLATE, EDR_RK_PREVENTION, EDR_RK_MAX } edr_rule_kind_t;

typedef enum edr_rule_state { EDR_RS_NONE = 0, EDR_RS_ACTIVE = 1, EDR_RS_INACTIVE_MISSING_CAPABILITY,
    EDR_RS_INACTIVE_NO_REPLAY, EDR_RS_DRAFT_DISABLE, EDR_RS_SHADOW, EDR_RS_MAX } edr_rule_state_t;

typedef struct edr_rule {
    uint32_t rule_id, kind;
    uint32_t revision;              /* детерминированная ревизия             */
    uint32_t window_events, window_secs, depth;
    uint32_t severity, confidence_map;
    uint32_t join_key_kind;         /* edr_entity_kind_t                    */
    sense_vis_contract_t coverage;  /* обязателен (EXF-K-05)                */
    uint8_t  attck[8];
    /* состояние lifecycle */
    uint32_t state;                 /* edr_rule_state_t                     */
    uint32_t replay_passed;
    uint32_t coverage_reason;       /* sense_vis_reason_t при INACTIVE      */
    /* drift (EXF-K-06) */
    uint32_t fires, tp, fp;
    uint64_t last_confirmed_ns;
} edr_rule_t;

typedef struct edr_pack {
    uint32_t abi_version, struct_size;
    uint64_t pack_id;
    uint32_t revision, n_rules;
    uint32_t required_kinds_mask;   /* payload types, которые пакет требует */
    uint32_t signer_key_id;
    uint64_t expiry_ns;
    uint32_t compat_min_abi, compat_max_abi;
    uint32_t budget_events_per_s, budget_memory_kib;
    uint64_t rollback_generation;
    uint8_t  digest[32];
    uint8_t  signature[64];
    edr_rule_t rules[EDR_PACK_RULES_MAX];
} edr_pack_t;

typedef enum edr_pack_state { EDR_PS_NONE = 0, EDR_PS_RECEIVED = 1, EDR_PS_VALIDATED, EDR_PS_COMPILED,
    EDR_PS_SHADOW, EDR_PS_COMPARED, EDR_PS_CANARY, EDR_PS_COMMITTED, EDR_PS_ROLLED_BACK,
    EDR_PS_REFUSED, EDR_PS_MAX } edr_pack_state_t;

typedef struct edr_shadow_result {
    uint32_t incidents_old, incidents_new, actions_old, actions_new;
    uint32_t events_replayed, cost_new_ms, disagreements;
    uint32_t replay_fixture_id;     /* зафиксированный replay, не обещание  */
    uint32_t _pad;
} edr_shadow_result_t;

typedef struct edr_pack_slot {
    int      in_use;
    uint32_t state;                 /* edr_pack_state_t                     */
    uint64_t generation;
    edr_pack_t pack;
    edr_shadow_result_t shadow;
    uint32_t canary_permille, canary_secs;
} edr_pack_slot_t;

typedef int (*edr_pack_verify_fn)(void *ctx, uint32_t key_id, const uint8_t digest[32], const uint8_t sig[64]);

typedef struct edr_packs {
    uint64_t generation;            /* текущее активное поколение            */
    uint32_t active;                /* индекс слота COMMITTED или UINT32_MAX */
    uint32_t previous;              /* для rollback                          */
    uint64_t refused, commits, rollbacks, inactive_missing, inactive_no_replay, drift_drafts;
    edr_pack_verify_fn verify; void *verify_ctx;
    edr_pack_slot_t slots[EDR_PACK_GENS_KEEP + 1u];
} edr_packs_t;

int  edr_packs_init(edr_packs_t *p, edr_pack_verify_fn verify, void *ctx);
int  edr_pack_init(edr_pack_t *pk, uint64_t pack_id, uint32_t revision, uint32_t signer_key_id,
                   uint64_t expiry_ns);
int  edr_pack_rule_add(edr_pack_t *pk, const edr_rule_t *r);
/* digest над каноническим wire (без подписи); pack «подписывается» снаружи */
int  edr_pack_digest(const edr_pack_t *pk, uint8_t out[32]);
/* RECEIVED: копия в свободный слот. */
int  edr_pack_receive(edr_packs_t *p, const edr_pack_t *pk, uint32_t *slot_out);
/* VALIDATED: подпись, expiry, compat, границы правил (окно, глубина, budget). */
int  edr_pack_validate(edr_packs_t *p, uint32_t slot, uint64_t now_ns, uint32_t edr_abi);
/* COMPILED: coverage contract каждого правила против наблюдаемого покрытия. */
int  edr_pack_compile(edr_packs_t *p, uint32_t slot, const sense_vis_observed_t *observed);
/* SHADOW → COMPARED: результат replay на зафиксированном fixture. */
int  edr_pack_shadow(edr_packs_t *p, uint32_t slot, const edr_shadow_result_t *res);
/* CANARY: доля и длительность; 0 — отказ. */
int  edr_pack_canary(edr_packs_t *p, uint32_t slot, uint32_t permille, uint32_t secs);
/* COMMITTED: generation++, предыдущее сохранено; правило без replay — отказ. */
int  edr_pack_commit(edr_packs_t *p, uint32_t slot, uint64_t *generation_out);
int  edr_pack_rollback(edr_packs_t *p, uint64_t *generation_out);
/* drift: пометить fires/tp/fp; правило с fp==fires≥min_fires за период → DRAFT_DISABLE */
int  edr_pack_note_outcome(edr_packs_t *p, uint32_t rule_id, int true_positive, uint64_t now_ns);
int  edr_pack_drift(edr_packs_t *p, uint32_t min_fires, uint64_t period_ns, uint64_t now_ns,
                    uint32_t *drafts_out);
const edr_rule_t *edr_pack_rule(const edr_packs_t *p, uint32_t rule_id);
const char *edr_pack_state_name(uint32_t s);
const char *edr_rule_state_name(uint32_t s);
include/platx/edr_projection.h
/* platx/edr_projection.h — подписанные минимальные проекции узла (XDR).
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §5.3 (EXF-X-04, X-05, X-07).
 *
 * XDR — режим, не продукт: каталога src/xdr нет (EXF-X-01). Node публикует
 * ПОДПИСАННЫЕ bounded-проекции: сводка инцидента, digest'ы сущностей с
 * generation, проекции гипотез (id, posterior_det, grade — без сырых
 * ячеек), evidence handles (digest, не байты), сводка покрытия, кандидаты
 * вердикта, отчёт об ослеплении. Проекция — структура фиксированного
 * размера: поля, способного нести байты улики или сырую телеметрию, в ней
 * нет по построению; это и есть проверяемая часть «privacy-preserving».
 *
 * Кадр: schema version, node_id, sequence (per-node), policy generation,
 * issued_ns, kind, payload, signer_key_id, signature над каноническим wire.
 * Несовместимая схема — отказ с причиной, не best-effort (EXF-X-07).
 */

#define EDR_PROJ_SCHEMA      1u
#define EDR_PROJ_ENTITIES    8u
#define EDR_PROJ_HANDLES     8u
#define EDR_PROJ_HYPS        EDR_HYPS_MAX

typedef enum edr_proj_kind {
    EDR_PJ_NONE = 0,
    EDR_PJ_INCIDENT_SUMMARY = 1,
    EDR_PJ_ENTITY_DIGEST,
    EDR_PJ_HYPOTHESIS,
    EDR_PJ_EVIDENCE_HANDLE,
    EDR_PJ_COVERAGE,
    EDR_PJ_VERDICT_CANDIDATE,
    EDR_PJ_ANOMALY_PATTERN,
    EDR_PJ_BLINDING_REPORT,      /* потеря sensor/audit/control на узле     */
    EDR_PJ_MAX
} edr_proj_kind_t;

typedef struct edr_proj_entity {
    uint64_t entity_id, generation;
    uint32_t kind, observation_level;
    uint8_t  digest[32];             /* identity digest (exe/identity/…)     */
    uint8_t  parent_digest[32];      /* нули — нет                            */
    uint64_t name_hash;              /* хэш имени; само имя не передаётся    */
} edr_proj_entity_t;

typedef struct edr_proj_hyp {
    uint32_t role;                   /* edr_hyp_role_t                        */
    uint32_t posterior_det_milli;
    uint32_t grade;
    uint32_t weakest_level;
} edr_proj_hyp_t;

/* Полезная нагрузка — фиксированный union; размер не зависит от kind. */
typedef struct edr_proj_payload {
    /* INCIDENT_SUMMARY / VERDICT_CANDIDATE */
    uint64_t incident_id;
    uint32_t state, severity, confidence, revision_no;
    uint32_t verdict_outcome, missing_mask;
    uint32_t uncertainty_mask, _pad0;
    edr_coverage_snapshot_t coverage;
    /* ENTITY_DIGEST */
    uint32_t n_entities, _pad1;
    edr_proj_entity_t entities[EDR_PROJ_ENTITIES];
    /* HYPOTHESIS */
    uint64_t hypothesis_set_id;
    uint32_t n_hyps, _pad2;
    edr_proj_hyp_t hyps[EDR_PROJ_HYPS];
    /* EVIDENCE_HANDLE */
    uint32_t n_handles, _pad3;
    uint8_t  handle_digest[EDR_PROJ_HANDLES][32];
    /* ANOMALY_PATTERN / BLINDING_REPORT */
    uint32_t pattern_id;             /* закрытый список у отправителя         */
    uint32_t cf_class;               /* edr_cf_class_t для BLINDING_REPORT    */
    uint64_t pattern_t_ns;
} edr_proj_payload_t;

typedef struct edr_projection {
    uint32_t schema_version;
    uint32_t kind;
    uint64_t node_id;
    uint64_t sequence;               /* per-node, монотонно                   */
    uint64_t policy_generation;
    uint64_t issued_ns;
    uint32_t tenant_id;              /* проекции разных tenant не смешиваются */
    uint32_t signer_key_id;
    edr_proj_payload_t p;
    uint8_t  digest[32];             /* над wire без подписи                  */
    uint8_t  signature[64];
} edr_projection_t;

#define EDR_PROJ_WIRE_HDR   (4u+4u+8u+8u+8u+8u+4u+4u)
#define EDR_PROJ_WIRE_PAY   (8u+4u*8u + (8u+4u*7u) + 4u+4u + EDR_PROJ_ENTITIES*(8u+8u+4u+4u+32u+32u+8u) \
                             + 8u+4u+4u + EDR_PROJ_HYPS*(4u*4u) + 4u+4u + EDR_PROJ_HANDLES*32u + 4u+4u+8u)
#define EDR_PROJ_WIRE_BYTES (EDR_PROJ_WIRE_HDR + EDR_PROJ_WIRE_PAY + 64u)

typedef int (*edr_proj_sign_fn)(void *ctx, const uint8_t *data, size_t len, uint8_t sig[64]);
typedef int (*edr_proj_verify_fn)(void *ctx, uint32_t key_id, uint64_t node_id,
                                  const uint8_t *data, size_t len, const uint8_t sig[64]);

/* Построение из ревизии инцидента / набора гипотез: только сводки. */
int  edr_proj_init(edr_projection_t *p, uint32_t kind, uint64_t node_id, uint64_t sequence,
                   uint64_t policy_generation, uint32_t tenant_id, uint64_t issued_ns);
int  edr_proj_from_incident(edr_projection_t *p, const edr_incident_revision_t *r);
int  edr_proj_from_hypset(edr_projection_t *p, const edr_hypothesis_set_t *s);
int  edr_proj_from_verdict(edr_projection_t *p, uint64_t incident_id, const edr_verdict_t *v);
int  edr_proj_entity_add(edr_projection_t *p, const edr_proj_entity_t *e);
int  edr_proj_handle_add(edr_projection_t *p, const uint8_t digest[32]);
int  edr_proj_blinding(edr_projection_t *p, uint32_t cf_class, uint64_t t_ns);
/* Канонический wire (BE, фиксированная длина). Возврат — байты или -1. */
int  edr_proj_wire(const edr_projection_t *p, uint8_t *out, size_t cap);
/* Разбор: схема ≠ EDR_PROJ_SCHEMA → EDR_R_VERSION; kind вне списка → SCHEMA. */
int  edr_proj_unwire(const uint8_t *in, size_t len, edr_projection_t *out);
int  edr_proj_sign(edr_projection_t *p, uint32_t key_id, edr_proj_sign_fn sign, void *ctx);
/* Проверка подписи по (key_id, node_id): подпись Central'а за узел — не подпись узла. */
int  edr_proj_verify(const edr_projection_t *p, edr_proj_verify_fn verify, void *ctx);
const char *edr_proj_kind_name(uint32_t k);
include/platx/edr_reason.h
/* platx/edr_reason.h — закрытый список причин домена EDR/XDR/FORENSIC.
 *
 * Причина — число из закрытого списка, никогда свободный текст: текст
 * нельзя сравнить в тесте, нельзя посчитать в счётчике и нельзя отличить
 * от опечатки. Каждый отказ в домене возвращает ровно одну причину, и её
 * имя печатается edr_reason_name(), которая никогда не возвращает NULL —
 * неизвестный код печатается как "?" и ловится тестом на полноту таблицы.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §7.5, правило R5.
 */

typedef enum edr_reason {
    EDR_R_OK                          = 0,
    /* отказы полномочия — у EDR их нет по построению (EXF-C2) */
    EDR_R_REFUSED_NO_LEASE            = 1,
    EDR_R_REFUSED_NO_AUTHORITY        = 2,
    EDR_R_REFUSED_NO_EVIDENCE         = 3,
    EDR_R_REFUSED_COVERAGE            = 4,
    EDR_R_REFUSED_ROLLBACK_UNTESTED   = 5,
    EDR_R_REFUSED_DRYRUN_UNSUPPORTED  = 6,
    EDR_R_REFUSED_CEILING             = 7,
    EDR_R_REFUSED_ENVELOPE            = 8,
    EDR_R_REFUSED_BUDGET              = 9,
    EDR_R_REFUSED_STALE_GENERATION    = 10,
    EDR_R_REFUSED_PINNING             = 11,
    EDR_R_REFUSED_SYNTHETIC           = 12,
    EDR_R_REFUSED_KILLED              = 13,
    EDR_R_REFUSED_MODE                = 14,  /* PLAN/SHADOW не исполняются   */
    /* исходы расследования (штатные, не ошибки) */
    EDR_R_INCONCLUSIVE                = 20,
    EDR_R_INSUFFICIENT_EVIDENCE       = 21,
    EDR_R_INDETERMINATE_BLIND         = 22,
    /* ёмкости и целостность */
    EDR_R_FULL                        = 30,
    EDR_R_EVICTED                     = 31,
    EDR_R_TAMPER                      = 32,
    EDR_R_SINK_LOST                   = 33,
    EDR_R_NOLINK                      = 34,
    EDR_R_GAP                         = 35,
    /* аргументы и состояние */
    EDR_R_ARGS                        = 40,
    EDR_R_STATE                       = 41,  /* нелегальный переход           */
    EDR_R_NOT_FOUND                   = 42,
    EDR_R_DUPLICATE                   = 43,
    EDR_R_SCHEMA                      = 44,  /* набор без BENIGN/DQ/RESIDUAL  */
    EDR_R_VERSION                     = 45,
    EDR_R_NO_PARENT                   = 46,  /* CAUSES без явного parent      */
    EDR_R_INFERRED_DECISIVE           = 47,  /* решающая ячейка от модели     */
    EDR_R_WITNESS_LEVEL               = 48,  /* слабейший свидетель ниже gate */
    EDR_R_INDEPENDENCE                = 49,  /* та же independence_group      */
    EDR_R_PRIVACY                     = 50,
    EDR_R_DEADLINE                    = 51,
    EDR_R_UNSUPPORTED                 = 52,
    EDR_R_MAX
} edr_reason_t;

/* Уровень наблюдения (E3-WIT-01). Закрытый список; НЕ шкала «качества» и
 * не сравнивается между backend. Числа растут от «изнутри наблюдаемой ОС»
 * к «аппаратный корень», чтобы «слабейший свидетель» считался как минимум,
 * но сложение или усреднение уровней запрещено (правило R6). */
typedef enum edr_obs_level {
    EDR_LVL_NONE       = 0,   /* уровень не заявлен — считается слабейшим */
    EDR_LVL_INSIDE_OS  = 1,   /* userspace той же ОС (procfs, API)        */
    EDR_LVL_KERNEL     = 2,   /* ядро той же ОС (eBPF, LKM)               */
    EDR_LVL_OUTSIDE    = 3,   /* снаружи гостя (HV, VMI)                  */
    EDR_LVL_BOOT       = 4,   /* измерение цепочки загрузки (IMA, TPM)    */
    EDR_LVL_HWROOT     = 5,   /* аппаратный корень                        */
    EDR_LVL_MAX
} edr_obs_level_t;

const char *edr_reason_name(int rc);
const char *edr_obs_level_name(uint32_t lvl);
include/platx/edr_recorder.h
/* platx/edr_recorder.h — adaptive flight recorder и TL-постура.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §4.8 (EXF-P-01, EXF-P-02, EXF-P-03).
 *
 * Recorder: bounded кольцо дешёвых событий. Слабый сигнал фиксирует
 * pre-trigger окно (копия последних N записей в снимок) и ставит глубокий
 * профиль ТОЛЬКО на затронутую сущность с deadline; tick() откатывает его
 * сам. Голодание кольца (drop rate) — наблюдаемое состояние STARVED, а не
 * тишина (R2). Триггер и объём — детерминированные.
 *
 * Постура: TL — вход детерминированной policy (MIL-5): sampling, резерв
 * evidence, armed prevention, CLI-бюджет, максимальная ступень лестницы без
 * человека. Детектор только RAISE; понижение — Policy с токеном; TL не
 * источник полномочий: ни один уровень не даёт lease.
 *
 * Prevention compile: только правило класса PREVENTION (синхронное, дешёвое,
 * высокоуверенное) превращается в запрос компиляции в SelfProtect inline
 * policy через signed → validate → prepare → selftest → commit generation.
 * SEQUENCE/CORRELATION — отказ: userspace-корреляция не становится kernel
 * callback (EXF-P-03).
 */

#define EDR_REC_RING      4096u
#define EDR_REC_PREWINDOW 256u
#define EDR_REC_DEEP_MAX  16u

typedef struct edr_rec_event {
    uint64_t seq, entity_id, entity_gen, t_ns;
    uint32_t payload_type, source_id;
} edr_rec_event_t;

typedef struct edr_rec_deep {
    int      armed;
    uint64_t entity_id, entity_gen, deadline_ns, armed_at_ns;
    uint32_t profile;             /* маска источников глубокого профиля     */
    uint32_t trigger_kind;
} edr_rec_deep_t;

typedef struct edr_rec_snapshot {
    int      in_use;
    uint64_t trigger_seq, entity_id, taken_ns;
    uint32_t n, _pad;
    edr_rec_event_t ev[EDR_REC_PREWINDOW];
} edr_rec_snapshot_t;

typedef struct edr_recorder {
    uint64_t next_seq, appended, dropped, triggers, restores_auto, restores_manual, starved_events;
    uint64_t duplicates, refused_full;            /* лимиты со счётчиком (R2)  */
    uint32_t head, n, n_deep, starved;
    uint64_t window_drop_ns, window_drop_count;   /* окно оценки голодания   */
    edr_rec_event_t ring[EDR_REC_RING];
    edr_rec_deep_t deep[EDR_REC_DEEP_MAX];
    edr_rec_snapshot_t snap[4];
} edr_recorder_t;

int  edr_rec_init(edr_recorder_t *r);
int  edr_rec_append(edr_recorder_t *r, const edr_rec_event_t *e);
/* Триггер: снимок pre-window + глубокий профиль на сущность с deadline. */
int  edr_rec_trigger(edr_recorder_t *r, uint64_t entity_id, uint64_t entity_gen, uint32_t profile,
                     uint32_t trigger_kind, uint64_t now_ns, uint64_t deadline_ns, uint32_t *snap_idx_out);
/* tick: истёкшие профили откатываются сами; возвращает число откатов. */
int  edr_rec_tick(edr_recorder_t *r, uint64_t now_ns);
int  edr_rec_restore(edr_recorder_t *r, uint64_t entity_id, uint64_t entity_gen);
/* Снимок держится, пока не забран в evidence; освобождение явное — снимки
 * не вытесняются молча; исчерпание — FULL (R2). */
int  edr_rec_snapshot_release(edr_recorder_t *r, uint32_t idx);
int  edr_rec_deep_active(const edr_recorder_t *r, uint64_t entity_id, uint64_t entity_gen);
/* голодание: доля потерь за окно выше порога → STARVED (наблюдаемо) */
int  edr_rec_starvation(edr_recorder_t *r, uint64_t now_ns, uint32_t threshold_ppm);

/* ── постура ─────────────────────────────────────────────────────────── */
typedef enum edr_tl { EDR_TL0 = 0, EDR_TL1, EDR_TL2, EDR_TL3, EDR_TL4, EDR_TL_MAX } edr_tl_t;
typedef struct edr_posture {
    uint32_t tl;
    uint32_t sampling_mul;        /* 1,1,2,4,4                               */
    uint32_t evidence_reserve_pct;/* резерв evidence budget                  */
    uint32_t prevention_armed;    /* TL3+                                    */
    uint32_t cli_budget_per_s;    /* 10 → 2 при TL3+                         */
    uint32_t max_rung_auto;       /* лестница без человека: TL4 → 6 в envelope */
    uint32_t escalations, refused_lower, lowered;
    uint64_t last_change_ns;
    uint32_t last_reason;
} edr_posture_t;

int  edr_posture_init(edr_posture_t *p);
/* Детектор может только поднять (INV-SENSE-02). Возвращает 1 при эскалации. */
int  edr_posture_raise(edr_posture_t *p, uint32_t tl, uint32_t reason, uint64_t now_ns);
/* Понижение — только Policy с токеном ≠ 0. */
int  edr_posture_lower(edr_posture_t *p, uint32_t tl, uint64_t policy_token, uint64_t now_ns);

/* ── prevention compile request (EXF-P-03) ───────────────────────────── */
typedef enum edr_prev_stage { EDR_PREV_NONE = 0, EDR_PREV_SIGNED = 1, EDR_PREV_VALIDATED,
    EDR_PREV_PREPARED, EDR_PREV_SELFTESTED, EDR_PREV_COMMITTED, EDR_PREV_MAX } edr_prev_stage_t;
typedef struct edr_prevention_req {
    uint32_t rule_id, kind;
    uint32_t stage;               /* edr_prev_stage_t                        */
    uint32_t signer_key_id;
    uint64_t policy_generation;   /* atomic generation commit                */
    uint8_t  rule_digest[32];
    uint32_t selftest_ok, _pad;
} edr_prevention_req_t;
int  edr_prevention_request(const edr_rule_t *r, uint32_t signer_key_id, edr_prevention_req_t *out);
/* Стадии идут строго по порядку; пропуск — отказ. */
int  edr_prevention_advance(edr_prevention_req_t *req, uint32_t to_stage, int selftest_ok,
                            uint64_t generation);
include/platx/edr_response.h
/* platx/edr_response.h — план ответа: Plan IR в режиме черновика, лестница
 * containment, counterfactual, уровни автономии, envelope, kill switch.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §4.7, §4.9 (EXF-R-01…10, EXF-A-01…04).
 *
 * Главное свойство файла — чего он НЕ умеет: у EDR нет lease_id, нет
 * authority_id и нет права выставить ACTION_MODE_LIVE. План строится с
 * authority_id = 0 у изменяющих шагов, поэтому platx_planir_admit
 * отказывает ему (NO_AUTHORITY) ровно до тех пор, пока Policy не подпишет
 * шаги edr_plan_authorize(). Это проверяется настоящим координатором
 * (tests/exf-integ), а не комментарием — образец ndr_response.h.
 *
 * Лестница: ступени 7 (terminate) и 8 (host containment) в Plan IR v1
 * невыразимы (закрытый список opcode) — они возвращают UNSUPPORTED и по
 * ТЗ остаются за человеком (L4). Расширение — новая версия codec, не case.
 */

typedef enum edr_rung {
    EDR_RUNG_NONE = 0,
    EDR_RUNG_OBSERVE = 1,       /* observe/enrich                          */
    EDR_RUNG_SNAPSHOT = 2,      /* snapshot / evidence capture (COLLECT)   */
    EDR_RUNG_RESTRICT = 3,      /* rate-limit / restrict capability (REVOKE)*/
    EDR_RUNG_FREEZE = 4,        /* freeze process/cgroup                   */
    EDR_RUNG_QUARANTINE = 5,    /* quarantine artifact                     */
    EDR_RUNG_ISOLATE = 6,       /* isolate network, preserve control       */
    EDR_RUNG_TERMINATE = 7,     /* невыразимо в Plan IR v1 → L4/человек    */
    EDR_RUNG_HOST = 8,          /* host/VM containment → L4/человек        */
    EDR_RUNG_MAX
} edr_rung_t;

typedef enum edr_autonomy { EDR_L0_OBSERVE = 0, EDR_L1_ENRICH = 1, EDR_L2_REVERSIBLE = 2,
    EDR_L3_REMEDIATE = 3, EDR_L4_CRITICAL = 4, EDR_L_MAX } edr_autonomy_t;

/* Counterfactual — обязателен для шагов ≥ REVERSIBLE (EXF-R-05). */
typedef struct edr_counterfactual {
    uint32_t will_stop;             /* маска классов, что остановится        */
    uint32_t will_break;
    uint32_t will_preserve;         /* control/evidence channel и т.п.       */
    uint32_t rollback_seconds;
    uint64_t rollback_last_tested_ns;   /* 0 — никогда                        */
    uint32_t rollback_tested;       /* 1 — есть протестированный откат       */
    uint32_t dryrun;                /* 0 FULL, 1 PARTIAL, 2 UNSUPPORTED      */
} edr_counterfactual_t;

typedef struct edr_response_req {
    uint64_t mission_id, incident_id;
    uint32_t generation;            /* replan generation                     */
    uint32_t rung;                  /* edr_rung_t                             */
    uint64_t target_id;             /* идентификатор, не адрес                */
    uint64_t evidence_id;           /* finding/handle, на котором стоит шаг    */
    uint64_t seed;
    uint32_t emergency;             /* 1 — evidence bypass допустим           */
    uint32_t evidence_reserved;     /* 1 — pre-action checkpoint зарезервирован*/
    uint32_t verify_predicate;      /* edr_verify_pred_t                      */
    uint32_t _pad;
    edr_counterfactual_t cf;
} edr_response_req_t;

typedef enum edr_verify_pred { EDR_VP_NONE = 0, EDR_VP_NO_EXECUTION_NO_EGRESS = 1,
    EDR_VP_EVIDENCE_CHAIN_CONTINUOUS, EDR_VP_SERVICE_AVAILABLE, EDR_VP_TARGET_ABSENT,
    EDR_VP_TARGET_FROZEN, EDR_VP_MAX } edr_verify_pred_t;

typedef struct edr_response_plan {
    platx_plan_ir_t ir;             /* черновик: authority_id = 0 у эффектов  */
    uint8_t  digest[32];            /* над каноническим wire черновика         */
    uint32_t rung;
    uint32_t main_step;             /* индекс изменяющего шага                */
    uint32_t evidence_bypassed;     /* 1 — emergency без checkpoint            */
    uint32_t expected_effect;       /* platx_plan_effect_t главного шага       */
    uint32_t max_autonomy;          /* по ограничителям EXF-A-02               */
    uint32_t autonomy_reason;       /* edr_reason_t, почему не выше            */
} edr_response_plan_t;

typedef struct edr_autonomy_ctx {
    uint32_t policy_level;          /* уровень из policy generation           */
    uint32_t rollback_tested;
    uint32_t dryrun_unsupported;
    uint32_t coverage_degraded;
    uint32_t blast_host_irreversible;
    uint32_t budget_exhausted;
} edr_autonomy_ctx_t;

/* Autonomous Defense Envelope — бинарный, не YAML (EXF-A-03). */
#define EDR_ENV_TRIG_FORENSIC_TAMPER      (1u<<0)
#define EDR_ENV_TRIG_SENSOR_BLINDING_HV   (1u<<1)
#define EDR_ENV_TRIG_AUDIT_LOST           (1u<<2)
#define EDR_ENV_TRIG_CONTROL_LOST         (1u<<3)
typedef struct edr_envelope {
    uint32_t abi_version;
    uint32_t trigger_mask;
    uint32_t allowed_ops;           /* биты platx_plan_op_t                    */
    uint32_t forbidden_ops;
    uint64_t issued_ns, duration_ns;
    uint32_t action_budget;
    uint32_t witnesses_required;    /* ≥ 2                                     */
    uint32_t mandatory_rollback;
    uint32_t signer_key_id;
    uint8_t  signature[64];
    uint32_t used_actions;          /* mutable                                 */
    uint32_t _pad;
} edr_envelope_t;

typedef struct edr_response_ctx {
    uint32_t killed;                /* kill switch: новых шагов нет            */
    uint32_t cleanup_allowed;       /* координатор доводит rollback            */
    uint64_t built, refused_killed, refused_cf, refused_evidence, refused_unsupported, bypassed;
} edr_response_ctx_t;

int  edr_response_init(edr_response_ctx_t *c);
/* Черновик плана: [COLLECT checkpoint] → главный шаг → VERIFY. Возврат —
 * edr_reason_t. Шаги ≥ REVERSIBLE без counterfactual/checkpoint — отказ. */
int  edr_plan_build(edr_response_ctx_t *c, const edr_response_req_t *req, edr_response_plan_t *out);
/* Policy подписывает: заполняет authority_id/lease_gen у всех шагов.
 * EDR эту функцию не вызывает — она здесь, чтобы у подписи был один формат. */
int  edr_plan_authorize(edr_response_plan_t *p, uint64_t authority_id, uint64_t lease_gen);
/* Дескриптор шага в PLAN-режиме (lease 0, authority из шага, mode PLAN). */
int  edr_plan_descriptor(const edr_response_plan_t *p, uint32_t step_idx, const char *target,
                         action_descriptor_t *out);
uint32_t edr_autonomy_eval(const edr_autonomy_ctx_t *ctx, uint32_t *reason_out);
int  edr_envelope_check(edr_envelope_t *e, uint32_t trigger, uint32_t op, uint64_t now_ns,
                        uint32_t witnesses_available);
int  edr_response_kill(edr_response_ctx_t *c);
uint32_t edr_rung_opcode(uint32_t rung);
uint32_t edr_rung_effect(uint32_t rung);
const char *edr_rung_name(uint32_t r);
const char *edr_autonomy_name(uint32_t l);
include/platx/edr_saga.h
/* platx/edr_saga.h — распределённый ответ: fleet plan (Central), повторная
 * проверка на Node, saga с per-target результатом, partition/epoch/fencing.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §5.5 (EXF-X-12…X-16).
 *
 * Central компилирует versioned typed план с immutable target snapshot
 * (generation каждого target), бюджетом и verify/compensation. Подлинная
 * подпись Central — НЕ разрешение (E2-CN-06): Node перед КАЖДЫМ шагом
 * заново проверяет capability, policy generation, lease, target
 * identity/generation, risk ceiling, coverage, authority, epoch и fencing.
 * Любая проверка не прошла — REFUSED с причиной, эффекта нет.
 *
 * Saga: per-target result OK / REFUSED(reason) / UNREACHABLE / PARTIAL /
 * VERIFIED; статус плана COMPLETE только если каждый target VERIFIED —
 * иначе PARTIAL с разбивкой (EXF-X-14). Partition: leases истекают,
 * локальная защита живёт, reconnect → reconcile → новый epoch; две копии
 * Central не выдают противоречивых прав (fencing token, E2-CN-09).
 */

#define EDR_SAGA_TARGETS_MAX 32u
#define EDR_FLEET_PLAN_ABI   1u

typedef enum edr_target_result { EDR_TR_NONE = 0, EDR_TR_OK = 1, EDR_TR_REFUSED, EDR_TR_UNREACHABLE,
    EDR_TR_PARTIAL, EDR_TR_VERIFIED, EDR_TR_COMPENSATED, EDR_TR_MAX } edr_target_result_t;

typedef struct edr_fleet_target {
    uint64_t node_id, target_id, target_generation;   /* immutable snapshot   */
    uint64_t authority_id, lease_id, lease_expires_ns;
    uint32_t rung;                    /* edr_rung_t; ≤ ceiling узла             */
    uint32_t capability_id;           /* требуемая capability на узле          */
    uint32_t result, refusal_reason;  /* edr_target_result_t / edr_reason_t     */
    uint32_t compensation_requested, compensation_done;
    uint64_t budget_ns;
} edr_fleet_target_t;

typedef struct edr_fleet_plan {
    uint32_t abi_version, struct_size;
    uint64_t plan_id, central_id;
    uint64_t epoch;                   /* epoch федерации                        */
    uint64_t fencing_token;           /* монотонный; больший вытесняет меньший */
    uint64_t policy_generation;
    uint32_t tenant_id, n_targets;
    uint64_t issued_ns, deadline_ns;
    uint32_t signer_key_id, _pad;
    uint8_t  ir_digest[32];           /* дайджест Plan IR шаблона              */
    uint8_t  digest[32];
    uint8_t  signature[64];
    edr_fleet_target_t targets[EDR_SAGA_TARGETS_MAX];
} edr_fleet_plan_t;

typedef enum edr_plan_status { EDR_PLS_NONE = 0, EDR_PLS_PENDING = 1, EDR_PLS_PARTIAL, EDR_PLS_COMPLETE,
    EDR_PLS_COMPENSATING, EDR_PLS_MAX } edr_plan_status_t;
typedef struct edr_saga_summary {
    uint32_t status;                  /* edr_plan_status_t                      */
    uint32_t n_targets, ok, refused, unreachable, partial, verified, compensated, pending;
} edr_saga_summary_t;

/* ── Central ─────────────────────────────────────────────────────────── */
int  edr_fleet_plan_init(edr_fleet_plan_t *p, uint64_t plan_id, uint64_t central_id, uint64_t epoch,
                         uint64_t fencing_token, uint64_t policy_generation, uint32_t tenant_id,
                         uint64_t issued_ns, uint64_t deadline_ns, const uint8_t ir_digest[32]);
int  edr_fleet_target_add(edr_fleet_plan_t *p, const edr_fleet_target_t *t);
int  edr_fleet_plan_digest(const edr_fleet_plan_t *p, uint8_t out[32]);
typedef int (*edr_saga_sign_fn)(void *ctx, const uint8_t *d, size_t len, uint8_t sig[64]);
typedef int (*edr_saga_verify_fn)(void *ctx, uint32_t key_id, uint64_t central_id, const uint8_t *d, size_t len, const uint8_t sig[64]);
int  edr_fleet_plan_sign(edr_fleet_plan_t *p, uint32_t key_id, edr_saga_sign_fn sign, void *ctx);
/* Saga: Central записывает результат target'а; сводка никогда не говорит
 * COMPLETE при хотя бы одном не-VERIFIED. */
int  edr_saga_result(edr_fleet_plan_t *p, uint32_t idx, uint32_t result, uint32_t refusal_reason);
int  edr_saga_summary(const edr_fleet_plan_t *p, edr_saga_summary_t *out);
/* Компенсация: для OK-без-VERIFIED после deadline; reconcile после связи:
 * UNREACHABLE → PENDING повторного запроса под НОВЫМ epoch. */
int  edr_saga_compensate(edr_fleet_plan_t *p, uint64_t now_ns, uint32_t *requested_out);
int  edr_saga_reconcile(edr_fleet_plan_t *p, uint64_t new_epoch, uint64_t new_fencing, uint32_t *requeued_out);

/* ── Node ────────────────────────────────────────────────────────────── */
typedef struct edr_node_ctx {
    uint64_t node_id;
    uint32_t tenant_id, _pad;
    uint64_t epoch, fencing_token;    /* известные узлу; меньший fencing — отказ */
    uint64_t policy_generation;
    uint64_t central_id;              /* кому узел верит сейчас (после reconcile) */
    uint32_t capability_mask;         /* биты capability_id, что есть на узле     */
    uint32_t risk_ceiling_rung;       /* edr_rung_t                               */
    uint32_t coverage_ok;             /* 0 — coverage деградировано               */
    uint32_t partitioned;             /* 1 — Central недоступен                   */
    uint64_t partition_since_ns;
    uint32_t local_ceiling_frozen;    /* при partition полномочия зафиксированы  */
    uint32_t _pad2;
    /* локальная истина о целях: (target_id, generation) */
    struct { uint64_t target_id, generation; } targets[EDR_SAGA_TARGETS_MAX];
    uint32_t n_targets, _pad3;
    /* счётчики (R2) */
    uint64_t checked, refused_sig, refused_epoch, refused_fencing, refused_tenant, refused_policy_gen,
             refused_lease, refused_target, refused_ceiling, refused_capability, refused_coverage,
             refused_authority, refused_deadline, refused_partition, accepted, effects_executed;
} edr_node_ctx_t;

int  edr_node_init(edr_node_ctx_t *n, uint64_t node_id, uint32_t tenant_id, uint64_t epoch,
                   uint64_t fencing_token, uint64_t policy_generation, uint64_t central_id,
                   uint32_t capability_mask, uint32_t risk_ceiling_rung);
int  edr_node_target_set(edr_node_ctx_t *n, uint64_t target_id, uint64_t generation);
/* Повторная проверка ПЕРЕД шагом: подпись Central подлинна — но это не
 * разрешение; каждая проверка отдельно, первая провалившаяся — причина.
 * Возврат EDR_R_OK означает «шаг допущен к координатору», не «исполнен». */
int  edr_node_check(edr_node_ctx_t *n, const edr_fleet_plan_t *p, uint32_t target_idx, uint64_t now_ns,
                    edr_saga_verify_fn verify, void *ctx, uint32_t *reason_out);
/* Partition: leases истекают, локальные полномочия фиксируются. */
int  edr_node_partition(edr_node_ctx_t *n, uint64_t now_ns);
/* Reconnect: reconcile обязателен — принимаются только планы нового epoch
 * с fencing ≥ известного; старые — REFUSED_STALE_GENERATION. */
int  edr_node_reconnect(edr_node_ctx_t *n, uint64_t central_id, uint64_t new_epoch, uint64_t new_fencing,
                        uint64_t policy_generation);
const char *edr_target_result_name(uint32_t r);
const char *edr_plan_status_name(uint32_t s);
include/platx/edr_storyline.h
/* platx/edr_storyline.h — storyline: bounded проекция причинно-временного графа.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §4.4 (EXF-S-01…06), SGRAPH §2.3/§2.6.
 *
 * Правила, из которых вырастает весь файл:
 *  - CAUSES/SPAWNED пишутся ТОЛЬКО если факт уже нёс parent с generation.
 *    Рядом по времени, общий corr_id — OBSERVED_WITH. Честнее пустая
 *    цепочка, чем ложная (EXF-S-01/03).
 *  - Ingest — копия payload факта в таблицу. Ни I/O, ни детекторов, ни
 *    emit, ни /proc: это делает ingest пригодным для вызова из callback
 *    plat_event (EXF-S-05).
 *  - Slab заранее, строки интернируются, overflow = drop + counter, id не
 *    переиспользуется (EXF-S-04).
 *  - Ребро несёт класс observed/derived/asserted, уровень наблюдения и
 *    source; противоречие двух свидетелей — отдельная запись discrepancy,
 *    обе версии сохраняются (EXF-S-02, R6).
 *  - Синтетика MIRAGE помечена и не смешивается с реальным миром: реальный
 *    факт, ссылающийся на синтетического родителя, — отказ (EXF-S-06).
 */

#define EDR_SL_ENTITIES_MAX   1024u
#define EDR_SL_EDGES_MAX      4096u
#define EDR_SL_STRPOOL_BYTES  16384u
#define EDR_SL_STRINGS_MAX    512u
#define EDR_SL_NAME_MAX       96u
#define EDR_SL_DISCREP_MAX    64u
#define EDR_SL_CHAIN_MAX      16u
#define EDR_SL_NEIGHBORS_MAX  64u

typedef enum edr_edge_type {
    EDR_EDGE_NONE = 0,
    EDR_EDGE_SPAWNED = 1,          /* process → process, только из parent   */
    EDR_EDGE_EXECUTED,             /* process → artifact                    */
    EDR_EDGE_OPENED,               /* process → file                        */
    EDR_EDGE_CONNECTED,            /* process → endpoint                    */
    EDR_EDGE_AUTHENTICATED_TO,     /* identity → service                    */
    EDR_EDGE_SUPPORTED_BY,         /* finding → observation                 */
    EDR_EDGE_CONTRADICTED_BY,      /* finding → observation                 */
    EDR_EDGE_AFFECTED,             /* response → entity                     */
    EDR_EDGE_OBSERVED_WITH,        /* корреляция, НЕ причина                */
    EDR_EDGE_MAX
} edr_edge_type_t;

typedef enum edr_edge_class { EDR_EC_NONE = 0, EDR_EC_OBSERVED = 1, EDR_EC_DERIVED,
    EDR_EC_ASSERTED, EDR_EC_MAX } edr_edge_class_t;

typedef struct edr_sl_entity {
    int      in_use;
    uint64_t sl_id;               /* локальный монотонный id                */
    uint32_t kind;                /* edr_entity_kind_t                      */
    uint32_t observation_level;   /* лучший (максимальный) уровень, где виден */
    uint64_t entity_id;           /* SENSE entity id                        */
    uint64_t generation;          /* birth/generation из факта              */
    uint32_t name;                /* индекс интернированной строки; 0 — нет */
    uint32_t flags;               /* EDR_SLE_*                              */
    uint64_t first_seen_ns, last_seen_ns;
    uint32_t seen_mask;           /* биты уровней, где сущность наблюдалась */
    uint32_t absent_mask;         /* биты уровней, где заявлено ОТСУТСТВИЕ  */
} edr_sl_entity_t;

#define EDR_SLE_SYNTHETIC       (1u<<0)
#define EDR_SLE_SYNTHETIC_TOUCH (1u<<1)
#define EDR_SLE_DISCREPANCY     (1u<<2)

typedef struct edr_sl_edge {
    int      in_use;
    uint64_t edge_id;
    uint64_t from, to;            /* sl_id                                  */
    uint32_t type;                /* edr_edge_type_t                        */
    uint32_t klass;               /* edr_edge_class_t                       */
    uint32_t source_id;           /* провайдер/детектор                     */
    uint32_t observation_level;
    uint64_t t_begin_ns, t_end_ns;
    uint32_t quality;             /* SENSE_QUAL_* биты                      */
    uint32_t confidence;          /* 0..3                                   */
    uint8_t  evidence_digest[32]; /* handle digest; нули — нет              */
    uint32_t flags;               /* EDR_SLE_SYNTHETIC                      */
    uint32_t _pad;
} edr_sl_edge_t;

typedef struct edr_sl_discrepancy {
    uint64_t sl_id;
    uint32_t level_present, level_absent;
    uint64_t t_ns;
    uint32_t source_present, source_absent;
} edr_sl_discrepancy_t;

/* Факт для ingest — копия, без указателей внутрь чужой памяти после возврата. */
typedef struct edr_sl_fact {
    uint32_t subject_kind;  uint64_t subject_id,  subject_gen;
    uint32_t object_kind;   uint64_t object_id,   object_gen;     /* 0 — нет */
    uint32_t parent_kind;   uint64_t parent_id,   parent_gen;     /* 0 — нет */
    uint32_t edge_type;     /* желаемое ребро subject→object                 */
    uint32_t klass;         /* edr_edge_class_t                              */
    uint32_t source_id;
    uint32_t observation_level;
    uint64_t t_ns;
    uint32_t quality, confidence;
    uint8_t  evidence_digest[32];
    uint32_t synthetic;     /* 1 — из MIRAGE                                 */
    uint32_t presence;      /* 1 PRESENT, 2 ABSENT (witness claim), 0 — н/п */
    char     subject_name[EDR_SL_NAME_MAX];   /* пусто — не интернируется   */
} edr_sl_fact_t;

typedef struct edr_storyline {
    uint64_t next_entity_id, next_edge_id;
    uint32_t n_entities, n_edges, n_strings, str_used;
    uint32_t n_discrep, _pad;
    /* счётчики R2 */
    uint64_t dropped_entities, dropped_edges, dropped_strings, refused_no_parent,
             refused_synthetic, refused_args, correlated_instead_of_caused,
             discrepancies, ingested;
    uint32_t str_off[EDR_SL_STRINGS_MAX];
    char     strpool[EDR_SL_STRPOOL_BYTES];
    edr_sl_entity_t entities[EDR_SL_ENTITIES_MAX];
    edr_sl_edge_t   edges[EDR_SL_EDGES_MAX];
    edr_sl_discrepancy_t discrep[EDR_SL_DISCREP_MAX];
} edr_storyline_t;

int  edr_sl_init(edr_storyline_t *s);
/* Ingest: копия в таблицы. Возврат edr_reason_t. SPAWNED без parent →
 * EDR_R_NO_PARENT и ребро OBSERVED_WITH (счётчик correlated_instead_of_caused). */
int  edr_sl_ingest(edr_storyline_t *s, const edr_sl_fact_t *f, uint64_t *edge_id_out);
const edr_sl_entity_t *edr_sl_entity(const edr_storyline_t *s, uint64_t entity_id, uint64_t generation);
const edr_sl_entity_t *edr_sl_entity_by_slid(const edr_storyline_t *s, uint64_t sl_id);
const char *edr_sl_name(const edr_storyline_t *s, uint32_t idx);
/* Причинная цепочка назад по SPAWNED: только настоящие рёбра. Возврат — длина. */
int  edr_sl_chain(const edr_storyline_t *s, uint64_t sl_id, uint64_t *out, size_t cap);
/* Соседи по любым рёбрам; include_synthetic=0 скрывает decoy-мир. */
int  edr_sl_neighbors(const edr_storyline_t *s, uint64_t sl_id, int include_synthetic,
                      uint64_t *out_edge_ids, size_t cap);
const edr_sl_edge_t *edr_sl_edge(const edr_storyline_t *s, uint64_t edge_id);
/* Слабейший уровень наблюдения среди рёбер вокруг сущности (R6). */
uint32_t edr_sl_weakest_level(const edr_storyline_t *s, uint64_t sl_id);
/* Канонический wire ребра (для package): 128 байт. */
#define EDR_SL_EDGE_WIRE 128u
int  edr_sl_edge_wire(const edr_sl_edge_t *e, uint8_t out[EDR_SL_EDGE_WIRE]);
const char *edr_edge_type_name(uint32_t t);
const char *edr_edge_class_name(uint32_t c);
include/platx/exf_wire.h
/* platx/exf_wire.h — big-endian примитивы канонического wire домена
 * EDR/XDR/FORENSIC. Один порядок байт, ноль padding, фиксированный порядок
 * полей — чтобы digest одного содержимого совпадал на любом build (EXF-B-07). */
typedef struct exf_w { uint8_t *p; size_t cap, n; int fail; } exf_w_t;
typedef struct exf_r { const uint8_t *p; size_t len, n; int fail; } exf_r_t;

void exf_w_init(exf_w_t *w, uint8_t *p, size_t cap); /* inline interface */
void exf_w_u8(exf_w_t *w, uint8_t v); /* inline interface */
void exf_w_u32(exf_w_t *w, uint32_t v); /* inline interface */
void exf_w_u64(exf_w_t *w, uint64_t v); /* inline interface */
void exf_w_bytes(exf_w_t *w, const void *b, size_t n); /* inline interface */

void exf_r_init(exf_r_t *r, const uint8_t *p, size_t len); /* inline interface */
uint8_t exf_r_u8(exf_r_t *r); /* inline interface */
uint32_t exf_r_u32(exf_r_t *r); /* inline interface */
uint64_t exf_r_u64(exf_r_t *r); /* inline interface */
void exf_r_bytes(exf_r_t *r, void *b, size_t n); /* inline interface */
12

elf

Анализ ELF и лаборатория загрузки исполняемых объектов
src/elf/Наблюдение и исследование9 файлов1 API headers

ELF domain анализирует, проверяет и в research mode загружает/исполняет ELF artifacts. Production-use часть должна ограничиться metadata/provenance/verifier; in-memory execution и patch остаются отдельным опасным backend.

Граница ответственности

  • Read-only parser отделён от execution loader.
  • Plugin loader использует проверенный subset/API, а не произвольную elf_load CLI.
  • Patch/exec требуют research profile, child isolation и explicit arm.

Устройство подсистемы

  • Parser bounds-checks ELF header, tables, sections, strings, symbols, relocations и dependencies.
  • Verifier строит immutable report: architecture/type/imports/W^X/TLS/segments/hash/provenance.
  • Loader maps segments с staged permissions, resolves allowlisted imports, applies relocations, then seals W^X.
  • Patch operation имеет preimage hash, exact range, rollback bytes и evidence.

Поток работы

  • Artifact fd/bytes → bounded parse.
  • Verify policy/provenance.
  • Optional load plan/dry-run.
  • Isolated execute/patch → report/cleanup.

Отказ и восстановление

  • Malformed offsets/overflow reject before allocation/map.
  • Unsupported relocation/TLS fails before control transfer.
  • Partial map rollback/unmap.
  • Target crash confined child; no capability published before READY.

Основные возможности

  • White/black analytical classification helpers.
  • ELF metadata, dependencies, sections, strings and verification.
  • In-memory loader variants, TLS handling and patch experiments.
Архитектурные детали и инварианты

Назначение

Верификация ELF-бинарей перед загрузкой: проверка magic, ELFCLASS64, ET_EXEC/DYN/REL,

и наличия секции .note.platx с подписью.

§SEC-3

Чтение через pread в статический буфер s_rdbuf[65536]. malloc не используется.

Управление и диагностика

Корневые команды: elf, elf_load. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / elf →
Состав подсистемы / 9 файлов
Файл / компонентНазначение и граница
src/elf/cmd_elf.cnamespace "elf": ELF-аналитическая лаборатория. Полноценный набор инструментов для разбора ELF поверх собственных источников платформы (@файл, memfd:, cdata:). Реализует: ЗАГРУЗКА: load (свой загрузчик elf_load), exec (fork+execve без своего ИНФО: info [--verbose] [--field=…]
src/elf/cmd_elf_black.celf infect + elf scan-caves Operator color black, not RT red/blue. Same leftover TU as the old cmd_elf_red.c name. Infect is not a product path — refuse, do not write. Образовательная лаборатория ELF-инфекций (57 техник, TZ_ELF.TXT). Скомпилируется автоматически: Makefile подбирает все *.c из src/.
src/elf/cmd_elf_white.cELF detect/scan (av-scan / entropy / heuristic / yara) Operator color white, not RT red/blue. Same leftover detect TU as the old cmd_elf_blue.c name. elf av-scan --file=PATH [--db=FILE] [--verbose] [--all] Сигнатурный сканер (~60 встроенных сигнатур, аналог PEiD для ELF).
src/elf/cmd_elfload.cELF loader commands for the universal console. Sub-commands under the "elf" dispatcher: elf info — comprehensive ELF metadata elf deps — dynamic dependencies only elf sections — section header table
src/elf/elf_loader.cELF loader hardening: bounds checks, overflow guards. STAB-119: Fuzz-hardened ELF parsing covering corrupted headers, sections, relocations, dynamic entries and integer overflow. (a) EI_MAG magic bytes verified before any parsing. (b) section_limit: sh_num capped to prevent out-of-bounds section walks.
src/elf/elf_verify.cРеализация elf / verify
src/elf/elfload.cuserspace ELF loader from a memfd file descriptor. ET_EXEC static executables (fixed vaddrs) ET_DYN static PIE or dynamic (loads ld-linux from PT_INTERP path) Architectures: x86-64, AArch64, i386 (x86-32), ARM32 - ELF mapping works generically. - jump_entry() has architecture-specific inline-asm for each target.
src/elf/elfload.hshared interface for the ELF loader module. Loader is not XIO and not the RA2C working path (connect → handshake → send/recv → switch → disconnect). Public inspection/exec entry points, plus a few internals reused by the in-process self-linking loader (elfload2_tls.c): elf_map_fd(), build_stack()
src/elf/elfload2_tls.cin-process ELF dynamic linker with full TLS. Second execution backend for the elf_load command group. Where elfload_exec() hands a dynamic binary to the system ld-linux, this module performs the whole job itself, inside the current process: - maps the main object and every DT_NEEDED shared object (via elf_map_fd)
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/elf_provenance.h
/* platx/elf_provenance.h — ELF load/exec provenance via XIO claim (INT-092).
 * Records what was loaded, from which lease+context, with sha256.
 * Does NOT touch elfload.c, cmd_elf.c.
 */
#define PLAT_ELF_PROV_PATH_MAX 256
#define PLAT_ELF_PROV_REASON   128

typedef struct {
    uint64_t lease_id;
    uint64_t ctx_id;
    uint32_t ctx_gen;
    char     path[PLAT_ELF_PROV_PATH_MAX];
    uint8_t  sha256[32];    /* caller provides; 0-filled = unknown */
    int      loaded;        /* 1 after plat_elf_prov_record succeeds */
    uint64_t loaded_at_ms;
    char     reason[PLAT_ELF_PROV_REASON];
} plat_elf_prov_t;

/* Record provenance: lease + context + path + digest.
 * Calls xio_claim_exec weak hook; audits.  0/-1. */
int plat_elf_prov_record(uint64_t lease_id, uint64_t ctx_id, uint32_t ctx_gen,
                         const char *path, const uint8_t sha256[32],
                         plat_elf_prov_t *out);

/* Check a recorded provenance object is still valid. 0/-1. */
int plat_elf_prov_check(const plat_elf_prov_t *prov, char *reason, size_t rsz);
13

fabric

Композиция версионированных поставщиков возможностей
src/fabric/Управление и контракты22 файлов10 API headers

Fabric связывает потребителя с подходящим поставщиком через общий реестр возможностей. Идентичность, версия контракта и поколение поставщика участвуют в жизненном цикле связи. Потеря поставщика, отзыв и несовместимость отражаются явно и не создают скрытую альтернативную реализацию.

Граница ответственности

  • Fabric связывает потребителя с подходящим поставщиком через общий реестр возможностей. Идентичность, версия контракта и поколение поставщика участвуют в жизненном цикле связи. Потеря поставщика, отзыв и несовместимость отражаются явно и не создают скрытую альтернативную реализацию.

Устройство подсистемы

  • the Action Coordinator (D092, D093, D096, D097). Everything here is a refusal that happens before a provider is asked. The provider is still responsible for its own checks; the point of doing these here is that they are then identical for every provider, and an operator
  • the one action backend of wave 2 (D094-D097). Reversible by construction: the file is moved, not copied and deleted, so rollback puts back the same inode rather than an equal-looking one. verify proves that by comparing (dev, ino) as well as the digest — a copy would
  • namespace "fabric": плоскость провайдеров в поставляемом fabric status поднята ли плоскость, кто зарегистрирован fabric up | down подъём и останов всей плоскости разом fabric providers список провайдеров и их состояние
  • bring the observation plane up as one unit. Wave 2 built a fabric that no profile started. This file is the difference between "the code is linked in" and "the capability exists at runtime", and are not a shipped profile. Two rules shape it: 1. All or nothing. The SENSE registry, the ring, the coverage plane, the
Архитектурные детали и инварианты

Invariants

INV-FABRIC-01 | Every provider operation has a witness journal entry (FAB_JF_WITNESS flag)

INV-FABRIC-02 | Journal full → operation rejected with FABRIC_ERR_JOURNAL_FULL (no silent drop)

§SEC-3 | Journal buffer is static BSS: 4096 × 128 bytes = 512 KB

Recovery

fabric_recover() replays the journal from seq=0, finds PREPARE entries without COMMIT/ABORT, and calls fabric_coord_abort() on them.

Управление и диагностика

Корневые команды: fabric. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / fabric →
Состав подсистемы / 22 файлов
Файл / компонентНазначение и граница
src/fabric/action_coord.cthe Action Coordinator (D092, D093, D096, D097). Everything here is a refusal that happens before a provider is asked. The provider is still responsible for its own checks; the point of doing these here is that they are then identical for every provider, and an operator
src/fabric/action_quarantine.cthe one action backend of wave 2 (D094-D097). Reversible by construction: the file is moved, not copied and deleted, so rollback puts back the same inode rather than an equal-looking one. verify proves that by comparing (dev, ino) as well as the digest — a copy would
src/fabric/cmd_fabric.cnamespace "fabric": плоскость провайдеров в поставляемом fabric status поднята ли плоскость, кто зарегистрирован fabric up | down подъём и останов всей плоскости разом fabric providers список провайдеров и их состояние
src/fabric/fabric_boot.cbring the observation plane up as one unit. Wave 2 built a fabric that no profile started. This file is the difference between "the code is linked in" and "the capability exists at runtime", and are not a shipped profile. Two rules shape it: 1. All or nothing. The SENSE registry, the ring, the coverage plane, the
src/fabric/fabric_coordinator.cINV-FABRIC-01: each operation appends a witness journal entry.
src/fabric/fabric_delivery.cmbus_publish is a weak symbol — if not linked (no MBus in this build),
src/fabric/fabric_gc.cРеализация fabric / gc
src/fabric/fabric_journal.cINV-FABRIC-02: returns FABRIC_ERR_JOURNAL_FULL when no slot free. Lock: single pthread_mutex, never held across vtable calls.
src/fabric/fabric_recover.cРеализация fabric / recover
src/fabric/fabric_snap.cvault_artifact_write is a weak symbol — if unlinked, result is not-stored.
src/fabric/fabric_snapshot.cРеализация fabric / snapshot
src/fabric/forensic_facade.cevidence handles (D091). A bounded, in-process custody table. It is not the FORENSIC store itself; it is the boundary that the observation plane sees, and the point of having it now is that every witness written in this wave hands out handles from the start. Retrofitting custody after consumers have learned to expect blobs is
src/fabric/hades_adapter.cHades sensors as sense providers (D066-D071). Three instances of one adapter. What each of them actually does: 1. pull one raw record from the transport; 2. resolve a STABLE identity for its subject — the part that decides whether two records are about the same thing tomorrow;
src/fabric/hades_live_bridge.cживой мост от кольца сенсора Hades к фабрике. Точка расширения объявлена в include/platx/hades_transport.h и доказана карточкой R1-07: профиль, законно владеющий Hades, линкует эту единицу трансляции, и она регистрирует себя вызовом prov_hades_live_register().
src/fabric/hades_transport.cthe two transports (R1 wiring). The live one refuses, once, in one place, with a reason. That is the whole design: the alternative — a live transport that quietly returns "nothing to read" when it has no privilege — turns an unattached sensor into a quiet
src/fabric/prov_conformance.cthe conformance suite (D098). Runs against a live provider. Nothing here inspects source text; every check is a call whose answer is compared with the contract.
src/fabric/prov_core.cenvelope, status and budget checks (D051, D065). Three small functions with one job between them: refuse a self-contradicting declaration once, here, instead of letting every consumer believe a different half of it. No allocation, no locks, no I/O: everything below is a pure check on a
src/fabric/prov_negotiate.cABI, schema and capability negotiation (D057). Negotiation exists so that "unavailable" can be a fact with a type instead of a zero. Three things are agreed, in this order, and the first one that cannot be agreed decides the reason: 1. ABI major — one number, no range. A caller built against a different
src/fabric/prov_registry.cthe provider fabric (D056-D060). Not a fifth registry. A sense provider registered here is registered as a SENSE source in the SENSE registry, and publication is decided there: prov_publish() runs validate → prepare → commit → start against sense_source_*() and then READS THE CAPABILITY BACK. The fabric never sets a
src/fabric/prov_sdk.cprovider-side emitter (D061, D062). Pure state machine over a caller-owned struct: no locks, no allocation, no clock. A provider that wants a different transport still gets the same accounting, which is the only way a conformance test can hold every provider
src/fabric/witness_coord.cwitness snapshot coordinator (D063, D064). One rule, applied to every participant: a snapshot is graded on what it shows, not on what it calls itself. ATOMIC needs a freeze token and zero deltas inside the window. A walk that saw the world change under it did not freeze it.
src/fabric/witness_providers.cthe four standard witnesses (D085-D090). The interesting decisions in this file are all about what NOT to say: - a witness that cannot look returns OFFLINE with a reason, never an empty snapshot. An empty ATOMIC snapshot says "nothing exists"; an OFFLINE one
Контракты API / 10 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/action_coord.h
/* platx/action_coord.h — the Action Coordinator (D092, D093, D096, D097).
 *
 * One place that decides whether an action may happen, so that every provider
 * does not have to be trusted to decide it identically.
 *
 * What the coordinator owns:
 *   idempotency  one key, one effect. A repeat returns the original receipt
 *                with applied == 0 — not an error, and not a second change.
 *   fencing      a lease has a generation. Once a newer generation is seen,
 *                the older one may not commit, even if it prepared first.
 *                Without this, "the old owner was slow" and "there are two
 *                owners" produce the same sequence of calls.
 *   pinning      commit is bound to the descriptor that was previewed. A plan
 *                approved for one target cannot be executed against another.
 *   prerequisite no lease, no authority, no evidence reference, no action.
 *                Each of those is a refusal with its own reason, because
 *                "denied" without a reason is what makes an operator disable
 *                the check.
 *   reconcile    after a restart, every transaction that was prepared or
 *                committed but never verified is reported, so it can be
 *                finished or undone rather than forgotten.
 */

#define ACTION_TXN_MAX    32u
#define ACTION_LEASE_MAX  16u

typedef struct action_txn {
    int      in_use;
    char     key[ACTION_KEY_MAX];
    char     provider_id[PROV_ID_MAX];
    uint64_t txn_id;
    uint32_t state;                 /* action_state_t */
    uint8_t  descriptor_hash[32];
    action_receipt_t receipt;
} action_txn_t;

typedef struct action_lease {
    int      in_use;
    uint64_t lease_id;
    uint64_t generation;            /* the newest generation ever accepted */
} action_lease_t;

typedef struct action_coord {
    action_txn_t   txns[ACTION_TXN_MAX];
    action_lease_t leases[ACTION_LEASE_MAX];
    uint64_t next_txn;
    uint64_t allowance_ns;          /* 0 = no deadline policy on this coordinator */
    uint64_t refusals;
    uint64_t commits;
    uint64_t repeats;               /* idempotent replays served from record */
} action_coord_t;

int action_coord_init(action_coord_t *c, prov_status_t *st);

/* Deadline policy of this coordinator, in nanoseconds per execute(). It is
 * checked before prepare and again before commit, and NEVER after: a
 * transaction that has changed the world is finished or rolled back, never
 * abandoned because the clock ran out. A prepare that outlived the allowance
 * is rolled back, so the reservation does not leak. */
int action_coord_set_deadline(action_coord_t *c, uint64_t allowance_ns);

/* SHA-256 over the descriptor's decision-relevant fields. The hash field
 * itself is excluded, so a descriptor can carry its own hash. */
int action_coord_hash(const action_descriptor_t *d, uint8_t out[32]);

/* Preview never changes state, whatever the mode. Fills the descriptor hash a
 * later commit will be pinned to. */
int action_coord_preview(action_coord_t *c, const char *provider_id,
                         const action_descriptor_t *d, action_preview_t *out,
                         prov_status_t *st);

/* prepare → commit → verify, under the rules above. Only ACTION_MODE_LIVE
 * changes anything; PLAN and SHADOW are refused here rather than being
 * quietly executed, because a mode that sometimes changes state is not a
 * dry run. */
int action_coord_execute(action_coord_t *c, const char *provider_id,
                         const action_descriptor_t *d, action_receipt_t *out,
                         prov_status_t *st);

int action_coord_rollback(action_coord_t *c, const char *provider_id,
                          const char *idempotency_key, action_receipt_t *out,
                          prov_status_t *st);

/* Transactions the provider still considers open after a restart. */
int action_coord_reconcile(action_coord_t *c, const char *provider_id,
                           action_receipt_t *out, uint32_t max,
                           uint32_t *n_out, prov_status_t *st);

uint64_t action_coord_lease_generation(const action_coord_t *c,
                                       uint64_t lease_id);
include/platx/action_quarantine.h
/* platx/action_quarantine.h — one safe, reversible file action (D094, D095).
 *
 * Quarantine is the right first action to build because it is the smallest one
 * that is genuinely dangerous: it changes the filesystem, it can be aimed at
 * the wrong file, and "undo" has to mean the same bytes back at the same path,
 * not a copy that looks similar.
 *
 * The mechanism is a MOVE inside one filesystem, not a copy-and-delete:
 *   - the inode does not change, so verify can prove that what sits in
 *     quarantine is the same object that was at the path, and not something
 *     with the same contents;
 *   - the durability protocol is the one stated in atomic_file.h — the
 *     directory entries are what changed, so both directories are fsynced;
 *   - a journal entry is written before and after the rename, because a
 *     transaction that a restart cannot see is a transaction nobody will
 *     finish (D097).
 */

#define AQ_PATH_MAX   512u
#define AQ_TXN_MAX     16u

typedef struct aq_txn {
    int      in_use;
    uint64_t txn_id;
    uint32_t state;                 /* action_state_t */
    char     key[ACTION_KEY_MAX];
    char     src[AQ_PATH_MAX];
    char     dst[AQ_PATH_MAX];
    uint8_t  digest[32];
    uint64_t dev, ino;
    uint32_t mode;
} aq_txn_t;

typedef struct action_quarantine {
    char provider_id[PROV_ID_MAX];
    char capability[PROV_ID_MAX];
    char dir[AQ_PATH_MAX];
    char journal[AQ_PATH_MAX];
    uint64_t epoch, generation, boot_id_hash;

    action_provider_v1_t vtable;
    aq_txn_t txns[AQ_TXN_MAX];
    uint64_t next_txn;
    uint64_t committed;
} action_quarantine_t;

int action_quarantine_init(action_quarantine_t *q, const char *provider_id,
                           const char *capability, const char *quarantine_dir,
                           uint64_t epoch, uint64_t generation,
                           uint64_t boot_id_hash, prov_status_t *st);

action_provider_v1_t *action_quarantine_provider(action_quarantine_t *q);

/* Drop in-process state while leaving the journal and the filesystem exactly
 * as they are — what a restart looks like from the outside. Test-only, and
 * named so it cannot be mistaken for part of the action ABI. */
void action_quarantine_simulate_restart(action_quarantine_t *q);
include/platx/fabric_delivery.h
/* platx/fabric_delivery.h — INT-139: Fabric artifacts → RA2C/MBus delivery.
 * mbus_publish is weak; if unlinked: PLAT_FAB_DELIV_NOLINK, no side-effects. */

#define PLAT_FAB_DELIV_MAX_TOPIC  64
#define PLAT_FAB_DELIV_MAX_DATA   4096

typedef enum {
    PLAT_FAB_DELIV_OK     = 0,
    PLAT_FAB_DELIV_NOLINK = 1,  /* mbus_publish not linked */
    PLAT_FAB_DELIV_ERROR  = 2,
} plat_fab_deliv_status_t;

typedef struct plat_fab_deliv_result {
    plat_fab_deliv_status_t status;
    char                    topic[PLAT_FAB_DELIV_MAX_TOPIC];
    uint64_t                delivered_at_ms;
    char                    reason[128];
} plat_fab_deliv_result_t;

/* Deliver a fabric artifact payload to RA2C/MBus via the mbus_publish hook.
 * Returns 0 on success, -1 otherwise. */
int plat_fab_deliver(const char *topic,
                     const void *data, size_t datasz,
                     plat_fab_deliv_result_t *out);
include/platx/fabric_snap.h
/* platx/fabric_snap.h — INT-135: process.snapshot:v1 → VFS/Vault typed result.
 *
 * A gadget result for process.snapshot:v1 is not a raw buffer; it is a typed
 * snapshot record that carries provenance (context, lease) and a storage path.
 * The vault_artifact_write hook is weak — if unlinked the result is marked
 * not-stored and the call returns -1 without touching any shared state.
 */

#define PLAT_SNAP_CAP_NAME   "process.snapshot:v1"
#define PLAT_SNAP_MAX_PATH   256
#define PLAT_SNAP_REASON_MAX 128

typedef struct plat_snap_result {
    uint64_t context_id;
    uint32_t context_gen;
    uint64_t lease_id;
    uint64_t captured_at_ms;                  /* CLOCK_MONOTONIC */
    char     artifact_path[PLAT_SNAP_MAX_PATH];
    int      stored;                           /* 1 if written to vault */
    char     reason[PLAT_SNAP_REASON_MAX];
} plat_snap_result_t;

/* Capture a process snapshot and store it via the vault hook.
 * Returns 0 on success, -1 on failure (result->reason set). */
int plat_snap_capture(uint64_t context_id, uint32_t context_gen,
                      uint64_t lease_id, plat_snap_result_t *out);

/* Fill buf with the canonical artifact path for lease_id. */
int plat_snap_artifact_path(uint64_t lease_id, char *buf, size_t bufsz);
include/platx/platx_fabric.h
/* include/platx/platx_fabric.h — public fabric coordinator/journal/recover/gc/snapshot (task 2.43).
 *
 * INV-FABRIC-01: every provider operation has a witness record.
 * INV-FABRIC-02: journal full → operation rejected (no silent drop).
 * Static BSS journal buffer (§SEC-3: no malloc in critical path).
 */

/* ── journal geometry ───────────────────────────────────────────────── */
#define FABRIC_JOURNAL_SLOTS     4096u
#define FABRIC_JOURNAL_ENTRY_SZ  128u
#define FABRIC_SNAPSHOT_MAX_SZ   (FABRIC_JOURNAL_SLOTS * FABRIC_JOURNAL_ENTRY_SZ)

/* ── 2-phase commit states ──────────────────────────────────────────── */
typedef enum {
    FAB_2PC_IDLE     = 0,
    FAB_2PC_PREPARE  = 1,
    FAB_2PC_COMMIT   = 2,
    FAB_2PC_ABORT    = 3,
    FAB_2PC_DONE     = 4,
} fab_2pc_state_t;

/* ── journal entry (128 bytes, static BSS) ──────────────────────────── */
typedef struct __attribute__((packed)) {
    uint64_t       seq;              /* monotonic sequence number        */
    uint64_t       ts_mono_us;       /* CLOCK_MONOTONIC (µs)             */
    uint32_t       op_type;          /* operation type code              */
    uint32_t       flags;            /* FAB_JF_*                         */
    uint64_t       provider_id;      /* provider identity                */
    uint64_t       context_id;       /* fabric context                   */
    uint8_t        payload[80];      /* operation-specific data          */
} fabric_journal_entry_t;

_Static_assert(sizeof(fabric_journal_entry_t) == 128,
               "fabric_journal_entry_t must be 128 bytes");

/* ── journal flags ──────────────────────────────────────────────────── */
#define FAB_JF_PREPARE  0x0001u
#define FAB_JF_COMMIT   0x0002u
#define FAB_JF_ABORT    0x0004u
#define FAB_JF_WITNESS  0x0008u    /* INV-FABRIC-01: witness record       */
#define FAB_JF_SNAPSHOT 0x0010u

/* ── journal op types ───────────────────────────────────────────────── */
#define FAB_OP_REGISTER   1u
#define FAB_OP_PUBLISH    2u
#define FAB_OP_UNPUBLISH  3u
#define FAB_OP_WITNESS    4u
#define FAB_OP_SNAPSHOT   5u
#define FAB_OP_GC         6u

/* ── coordinator context ─────────────────────────────────────────────── */
typedef struct {
    uint64_t        ctx_id;
    fab_2pc_state_t state;
    uint32_t        participants;    /* bitmask                          */
    uint32_t        prepared;        /* bitmask                          */
    uint32_t        committed;       /* bitmask                          */
    uint64_t        started_ms;
    uint64_t        timeout_ms;
    int             result;          /* 0=commit, -1=abort               */
} fab_2pc_ctx_t;

/* ── coordinator API ─────────────────────────────────────────────────── */
int  fabric_coord_begin(uint64_t ctx_id, uint32_t participant_mask,
                        uint64_t timeout_ms, fab_2pc_ctx_t *out);
int  fabric_coord_prepare(uint64_t ctx_id, uint32_t participant_bit);
int  fabric_coord_commit(uint64_t ctx_id);
int  fabric_coord_abort(uint64_t ctx_id, const char *reason);
int  fabric_coord_get(uint64_t ctx_id, fab_2pc_ctx_t *out);

/* ── journal API ─────────────────────────────────────────────────────── */
/* INV-FABRIC-02: returns -1 (FABRIC_ERR_JOURNAL_FULL) if no slots left. */
int  fabric_journal_append(const fabric_journal_entry_t *e);
int  fabric_journal_read(uint64_t from_seq,
                         fabric_journal_entry_t *out, int max);
uint64_t fabric_journal_head_seq(void);
uint32_t fabric_journal_used(void);

/* ── recovery API ────────────────────────────────────────────────────── */
int  fabric_recover(void);   /* replay journal, rebuild coordinator state */

/* ── GC API ──────────────────────────────────────────────────────────── */
/* Remove entries whose seq < watermark and state == DONE/ABORT.         */
uint32_t fabric_gc_run(uint64_t watermark_seq);

/* ── snapshot API ────────────────────────────────────────────────────── */
typedef struct {
    uint64_t snap_seq;       /* journal seq at snapshot time             */
    uint64_t ts_mono_us;
    uint32_t entry_count;
    uint32_t crc32;          /* crc32 of all entries                     */
} fabric_snapshot_hdr_t;

int  fabric_snapshot_write(const char *path);
int  fabric_snapshot_read(const char *path,
                          fabric_snapshot_hdr_t *hdr_out);

/* ── error codes ─────────────────────────────────────────────────────── */
#define FABRIC_OK                 0
#define FABRIC_ERR_ARGS          -1
#define FABRIC_ERR_JOURNAL_FULL  -2   /* INV-FABRIC-02 */
#define FABRIC_ERR_NOT_FOUND     -3
#define FABRIC_ERR_STATE         -4
#define FABRIC_ERR_IO            -5
#define FABRIC_ERR_CRC           -6
include/platx/prov_conformance.h
/* platx/prov_conformance.h — the conformance suite (D098).
 *
 * A provider ABI is a set of promises. This is the code that checks a given
 * implementation actually keeps them — at runtime, against a live provider,
 * not by reading its source.
 *
 * The checks are chosen to be the ones that fail silently in production:
 *
 *   sense    an empty batch that means "I could not look"; an ack beyond what
 *            was handed out; an unbounded budget accepted; loss that does not
 *            appear in health.
 *   witness  a snapshot that claims more consistency than the provider said it
 *            could deliver; an OFFLINE snapshot carrying claims.
 *   action   a preview that changes something; a verify that only re-reads the
 *            provider's own bookkeeping instead of the world; a second commit
 *            under one idempotency key that changes the world twice.
 *
 * Every check is executed. A provider that "obviously" behaves correctly still
 * has to demonstrate it, because the failures above are exactly the ones that
 * look correct from the inside.
 */

#define PROV_CONF_FAILS_MAX 8u

typedef struct prov_conf_result {
    uint32_t checks;
    uint32_t failures;
    uint32_t n_reasons;
    char     reasons[PROV_CONF_FAILS_MAX][PROV_REASON_MAX];
} prov_conf_result_t;

int prov_conform_sense  (sense_provider_v1_t   *p, prov_conf_result_t *out);
int prov_conform_witness(witness_provider_v1_t *p, prov_conf_result_t *out);

/* The action suite needs a descriptor it is allowed to execute for real, and a
 * `world_still_there` probe that answers "is the target where it was" from
 * outside the provider. That probe is how the suite tells a verify that reads
 * the world from one that reads its own notes. */
typedef int (*prov_conf_world_fn)(void *ctx);

int prov_conform_action(action_provider_v1_t *p, const action_descriptor_t *d,
                        prov_conf_world_fn world_probe, void *world_ctx,
                        prov_conf_result_t *out);

const char *prov_conf_summary(const prov_conf_result_t *r, char *buf, size_t n);
include/platx/provider.h
/* platx/provider.h — Provider Fabric0 v1: the common security-provider envelope.
 *
 * Wave 2 (D051-D100) adds three provider ABIs on top of the SENSE observation
 * plane: sense_provider_v1 (facts), witness_provider_v1 (bounded snapshots and
 * claims) and action_provider_v1 (transactional change). This header holds what
 * all three share and nothing else.
 *
 * Normative contract:
 *   A provider states what it is, what it can see and how it could be wrong.
 *   No provider is trusted because it registered. Identity, build, boot,
 *   generation, epoch, quality, scope and independence group are declared and
 *   checked; a missing or self-contradicting declaration is a refusal, never a
 *   default.
 *   Every refusal carries a class (REFUSAL vs FAULT) and, for unavailability,
 *   a typed reason. "Returned zero results" and "could not look" are different
 *   facts and are never merged (D057).
 *   Two providers behind the same bridge are not independent, whatever they
 *   declare (D060, D089).
 *
 * This header depends on nothing private: no Hades header, no src/ path, no
 * kernel type. That is enforced by tests/cli/provider_abi_include_gate.sh
 * (D052-D054), because an ABI that needs a private header is not public.
 *
 * ABI freeze: PROV_ABI_VERSION 1 (D051 sealed 2026-09-01)
 */

/* ── ABI version ─────────────────────────────────────────────────────────── */
#define PROV_ABI_VERSION    1u
#define PROV_ABI_MINOR      0u

/* ── Bounds ──────────────────────────────────────────────────────────────── */
#define PROV_ID_MAX         64u   /* provider_id, capability */
#define PROV_REASON_MAX    128u
#define PROV_PROVIDERS_MAX  32u   /* fabric capacity; static, no allocation */

/* ── Provider kind ───────────────────────────────────────────────────────── */
typedef enum prov_kind {
    PROV_KIND_NONE    = 0,
    PROV_KIND_SENSE   = 1,   /* emits normalized facts into SENSE */
    PROV_KIND_WITNESS = 2,   /* answers bounded questions with claims */
    PROV_KIND_ACTION  = 3,   /* changes state transactionally */
    PROV_KIND_ENRICH  = 4,   /* deferred: contract only, D055 */
    PROV_KIND_MAX
} prov_kind_t;

/* ── Independence groups (D060, Reality Quorum) ───────────────────────────
 * The group answers one question: if this observation is wrong, what else is
 * wrong with it. Two providers in the same group share a failure mode and do
 * not corroborate each other, no matter how different their code is.
 */
typedef enum prov_indep_group {
    PROV_IG_NONE       = 0,   /* undeclared — refused at registration */
    PROV_IG_USERSPACE  = 1,   /* /proc, libc, userspace agents */
    PROV_IG_KERNEL_BPF = 2,   /* eBPF programs and maps */
    PROV_IG_KERNEL_LKM = 3,   /* in-kernel module, direct structures */
    PROV_IG_HYPERVISOR = 4,   /* below the kernel under test */
    PROV_IG_FIRMWARE   = 5,   /* measured boot, TPM, IMA baseline */
    PROV_IG_EXTERNAL   = 6,   /* another host or the network fabric */
    PROV_IG_MAX
} prov_indep_group_t;

/* ── Observation scope ───────────────────────────────────────────────────── */
#define PROV_SCOPE_HOST         (1u << 0)
#define PROV_SCOPE_CONTAINER    (1u << 1)
#define PROV_SCOPE_NETNS        (1u << 2)
#define PROV_SCOPE_MOUNTNS      (1u << 3)
#define PROV_SCOPE_PIDNS        (1u << 4)
#define PROV_SCOPE_FILE_SUBTREE (1u << 5)
#define PROV_SCOPE_PROC_SUBTREE (1u << 6)
#define PROV_SCOPE_SELF         (1u << 7)   /* the provider's own process only */

/* ── Declared quality ────────────────────────────────────────────────────── */
#define PROV_QUAL_DIRECT      (1u << 0)  /* reads the authority, not a copy */
#define PROV_QUAL_SYNTHESIZED (1u << 1)  /* some fields reconstructed */
#define PROV_QUAL_SAMPLED     (1u << 2)  /* not every occurrence is seen */
#define PROV_QUAL_LOSSY       (1u << 3)  /* bounded loss is expected */
#define PROV_QUAL_ORDERED     (1u << 4)  /* per-entity order is preserved */
#define PROV_QUAL_ATTRIBUTED  (1u << 5)  /* actor attribution is carried */

/* ── Trust ladder (mirrors sense_trust_t; kept independent of sense.h) ───── */
typedef enum prov_trust {
    PROV_TRUST_UNKNOWN    = 0,
    PROV_TRUST_USERSPACE  = 1,
    PROV_TRUST_MEDIATED   = 2,
    PROV_TRUST_KERNEL_BPF = 3,
    PROV_TRUST_HYPERVISOR = 4
} prov_trust_t;

/* ── The envelope (D051) ─────────────────────────────────────────────────
 * One struct, declared once at registration and re-read on every epoch bump.
 * `bridge_id` is the transport a claim actually arrives through: an LKM and a
 * hypervisor that both report through the same bridge fail independence even
 * though their groups differ (D089).
 */
typedef struct prov_envelope {
    uint32_t abi_version;          /* PROV_ABI_VERSION */
    uint32_t struct_size;          /* sizeof(prov_envelope_t) */

    char     provider_id[PROV_ID_MAX];  /* stable, no aliases */
    char     capability[PROV_ID_MAX];   /* published capability name */

    uint32_t kind;                 /* prov_kind_t */
    uint32_t trust;                /* prov_trust_t */

    uint8_t  build_id[20];         /* ELF build-id, or all zero */
    uint8_t  config_digest[32];    /* SHA-256 of the active config */
    uint32_t module_version;

    uint64_t boot_id[2];           /* host boot identity, 128 bit */
    uint64_t generation;           /* bumps when a config is committed */
    uint64_t epoch;                /* bumps on reload/reset/reattach (D058) */

    uint32_t quality;              /* PROV_QUAL_* */
    uint32_t scope;                /* PROV_SCOPE_* */
    uint32_t independence_group;   /* prov_indep_group_t; NONE is refused */
    uint32_t bridge_id;            /* 0 = in-process, no bridge */

    uint8_t  reserved[32];
} prov_envelope_t;

_Static_assert(sizeof(prov_envelope_t) == 280,
               "prov_envelope_t ABI size changed");

/* ── Error classes (AI seam: two classes of error) ────────────────────────
 * REFUSAL: the contract says no. Retrying the same call changes nothing.
 * FAULT:   the mechanism failed. The same call may succeed later.
 * Merging the two is how a permanent policy denial gets retried forever and
 * how a broken sensor gets read as "policy".
 */
typedef enum prov_err_class {
    PROV_ERRC_NONE    = 0,
    PROV_ERRC_REFUSAL = 1,
    PROV_ERRC_FAULT   = 2
} prov_err_class_t;

/* ── Typed unavailability (D057) ─────────────────────────────────────────── */
typedef enum prov_unavail {
    PROV_UNAVAIL_NONE          = 0,
    PROV_UNAVAIL_ABI_TOO_OLD   = 1,
    PROV_UNAVAIL_ABI_TOO_NEW   = 2,
    PROV_UNAVAIL_SCHEMA        = 3,  /* no common schema major */
    PROV_UNAVAIL_CAPABILITY    = 4,  /* the provider lacks the asked capability */
    PROV_UNAVAIL_PRIVILEGE     = 5,  /* CAP_* / uid boundary */
    PROV_UNAVAIL_KERNEL        = 6,  /* kernel feature, BTF, LSM absent */
    PROV_UNAVAIL_CONFIG        = 7,  /* config refused by the provider */
    PROV_UNAVAIL_BUDGET        = 8,  /* the request cannot be served in budget */
    PROV_UNAVAIL_RETIRED       = 9,  /* generation or epoch is stale */
    PROV_UNAVAIL_UNBOUNDED     = 10, /* the request declared no bound (D065) */
    PROV_UNAVAIL_MAX
} prov_unavail_t;

/* ── Result of any provider call ─────────────────────────────────────────── */
typedef struct prov_status {
    int32_t  rc;                       /* PROV_OK or PROV_ERR_* */
    uint32_t err_class;                /* prov_err_class_t */
    uint32_t unavail;                  /* prov_unavail_t; NONE when rc == OK */
    uint32_t _pad;
    char     reason[PROV_REASON_MAX];  /* human text; never parsed by policy */
} prov_status_t;

/* ── Negotiation (D057) ──────────────────────────────────────────────────── */
typedef struct prov_negotiate_req {
    uint32_t abi_version;      /* caller's PROV_ABI_VERSION */
    uint32_t struct_size;
    uint32_t kind;             /* prov_kind_t the caller wants */
    uint32_t schema_min;       /* caller's supported schema major range */
    uint32_t schema_max;
    uint32_t capability_need;  /* provider-kind specific capability bits */
} prov_negotiate_req_t;

typedef struct prov_negotiate_result {
    uint32_t abi_version;      /* agreed ABI major */
    uint32_t schema_major;     /* agreed schema major */
    uint32_t capability_grant; /* subset of capability_need actually granted */
    uint32_t _pad;
    prov_status_t status;      /* rc == PROV_OK only if all three agreed */
} prov_negotiate_result_t;

/* ── Budgets and privacy (D065) ──────────────────────────────────────────
 * A request without a bound is refused with PROV_UNAVAIL_UNBOUNDED. An
 * unbounded request is not a generous request; it is one whose cost cannot be
 * refused later, which is the same as having no admission control at all.
 */
#define PROV_PRIV_NONE      0u
#define PROV_PRIV_IDENTITY  (1u << 0)  /* uid, user names, entity identity */
#define PROV_PRIV_PATH      (1u << 1)  /* filesystem paths */
#define PROV_PRIV_CONTENT   (1u << 2)  /* file or packet content */
#define PROV_PRIV_NETPEER   (1u << 3)  /* remote addresses */
#define PROV_PRIV_CMDLINE   (1u << 4)  /* process arguments and environment */

typedef struct prov_budget {
    uint32_t abi_version;
    uint32_t struct_size;
    uint64_t max_records;      /* > 0 required */
    uint64_t max_bytes;        /* > 0 required */
    uint64_t deadline_ns;      /* per-operation budget in nanoseconds, > 0
                                * required. RELATIVE, not an absolute clock
                                * reading: a stream budget outlives any single
                                * call, so an absolute deadline stored at
                                * publish time would already be in the past by
                                * the first read. The fabric turns it into an
                                * absolute deadline per call. */
    uint32_t max_cpu_permille; /* 1..1000; 0 refused */
    uint32_t privacy_lease;    /* PROV_PRIV_* the caller actually holds */
    uint32_t privacy_required; /* out: what the provider needed */
    uint32_t flags;
} prov_budget_t;

/* Monotonic nanoseconds. One clock for every deadline in the fabric: mixing
 * CLOCK_REALTIME in would make a deadline jump when the host's time is
 * corrected, and a security plane whose timeouts move with NTP is one an
 * attacker can extend. */
uint64_t prov_now_ns(void);

/* ── Cancellation ────────────────────────────────────────────────────────
 * A deadline answers "how long may this take"; cancellation answers "stop
 * now, the answer is no longer wanted". Both are needed, and neither implies
 * the other: work cancelled at 1ms and work that ran out of budget at 10s must
 * be distinguishable in the audit trail, because one is an operator's decision
 * and the other is a resource fact.
 *
 * Cancellation is cooperative and checked between units of work. Nothing is
 * ever cancelled mid-commit: a transaction that has changed the world is
 * finished or rolled back, never abandoned.
 */
typedef struct prov_cancel {
    int requested;
    char reason[PROV_REASON_MAX];
} prov_cancel_t;

void prov_cancel_init   (prov_cancel_t *c);
void prov_cancel_request(prov_cancel_t *c, const char *reason);
int  prov_cancel_check  (const prov_cancel_t *c);   /* 1 = stop */

/* Validate a budget envelope. Fills `st` and returns its rc.
 * Refuses (REFUSAL/UNBOUNDED) when any bound is zero, and
 * (REFUSAL/PRIVACY-as-CAPABILITY) when privacy_required is not covered by
 * privacy_lease. Never rewrites the caller's numbers. */
int prov_budget_check(const prov_budget_t *b, uint32_t privacy_required,
                      prov_status_t *st);

/* ── Envelope validation (D051) ──────────────────────────────────────────
 * A declaration that contradicts itself is refused here, once, rather than
 * being half-believed by every consumer:
 *   - abi/struct_size mismatch
 *   - empty provider_id or capability
 *   - kind or trust out of range
 *   - independence_group NONE or out of range
 *   - scope == 0 (a provider that sees nothing declares nothing)
 *   - generation == 0 (there is no epoch zero, as elsewhere in the platform)
 *   - PROV_QUAL_DIRECT together with PROV_QUAL_SYNTHESIZED
 *   - PROV_TRUST_HYPERVISOR with PROV_IG_USERSPACE, and the BPF/LKM pairs:
 *     the trust ladder and the failure group must agree.
 */
int prov_envelope_check(const prov_envelope_t *e, prov_status_t *st);

/* Fill `st` in one place so callers cannot forget a field. Returns rc. */
int prov_status_set(prov_status_t *st, int32_t rc, prov_err_class_t cls,
                    prov_unavail_t un, const char *reason);

/* Human-readable names for logs and evidence. Never NULL. */
const char *prov_kind_name(uint32_t kind);
const char *prov_unavail_name(uint32_t unavail);
const char *prov_indep_group_name(uint32_t group);

/* ── Provider lifecycle state ────────────────────────────────────────────
 * The fabric's own view of a provider. It is not the sensor lifecycle of
 * sense.h: a provider can be PUBLISHED while the source behind it is BLIND,
 * and the difference is exactly what a consumer needs to see.
 */
typedef enum prov_state {
    PROV_STATE_DECLARED   = 0,  /* envelope accepted, nothing negotiated */
    PROV_STATE_NEGOTIATED = 1,  /* ABI/schema/capability agreed */
    PROV_STATE_PUBLISHED  = 2,  /* capability visible, read-back confirmed */
    PROV_STATE_DEGRADED   = 3,  /* serving with declared loss */
    PROV_STATE_BLIND      = 4,  /* cannot see; no auto-recovery */
    PROV_STATE_RETIRED    = 5,  /* epoch closed; calls refused as STALE */
    PROV_STATE_FAILED     = 6,  /* mechanism broke; re-registration required */
    PROV_STATE_MAX
} prov_state_t;

/* ── Health projection (D059) ────────────────────────────────────────────
 * Reported, never inferred. `selftest_passed == 2` means the self-test has
 * never run: a provider that was never tested must not read as one that
 * passed. `last_gap_*` is the projection of the newest gap, so a consumer can
 * tell "nothing happened" from "nobody was looking" without a second call.
 */
typedef struct prov_health {
    uint32_t abi_version;
    uint32_t struct_size;
    uint32_t state;             /* prov_state_t */
    uint32_t quality_now;       /* PROV_QUAL_* as observed, not as declared */

    uint64_t epoch;             /* the epoch this report describes */
    uint64_t generation;

    uint64_t records_emitted;
    uint64_t records_lost;      /* loss counted by the provider itself */
    uint64_t kernel_lost;       /* loss reported by the kernel ring, if any */

    uint64_t cursor_seq;        /* newest sequence handed to a consumer */
    uint64_t acked_seq;         /* newest sequence a consumer acknowledged */

    uint64_t last_gap_first;    /* 0 = no gap has ever been opened */
    uint64_t last_gap_last;
    uint32_t last_gap_open;     /* 1 = the newest gap is still open */
    uint32_t selftest_passed;   /* 0 failed, 1 passed, 2 never run */
    uint64_t selftest_at_ns;    /* 0 = never */
    uint64_t heartbeat_at_ns;   /* 0 = no heartbeat capability */
} prov_health_t;

_Static_assert(sizeof(prov_health_t) == 112,
               "prov_health_t ABI size changed");

typedef struct prov_selftest {
    uint32_t abi_version;
    uint32_t struct_size;
    uint32_t passed;            /* non-zero: every sub-test passed */
    uint32_t tests_run;
    uint32_t tests_failed;
    uint32_t _pad;
    uint64_t at_ns;
    char     detail[PROV_REASON_MAX];
} prov_selftest_t;

/* ── Error codes ─────────────────────────────────────────────────────────── */
#define PROV_OK                 0
#define PROV_ERR_INVAL         -1
#define PROV_ERR_FULL          -2
#define PROV_ERR_NOT_FOUND     -3
#define PROV_ERR_DUPLICATE     -4
#define PROV_ERR_STALE_EPOCH   -5
#define PROV_ERR_STALE_GEN     -6
#define PROV_ERR_UNAVAILABLE   -7   /* see prov_status_t.unavail */
#define PROV_ERR_UNSUPPORTED   -8
#define PROV_ERR_BUSY          -9
#define PROV_ERR_TIMEOUT      -10
#define PROV_ERR_PRIVACY      -11
#define PROV_ERR_STATE        -12
#define PROV_ERR_IO           -13
#define PROV_ERR_NOMEM        -14
#define PROV_ERR_INDEPENDENCE -15   /* declared independence is not real */
#define PROV_ERR_CANCELLED    -16   /* an operator asked for it to stop */
include/platx/provider_fabric.h
/* platx/provider_fabric.h — the provider fabric (D056-D060, D063).
 *
 * The fabric is deliberately NOT a fifth registry. PLATX already has four
 * tables that answer "what exists": the XIO fd registry, the XIM instance
 * table, the plugin manager and the SENSE source registry. A sense provider
 * registered here becomes a SENSE source in the SENSE registry — that registry
 * stays the single answer to "is this capability published", and the fabric
 * holds only what SENSE has no field for: the envelope, the negotiated result,
 * the epoch and the provider vtable.
 *
 * The consequence is worth stating plainly: prov_publish() cannot report a
 * capability as published unless sense_source_cap_published() says so. There
 * is no second bookkeeping to disagree with.
 *
 * Locking: PLATX_LR_PROV_FABRIC (116) is taken before PLATX_LR_SENSE_REGISTRY
 * (213). No provider vtable entry is ever called while the fabric lock is
 * held, for the same reason SENSE056 forbids it.
 */

/* ── Bring-up of the whole plane (R1 shipped-profile wiring) ─────────────
 * The fabric is useless on its own: it needs the SENSE registry, the ring, the
 * coverage plane and the evidence facade. plat_fabric_boot() brings all of
 * them up as one unit and plat_fabric_shutdown() takes them down, because a
 * half-initialised observation plane is worse than none — it answers.
 *
 * Booting does NOT attach any sensor. Kernel sensors are attached only when an
 * operator asks (`fabric attach …`), for the same reason REG_hades() registers
 * a CLI and does not autostart sensors: a platform that starts watching the
 * kernel because it was linked in has made that decision for its owner.
 *
 * Teardown is checkable rather than asserted: after shutdown the source count,
 * the consumer count and the provider count are all zero, and
 * plat_fabric_leftovers() returns 0.
 */
int plat_fabric_boot(void);
int plat_fabric_shutdown(void);
int plat_fabric_is_up(void);
int plat_fabric_leftovers(void);   /* sources + consumers + providers left */

/* ── Lifecycle ───────────────────────────────────────────────────────────── */
int  prov_fabric_init(void);
void prov_fabric_fini(void);

/* ── Registration (D056) ─────────────────────────────────────────────────
 * The descriptor is the caller's and must outlive its registration, the same
 * contract SENSE and OBS0 already state. describe() is called once, outside
 * the fabric lock, and its envelope is checked before anything is stored.
 *
 * A sense provider is additionally registered as a SENSE source under the same
 * provider_id, so that the SENSE registry — not the fabric — owns publication.
 */
int prov_register_sense  (sense_provider_v1_t   *p);
int prov_register_witness(witness_provider_v1_t *p);
int prov_register_action (action_provider_v1_t  *p);

/* Remove a provider. Streams are closed and, for a sense provider, the SENSE
 * source is deregistered. Idempotent for an unknown id: PROV_ERR_NOT_FOUND. */
int prov_deregister(const char *provider_id);

/* ── Negotiation (D057) ──────────────────────────────────────────────────
 * Pure resolution: no state, no locks. A provider implementation calls this
 * from its own negotiate() so that every provider refuses the same way.
 */
int prov_negotiate_resolve(const prov_negotiate_req_t *req,
                           uint32_t provider_abi,
                           uint32_t schema_min, uint32_t schema_max,
                           uint32_t capability_supported,
                           prov_negotiate_result_t *out);

/* Negotiate against a registered provider and remember the result. */
int prov_negotiate(const char *provider_id, const prov_negotiate_req_t *req,
                   prov_negotiate_result_t *out);

/* ── Budgets (D065) ──────────────────────────────────────────────────────
 * Publication refuses a provider that has no budget: an unbounded stream is
 * one whose cost cannot be refused later. The budget is validated by
 * prov_budget_check() before it is stored, so an impossible budget is
 * rejected at the moment it is set rather than at the moment it is exceeded.
 */
int prov_set_budget(const char *provider_id, const prov_budget_t *b);
int prov_get_budget(const char *provider_id, prov_budget_t *out);

/* ── Callback-under-lock detector ────────────────────────────────────────
 * Mirrors SENSE056: increments if a provider vtable entry is ever entered
 * while this thread holds the fabric lock. A test reads it instead of waiting
 * for a deadlock to prove the point.
 */
unsigned prov_callback_under_lock_violations(void);
void     prov_callback_under_lock_reset(void);

/* ── Publication ─────────────────────────────────────────────────────────
 * For a sense provider: validate → prepare → commit → start on the SENSE
 * registry, then read the capability back. The read-back is the fact; the
 * return code of start() is only a claim (SENSE052).
 */
int prov_publish  (const char *provider_id, uint64_t owner);
int prov_unpublish(const char *provider_id, uint64_t owner);
int prov_is_published(const char *provider_id);

/* ── Pump: provider → SENSE ring (D100) ──────────────────────────────────
 * The missing link between "a provider produced records" and "SENSE has them".
 * Reads up to `max_batches` batches from a published sense provider and
 * publishes every record into the SENSE ring under `owner`.
 *
 * Loss is not smoothed over on the way through: a record the ring refuses
 * (oversized, retired owner) is counted in `refused_out` and reported, because
 * a pump that silently drops what it cannot deliver turns a loud provider into
 * a quiet host — the exact failure this plane exists to prevent.
 */
int prov_pump(const char *provider_id, uint64_t owner, uint32_t max_batches,
              uint64_t *published_out, uint64_t *refused_out,
              uint64_t *lost_out, prov_status_t *st);

/* The pump enforces two limits between batches, and reports what it managed to
 * do in both cases rather than discarding it:
 *
 *   deadline      the provider's own budget (prov_set_budget) is a
 *                 per-operation allowance; the pump turns it into an absolute
 *                 deadline at entry and stops at it with PROV_ERR_TIMEOUT.
 *   cancellation  prov_cancel_provider() sets a flag the pump checks between
 *                 batches; it stops with PROV_ERR_CANCELLED and the reason the
 *                 canceller gave.
 *
 * Neither ever interrupts a batch that is already being published: a partial
 * batch in the ring would be loss without a gap, which is the one thing this
 * plane must not produce.
 */
int prov_cancel_provider(const char *provider_id, const char *reason);
int prov_cancel_clear   (const char *provider_id);
int prov_cancel_pending (const char *provider_id);

/* ── Epoch (D058) ────────────────────────────────────────────────────────
 * An epoch bump means: everything handed out before this moment refers to a
 * world that no longer exists. Cursors are invalidated, open streams are
 * closed, and the provider must report a strictly greater epoch — a provider
 * that reloads without moving its epoch is refused, because a stale record
 * would then be indistinguishable from a fresh one (D084).
 */
typedef enum prov_epoch_reason {
    PROV_EPOCH_RELOAD   = 1,
    PROV_EPOCH_RESET    = 2,
    PROV_EPOCH_REATTACH = 3,
    PROV_EPOCH_MAX
} prov_epoch_reason_t;

int prov_epoch_bump(const char *provider_id, prov_epoch_reason_t reason,
                    prov_status_t *st);
int prov_epoch_get (const char *provider_id, uint64_t *epoch_out);

/* Is `epoch` still the live epoch of this provider? A record carrying an older
 * epoch is stale and must be refused, not renumbered. */
int prov_epoch_is_current(const char *provider_id, uint64_t epoch);

/* ── Health (D059) ───────────────────────────────────────────────────────
 * Asks the provider, then checks the answer for self-contradiction before
 * returning it: acked beyond cursor, an open gap with no first sequence, a
 * self-test timestamp with no verdict. A health report that cannot be true is
 * a FAULT, not a health report.
 */
int prov_health(const char *provider_id, prov_health_t *out, prov_status_t *st);
int prov_selftest(const char *provider_id, prov_selftest_t *out,
                  prov_status_t *st);
int prov_state(const char *provider_id);   /* prov_state_t, or -1 */

/* ── Independence and quorum (D060, D089) ────────────────────────────────
 * Two providers corroborate each other only when they can fail separately.
 * `bridge_id` collapses them: an LKM and a hypervisor that both report through
 * the same bridge share that bridge's failure mode, so they count once —
 * whatever their declared groups say.
 *
 * Returns PROV_OK when at least `min_groups` genuinely independent groups are
 * present, PROV_ERR_INDEPENDENCE otherwise, with the reason naming the
 * collapse. `groups_out` receives the effective count.
 */
int prov_quorum_check(const char *const *provider_ids, size_t n,
                      uint32_t min_groups, uint32_t *groups_out,
                      prov_status_t *st);

/* ── Witness snapshot coordinator (D063, D064) ───────────────────────────
 * Takes one snapshot across several witnesses and reports the consistency it
 * ACHIEVED, never the one it was asked for. Each participant is graded against
 * two things: what it said it could do (max_consistency) and what its own
 * snapshot shows. A walk labelled ATOMIC without a freeze token, or with
 * deltas inside the window, is downgraded and the downgrade is counted.
 *
 * This is the difference between a quorum and a chorus: a coordinator that
 * accepts labels produces a very confident report about nothing.
 */
#define WITNESS_COORD_MAX 8u

typedef struct witness_coord_part {
    char     provider_id[PROV_ID_MAX];
    uint32_t declared_max;   /* what max_consistency() reported */
    uint32_t claimed;        /* what the snapshot labelled itself */
    uint32_t achieved;       /* what the coordinator accepted */
    uint32_t downgraded;     /* 1 = the claim was not honoured */
    uint32_t n_claims;
    uint32_t _pad;
    char     reason[PROV_REASON_MAX];
} witness_coord_part_t;

typedef struct witness_coord_result {
    uint32_t consistency;        /* overall achieved: the weakest participant */
    uint32_t n_parts;
    uint32_t violations;         /* claims above declared capability */
    uint32_t independent_groups; /* after bridge collapse (D089) */
    uint64_t window_begin_ns;
    uint64_t window_end_ns;
    witness_coord_part_t parts[WITNESS_COORD_MAX];
} witness_coord_result_t;

/* Grade one snapshot on its own evidence. Returns the accepted consistency and
 * writes the reason when it is below the claim. Pure: no locks, no registry. */
uint32_t witness_coord_grade(const witness_snapshot_t *snap,
                             uint32_t declared_max,
                             char *why, size_t whysz);

int witness_coord_snapshot(const char *const *provider_ids, size_t n,
                           uint32_t subject, const prov_budget_t *budget,
                           witness_coord_result_t *out, prov_status_t *st);

/* Enumerate registered providers (evidence and CLI). Returns the count. */
int prov_list(char ids[][PROV_ID_MAX], uint32_t max);
int prov_count(void);

/* Vtable of a registered provider, or NULL. The pointer is the caller's
 * descriptor, which must outlive its registration; the fabric never copies a
 * vtable, so there is no second one to go stale. */
sense_provider_v1_t   *prov_sense_ptr  (const char *provider_id);
witness_provider_v1_t *prov_witness_ptr(const char *provider_id);
action_provider_v1_t  *prov_action_ptr (const char *provider_id);

/* Envelope of a registered provider, as accepted. */
int prov_envelope_get(const char *provider_id, prov_envelope_t *out);
include/platx/provider_sdk.h
/* platx/provider_sdk.h — the provider-side SDK (D061, D062).
 *
 * Every sense provider has to do the same four things correctly, and they are
 * the four things hand-written providers get wrong:
 *
 *   1. number records so a consumer can tell a missing one from a late one;
 *   2. respect an unacked window instead of buffering without limit;
 *   3. when a record is dropped, SAY SO — count it and open a gap, in the same
 *      call that drops it, not in a later summary;
 *   4. report health as a projection of those counters rather than as an
 *      opinion about them.
 *
 * The emitter below is that, and nothing else: no threads, no allocation, no
 * I/O. A provider fills it, flushes a batch and answers health from it. The
 * invariant a conformance test can check from outside is exact:
 *
 *      offered == emitted + dropped     and     dropped > 0 implies a gap
 *
 * A provider that loses a record without a gap is not "slightly lossy"; it is
 * reporting silence as absence, which is the failure this whole plane exists
 * to make impossible.
 */

#define PROV_SDK_BATCH_MAX   64u    /* records per batch */
#define PROV_SDK_RECORD_MAX 512u    /* one record; mirrors SENSE_SLOT_BYTES */
#define PROV_SDK_BUF_BYTES  (PROV_SDK_BATCH_MAX * PROV_SDK_RECORD_MAX)

typedef struct prov_emitter {
    uint64_t epoch;
    uint64_t next_seq;       /* sequence the next accepted record will get */
    uint64_t acked_seq;      /* newest sequence acknowledged by the consumer */
    uint32_t window;         /* max unacked records; 0 is refused at init */
    uint32_t _pad;

    uint64_t offered;        /* records handed to the emitter, plus records
                              * the transport says existed and lost */
    uint64_t emitted;        /* records accepted into a batch */
    uint64_t dropped;        /* records refused and counted as loss */
    uint64_t kernel_lost;    /* loss the transport reported (ring overwrite) */

    uint64_t gap_first;      /* first missing sequence of the newest gap */
    uint64_t gap_last;       /* last missing sequence seen so far */
    uint32_t gap_open;
    uint32_t batches;        /* flushes so far; 0 marks the epoch start */

    uint64_t lost_before;    /* loss to declare on the next flush */

    uint32_t n;              /* records staged in the current batch */
    uint32_t used;           /* bytes staged */
    uint64_t first_seq;      /* sequence of records[0] in the current batch */

    uint32_t lens[PROV_SDK_BATCH_MAX];
    uint8_t  buf[PROV_SDK_BUF_BYTES];
} prov_emitter_t;

/* Initialise for one stream of one epoch. `window` is the unacked ceiling and
 * must be non-zero: an emitter with no window is an unbounded queue. */
int prov_emit_init(prov_emitter_t *e, uint64_t epoch, uint32_t window,
                   prov_status_t *st);

/* Stage one already-encoded record.
 *   PROV_OK              accepted, `seq_out` receives its sequence
 *   PROV_ERR_FULL        the batch is full — flush and retry; NOT a loss
 *   PROV_ERR_BUSY        the unacked window is exhausted: the record is
 *                        DROPPED, counted, and a gap is opened in this call
 *   PROV_ERR_INVAL       len is 0 or above PROV_SDK_RECORD_MAX
 */
int prov_emit_record(prov_emitter_t *e, const uint8_t *wire, uint32_t len,
                     uint64_t *seq_out, prov_status_t *st);

/* Publish the staged records as a batch. An empty batch is legal only when
 * there is no pending loss to declare; pending loss must reach the consumer,
 * so a flush with dropped records and no data still produces a batch carrying
 * SENSE_BATCH_F_GAP_BEFORE. `out->wire` points into the emitter and is valid
 * until the next record/flush call. */
int prov_emit_flush(prov_emitter_t *e, sense_prov_batch_t *out,
                    prov_status_t *st);

/* Acknowledge through a sequence. Acking beyond what was handed out is a
 * REFUSAL: it would silently reopen the window on records the consumer never
 * saw. Acking backwards is accepted and changes nothing. */
int prov_emit_ack(prov_emitter_t *e, uint64_t through_seq, prov_status_t *st);

/* Loss that happened before the emitter saw it: a kernel ring overwrote `n`
 * records, a reader was too slow, a transport dropped a batch. It counts as
 * both offered and dropped — those records existed — and it opens a gap in
 * this call, for the same reason a locally dropped record does.
 *
 * The alternative, "count it in health and move on", is exactly how a consumer
 * ends up reading a quiet stream as a quiet host. */
int prov_emit_lost_external(prov_emitter_t *e, uint64_t n, prov_status_t *st);

/* Close the open gap once the missing range is known to be bounded. */
int prov_emit_gap_close(prov_emitter_t *e, prov_status_t *st);

/* Fill a health projection from the emitter's own counters (D059). */
int prov_emit_health(const prov_emitter_t *e, uint32_t state,
                     uint32_t quality_now, prov_health_t *out);

/* The invariant, checkable from outside: offered == emitted + dropped, and a
 * non-zero drop count implies a gap was opened. Returns 1 when it holds. */
int prov_emit_accounting_balanced(const prov_emitter_t *e);
include/platx/witness_providers.h
/* platx/witness_providers.h — the four standard witnesses (D085-D090).
 *
 * Each one answers the same question from a different place, and the whole
 * point is that they can fail separately:
 *
 *   procfs   userspace, always available, always BEST_EFFORT. It is the view
 *            an attacker on the host can edit, which is exactly why it is
 *            worth having: it is the "before" side of every divergence.
 *   lkm      inside the kernel, reading the structures procfs is generated
 *            from. Can be ATOMIC when it takes the walk under a freeze.
 *   hv       below the kernel under test. Explicitly architecture-bound: on a
 *            machine it does not cover it says OFFLINE, never "nothing found".
 *   ima      measurements made before either of the above existed.
 *
 * Every one of them may be OFFLINE, and OFFLINE is a first-class answer: a
 * witness that cannot look must not return an empty list, because an empty
 * list is an assertion about the world.
 */

/* Common identity for a witness instance. */
typedef struct witness_cfg {
    const char *provider_id;
    const char *capability;
    uint64_t    epoch;          /* 0 refused */
    uint64_t    generation;     /* 0 refused */
    uint64_t    boot_id_hash;
    uint32_t    bridge_id;      /* 0 = in-process; shared value collapses
                                 * independence with anything else behind it */
} witness_cfg_t;

/* ── procfs witness (userspace group) ────────────────────────────────────── */
typedef struct witness_procfs {
    witness_cfg_t cfg;
    witness_provider_v1_t vtable;
    const char *proc_root;      /* NULL = "/proc"; a test may point elsewhere */
    uint64_t walks;
    uint64_t claims_last;
} witness_procfs_t;

int witness_procfs_init(witness_procfs_t *w, const witness_cfg_t *cfg,
                        const char *proc_root, prov_status_t *st);

/* ── LKM witness (kernel-module group) ───────────────────────────────────
 * The transport is injected: on a host with the SelfProtect module loaded it
 * is a real query, in a test it is a fixture. Returning -1 means the module is
 * not there, and the provider then reports OFFLINE rather than an empty walk.
 * A non-zero freeze token means the walk was taken under a freeze, which is
 * the only thing that lets the snapshot claim ATOMIC.
 */
typedef int (*witness_kernel_query_fn)(void *ctx, uint32_t subject,
                                       witness_claim_t *out, uint32_t max,
                                       uint64_t *freeze_token_out);

typedef struct witness_lkm {
    witness_cfg_t cfg;
    witness_provider_v1_t vtable;
    witness_kernel_query_fn query;
    void *query_ctx;
} witness_lkm_t;

int witness_lkm_init(witness_lkm_t *w, const witness_cfg_t *cfg,
                     witness_kernel_query_fn query, void *query_ctx,
                     prov_status_t *st);

/* ── hypervisor witness (hypervisor group) ───────────────────────────────
 * `arch` is the architecture this instance claims to cover ("x86_64"); when it
 * does not match the machine, every snapshot is OFFLINE with that reason.
 * Claims are sealed into the FORENSIC facade and carry handles, never bytes.
 */
typedef struct witness_hv {
    witness_cfg_t cfg;
    witness_provider_v1_t vtable;
    witness_kernel_query_fn query;
    void *query_ctx;
    const char *arch;           /* NULL = "x86_64" */
    const char *machine;        /* NULL = uname -m at init */
    uint64_t sealed;
} witness_hv_t;

int witness_hv_init(witness_hv_t *w, const witness_cfg_t *cfg,
                    witness_kernel_query_fn query, void *query_ctx,
                    const char *arch, const char *machine_override,
                    prov_status_t *st);

/* ── IMA / measurement witness (firmware group, D090) ────────────────────── */
typedef struct witness_ima {
    witness_cfg_t cfg;
    witness_provider_v1_t vtable;
    const char *measurements_path;   /* NULL = the kernel's IMA log */
    const char *baseline_path;       /* NULL = no baseline comparison */
    uint64_t entries_last;
    uint64_t mismatches_last;
} witness_ima_t;

int witness_ima_init(witness_ima_t *w, const witness_cfg_t *cfg,
                     const char *measurements_path, const char *baseline_path,
                     prov_status_t *st);

witness_provider_v1_t *witness_procfs_provider(witness_procfs_t *w);
witness_provider_v1_t *witness_lkm_provider(witness_lkm_t *w);
witness_provider_v1_t *witness_hv_provider(witness_hv_t *w);
witness_provider_v1_t *witness_ima_provider(witness_ima_t *w);

/* ── divergence ──────────────────────────────────────────────────────────
 * Claims present in `a` and absent from `b`, copied into `out` with the HIDDEN
 * flag set. This is the shape of every interesting finding in this platform:
 * not "a sensor reported something bad", but "two witnesses that should agree
 * do not, and here is exactly where".
 *
 * Only comparable when both snapshots actually looked: if either is OFFLINE
 * the answer is 0 and `why` says so, because "absent from a witness that could
 * not look" is not absence.
 */
uint32_t witness_snapshot_diff(const witness_snapshot_t *a,
                               const witness_snapshot_t *b,
                               witness_claim_t *out, uint32_t max,
                               char *why, size_t whysz);
14

flow

Пассивный анализ пакетов и потоков
src/flow/Наблюдение и исследование3 файлов3 API headers

Flow выполняет пассивную нормализацию packet/connection observations в bounded flow records и summaries. Он не является transport/proxy и не должен влиять на forwarding или lifecycle наблюдаемого traffic.

Граница ответственности

  • Input — captured records/pcap/event, output — immutable flow facts.
  • No payload retention by default; policy controls sampling/redaction.
  • MBus/Audit export asynchronous и не блокирует capture source.

Устройство подсистемы

  • Decoder layer bounds-checks L2/L3/L4 and selected application metadata.
  • Flow table имеет key, owner/source, timestamps, counters, classification и bounded eviction policy.
  • Tick expires idle records and emits summary; clock monotonic.
  • Schema adapter отправляет MBus/observe/audit records с correlation when available.

Поток работы

  • Capture source → decode/normalize.
  • Lookup/update bounded flow table.
  • Tick/terminal → summary.
  • Export metrics/event without feedback to source.

Отказ и восстановление

  • Malformed/truncated packet increments reason counter, no overread.
  • Table full uses documented eviction/reject, не unbounded malloc.
  • Slow consumer triggers drop/backpressure metrics.
  • Clock jump не ломает timeout благодаря monotonic time.

Основные возможности

  • Decodes packet/pcap-derived flow information.
  • Emits compact MBus/Audit summaries.
  • Operator tick/stats interface supports bounded passive analysis.

Управление и диагностика

Домен не регистрирует собственную корневую команду. Его контракт используется вызывающими подсистемами через API и интерфейсы audit, ndr.

Справочник CLI / flow →
Состав подсистемы / 3 файлов
Файл / компонентНазначение и граница
src/flow/flow_decoder.cFlow packet/pcap decoder with fuzz hardening. STAB-129: MAX_PKT_LEN, MAX_PCAP_RECORD, MAX_AUDIT_RECORD, overflow guards.
src/flow/flow_hades_enrich.cINT-126: Flow metadata enrichment for Hades events.
src/flow/flow_mbus.cFlow summaries to MBus/Audit (INT-106).
Контракты API / 3 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/flow_decoder.h
/* platx/flow_decoder.h — декодер пакетов/pcap/audit с границами (STAB-129).
 *
 * src/flow/flow_decoder.c включал "flow_decoder.h", которого в дереве нет:
 * файл не компилировался ни разу, а значит ни одна из его границ ни разу
 * не проверялась компилятором. Пределы объявлены здесь, чтобы вызывающий
 * мог сверить свой буфер с той же константой, а не с копией.
 */
/* (a) максимальная длина принимаемого сырого пакета */
#define MAX_PKT_LEN        65535u
/* (b) максимальный размер записи pcap */
#define MAX_PCAP_RECORD   262144u
/* (c) максимальный размер записи аудита */
#define MAX_AUDIT_RECORD    4096u

/* Заголовок разбираемого пакета. offset/length приходят из недоверенных
 * данных (C27) и до проверки в flow_decode_packet() смысла не имеют. */
typedef struct {
    uint32_t offset;   /* смещение полезной нагрузки от начала пакета */
    uint32_t length;   /* длина полезной нагрузки */
    uint32_t proto;
    uint32_t flags;
} flow_hdr_t;

typedef struct {
    flow_hdr_t     hdr;
    const uint8_t *payload;   /* указывает внутрь входного буфера */
    uint32_t       pay_len;
} flow_pkt_t;

typedef struct {
    uint32_t ts_sec;
    uint32_t ts_usec;
    uint32_t incl_len;
    uint32_t orig_len;
} flow_pcap_hdr_t;

typedef struct {
    flow_pcap_hdr_t hdr;
    const uint8_t  *data;
    size_t          len;
} flow_pcap_rec_t;

typedef struct {
    const char *data;
    size_t      len;
} flow_audit_rec_t;

/* 0 — успех. Отказы: -EMSGSIZE (предел), -EINVAL (короче заголовка),
 * -ERANGE (offset+length выходит за пакет либо переполняет). Выходная
 * структура при отказе не заполняется. */
int flow_decode_packet(const uint8_t *pkt, size_t pkt_len, flow_pkt_t *out);
int flow_decode_pcap_record(const uint8_t *rec, size_t rec_len, flow_pcap_rec_t *out);
int flow_decode_audit_record(const char *buf, size_t len, flow_audit_rec_t *out);
include/platx/flow_hades_enrich.h
/* platx/flow_hades_enrich.h — INT-126: Flow metadata enrichment for Hades events. */

#define PLAT_FLOW_HADES_IP_MAX  46
typedef struct {
    uint64_t flow_id;
    char     src_ip[PLAT_FLOW_HADES_IP_MAX];
    uint16_t src_port;
    char     dst_ip[PLAT_FLOW_HADES_IP_MAX];
    uint16_t dst_port;
    uint8_t  proto;
    char     event_kind[PLAT_HADES_EVENT_KIND_MAX];
    uint64_t bytes_in;
    uint64_t bytes_out;
} plat_flow_hades_enrich_t;
/* Enrich Hades event with flow metadata. 0 if found, -1 if not linked/found. */
int plat_flow_hades_enrich(const plat_hades_event_t *ev,
                            plat_flow_hades_enrich_t *out);
include/platx/flow_mbus.h
/* platx/flow_mbus.h — Flow summaries to MBus/Audit (INT-106). */
#define PLAT_FLOW_TOPIC    "flow.summary"
#define PLAT_FLOW_REASON   128

typedef struct {
    uint64_t flow_id;
    uint32_t proto;
    uint32_t src_ip;
    uint32_t dst_ip;
    uint16_t src_port;
    uint16_t dst_port;
    uint64_t bytes_in;
    uint64_t bytes_out;
    uint64_t pkts_in;
    uint64_t pkts_out;
    uint64_t duration_ms;
} plat_flow_summary_t;

/* Publish a flow summary to MBus and audit.  0/-1. */
int plat_flow_mbus_publish(const plat_flow_summary_t *summary);

/* Drain pending flow summaries and publish all.  Returns count/-1. */
int plat_flow_mbus_drain(void);
15

forensic

Сбор, связывание и проверка материалов расследования
src/forensic/Наблюдение и исследование8 файлов6 API headers

Forensic оформляет наблюдаемые артефакты в пригодную для повторной проверки цепочку. У материала есть происхождение, время, связь с объектом и условия сбора. Ограниченный сбор не задерживает локализацию инцидента; пробелы и потери сохраняют своё значение в итоговом пакете.

Граница ответственности

  • Forensic оформляет наблюдаемые артефакты в пригодную для повторной проверки цепочку. У материала есть происхождение, время, связь с объектом и условия сбора. Ограниченный сбор не задерживает локализацию инцидента; пробелы и потери сохраняют своё значение в итоговом пакете.

Устройство подсистемы

  • адаптер CLI домена FORENSIC к консоли платформы. Логики нет: она в forensic_cli.c и печатает в FILE*; вывод собирается в memstream и отдаётся консоли одним куском. Регистрация — только из register_cmds дескриптора модуля.
  • CLI домена FORENSIC. Логика и печать в FILE*. ЧТО ЭТОТ CLI ПОКАЗЫВАЕТ. Цепочка без её разрывов и пакет без его дефектов — не улика, а утверждение. Поэтому `chain verify` печатает seq-диапазон, число GAP-записей и непроверенных решений рядом с rc; `verify `
  • автономный CLI домена FORENSIC (домен вне поставки). Состояние живёт в памяти процесса — сценарий из нескольких команд идёт через `-f `; `verify` и `why` работают по каталогу пакета и состояния не требуют.
  • дерево Меркла по RFC 6962/9162: корень, доказательства включения и согласованности, со-подписи, откат. Верификаторы работают только с корнем и путём — журнал им не нужен. Это и есть содержание E3-ANCH-02: третья сторона проверяет факт, не получая доступа к тому, кто его выпустил.

Управление и диагностика

Корневые команды: forensic. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / forensic →
Состав подсистемы / 8 файлов
Файл / компонентНазначение и граница
src/forensic/cmd_forensic.cадаптер CLI домена FORENSIC к консоли платформы. Логики нет: она в forensic_cli.c и печатает в FILE*; вывод собирается в memstream и отдаётся консоли одним куском. Регистрация — только из register_cmds дескриптора модуля.
src/forensic/forensic_cli.cCLI домена FORENSIC. Логика и печать в FILE*. ЧТО ЭТОТ CLI ПОКАЗЫВАЕТ. Цепочка без её разрывов и пакет без его дефектов — не улика, а утверждение. Поэтому `chain verify` печатает seq-диапазон, число GAP-записей и непроверенных решений рядом с rc; `verify `
src/forensic/forensicctl_main.cавтономный CLI домена FORENSIC (домен вне поставки). Состояние живёт в памяти процесса — сценарий из нескольких команд идёт через `-f `; `verify` и `why` работают по каталогу пакета и состояния не требуют.
src/forensic/fx_anchor.cдерево Меркла по RFC 6962/9162: корень, доказательства включения и согласованности, со-подписи, откат. Верификаторы работают только с корнем и путём — журнал им не нужен. Это и есть содержание E3-ANCH-02: третья сторона проверяет факт, не получая доступа к тому, кто его выпустил.
src/forensic/fx_chain.cцепочка доказательств schema v3: live-окно, источники, checkpoint перед вытеснением, GAP после, идемпотентный re-emit. Почему CHECKPOINT пишется ДО вытеснения. Запись, которую вытеснили без подписанного digest'а головы, нельзя ни доказать, ни опровергнуть: её
src/forensic/fx_custody.cистория владения объектом доказательства над forensic_facade. Байты — в facade. Здесь никакого копирования байтов в записи цепочки: событие custody несёт handle (kind, ref, digest) и параметры события, поэтому fx_bytes_into_observations() остаётся нулём и после экспорта.
src/forensic/fx_package.cманифест, digest, верификатор package. Верификатор — самая важная функция домена, потому что это единственный код, которому суд, аудитор или второй узел могут верить, не веря узлу, выпустившему бандл. Поэтому он: не читает ничего, кроме переданных байтов;
src/forensic/fx_why.cдетерминированный WHY, timeline с частичным порядком, рецепты сбора по волатильности. WHY печатает только целые поля и имена из закрытых списков: ни времени прогона, ни адресов, ни текста модели. Один package → одни байты на любом хосте; golden-тест держит SHA-256 вывода (EXF-52).
Контракты API / 6 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/forensic_anchor.h
/* platx/forensic_anchor.h — внешний якорь: прозрачный журнал с деревом Меркла.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §6.6 (EXF-F-17…20), E3-ANCH-01…06.
 *
 * Модель — RFC 6962/9162: лист = SHA-256(0x00 || data), узел =
 * SHA-256(0x01 || left || right), корень дерева размера n определён
 * однозначно. Журнал выдаёт доказательство включения (путь от листа к
 * корню) и доказательство согласованности (старый корень — префикс нового).
 * Проверяющий с корнем и доказательством устанавливает факт БЕЗ доступа к
 * журналу (EXF-F-17).
 *
 * Со-подписанты: корень подписывают независимые стороны. Со-подписант с
 * тем же node_id, что выпускающий, независимым не считается; якорь
 * состоялся, только если независимых подписей ≥ порога (EXF-F-18).
 *
 * Откат: наблюдение корня меньшего размера — ANCHOR_ROLLBACK, отозванные
 * утверждения не восстанавливаются (EXF-F-19).
 */

#define FX_ANCHOR_LEAVES_MAX  1024u
#define FX_ANCHOR_PATH_MAX    32u
#define FX_ANCHOR_COSIGN_MAX  8u

typedef struct fx_anchor_entry {
    uint32_t type;                /* fx_record_type / package / release      */
    uint32_t policy_version;
    uint64_t subject_id;
    uint64_t issued_ns;
    uint64_t issuer_node;
    uint8_t  content_digest[32];
} fx_anchor_entry_t;

typedef struct fx_anchor_log {
    uint32_t n_leaves;
    uint32_t _pad;
    uint64_t issuer_node;
    uint64_t refused_full, rollbacks_seen;
    uint8_t  leaf[FX_ANCHOR_LEAVES_MAX][32];
    fx_anchor_entry_t entry[FX_ANCHOR_LEAVES_MAX];
} fx_anchor_log_t;

typedef struct fx_inclusion_proof {
    uint32_t leaf_index, tree_size, n_path, _pad;
    uint8_t  leaf_hash[32];
    uint8_t  path[FX_ANCHOR_PATH_MAX][32];
} fx_inclusion_proof_t;

typedef struct fx_consistency_proof {
    uint32_t old_size, new_size, n_path, _pad;
    uint8_t  path[FX_ANCHOR_PATH_MAX][32];
} fx_consistency_proof_t;

typedef int (*fx_cosign_fn)(void *ctx, const uint8_t *data, size_t len, uint8_t sig[64]);
typedef int (*fx_cosign_verify_fn)(void *ctx, uint32_t key_id, const uint8_t *data, size_t len,
                                   const uint8_t sig[64]);

typedef struct fx_cosignature {
    uint32_t key_id;
    uint32_t _pad;
    uint64_t node_id;             /* где живёт со-подписант                  */
    uint8_t  sig[64];
} fx_cosignature_t;

typedef struct fx_anchor_root {
    uint32_t tree_size;
    uint32_t n_cosign;
    uint8_t  root[32];
    uint64_t issuer_node;
    fx_cosignature_t cosign[FX_ANCHOR_COSIGN_MAX];
} fx_anchor_root_t;

int  fx_anchor_init(fx_anchor_log_t *l, uint64_t issuer_node);
int  fx_anchor_append(fx_anchor_log_t *l, const fx_anchor_entry_t *e, uint32_t *index_out);
int  fx_anchor_root(const fx_anchor_log_t *l, uint32_t tree_size, uint8_t out[32]);
int  fx_anchor_inclusion(const fx_anchor_log_t *l, uint32_t leaf_index, uint32_t tree_size,
                         fx_inclusion_proof_t *out);
int  fx_anchor_consistency(const fx_anchor_log_t *l, uint32_t old_size, uint32_t new_size,
                           fx_consistency_proof_t *out);
/* Проверка без журнала: только корень и доказательство. */
int  fx_anchor_verify_inclusion(const fx_inclusion_proof_t *p, const uint8_t root[32]);
int  fx_anchor_verify_consistency(const fx_consistency_proof_t *p, const uint8_t old_root[32],
                                  const uint8_t new_root[32]);
/* Канонический лист из записи; проверяющий пересчитывает его сам. */
int  fx_anchor_leaf_hash(const fx_anchor_entry_t *e, uint8_t out[32]);
/* Со-подпись корня: подписываются root||tree_size||issuer_node. */
int  fx_anchor_cosign(fx_anchor_root_t *r, uint32_t key_id, uint64_t node_id,
                      fx_cosign_fn sign, void *ctx);
/* Независимые подписи (node_id ≠ issuer, ключи уникальны, подпись верна) ≥ threshold. */
int  fx_anchor_check_cosign(const fx_anchor_root_t *r, uint32_t threshold,
                            fx_cosign_verify_fn verify, void *ctx, uint32_t *independent_out);
/* Наблюдение нового корня: размер меньше прежнего — откат (EDR_R_STATE),
 * согласованность не доказана — TAMPER. */
int  fx_anchor_observe(fx_anchor_log_t *l, const fx_anchor_root_t *prev, const fx_anchor_root_t *next,
                       const fx_consistency_proof_t *proof);
include/platx/forensic_chain.h
/* platx/forensic_chain.h — цепочка доказательств FORENSIC, schema v3.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §6.2 (EXF-F-01…05), правила R2, R5, R7.
 *
 * Что это. Один владелец плоскости доказательств над уже существующими
 * источниками: SP forensic chain и MIRAGE ledger остаются владельцами своих
 * записей и подключаются как ИСТОЧНИКИ — их записи входят сюда digest'ом
 * с source_chain_id, а не переписываются (EXF-F-01).
 *
 * Что здесь нельзя сделать молча:
 *  - потерять запись при wrap: перед вытеснением пишется CHECKPOINT, после —
 *    GAP с диапазоном sequence; счётчик evicted виден снаружи;
 *  - продублировать логическую запись при отказе sink: повтор идёт по
 *    record_id из очереди re-emit, а не новым append (EXF-F-05);
 *  - выдать CHECKPOINT без подписи за подписанный: нет signer — флаг
 *    FX_F_UNSIGNED, никогда VERIFIED (контракт platx.signer).
 *
 * Здесь нет I/O, malloc и часов; время и bytes приходят от вызывающего.
 * Wire записи — big-endian, фиксированный заголовок 192 байта + payload.
 */

#define FX_SCHEMA_VERSION   3u
#define FX_HDR_WIRE_BYTES   192u
#define FX_PAYLOAD_MAX      512u    /* больше — не запись, а объект под custody */
#define FX_CHAIN_LIVE       1024u   /* live-окно; профиль                    */
#define FX_SOURCES_MAX      8u
#define FX_REEMIT_MAX       64u

typedef enum fx_record_type {
    FX_REC_NONE = 0,
    FX_REC_CLAIM = 1, FX_REC_FINDING, FX_REC_HYPOTHESIS_REVISION,
    FX_REC_INCIDENT_REVISION, FX_REC_DECISION, FX_REC_PLAN_ADMITTED,
    FX_REC_PRE_ACTION_CHECKPOINT, FX_REC_EFFECT_RECEIPT, FX_REC_VERIFY_RESULT,
    FX_REC_ARTIFACT, FX_REC_GAP, FX_REC_CHECKPOINT, FX_REC_AI_HYPOTHESIS,
    FX_REC_SYNTHETIC_MARK, FX_REC_RETENTION_EVENT, FX_REC_CUSTODY_EVENT,
    FX_REC_EXTERNAL,          /* запись источника (SP chain / MIRAGE ledger) */
    FX_REC_ANCHOR,            /* регистрация в журнале Меркла               */
    FX_REC_MAX
} fx_record_type_t;

#define FX_F_EVIDENCE_BYPASSED (1u<<0)
#define FX_F_TIME_UNTRUSTED    (1u<<1)
#define FX_F_UNSIGNED          (1u<<2)
#define FX_F_SIGNED            (1u<<3)
#define FX_F_SYNTHETIC         (1u<<4)
#define FX_F_AUTO              (1u<<5)   /* CHECKPOINT/GAP, выпущенные цепочкой */
#define FX_F_REEMITTED         (1u<<6)

typedef struct fx_record_hdr {
    uint32_t schema_version;
    uint32_t type;
    uint8_t  record_id[16];
    uint8_t  causal_parent_id[16];
    uint8_t  prev_record_digest[32];
    uint8_t  payload_digest[32];
    uint64_t sequence;
    uint64_t monotonic_ns, wall_ns_claim;
    uint8_t  corr_id[16];
    uint32_t observation_level;
    uint32_t source_chain_id;     /* 0 — собственная запись                   */
    uint32_t payload_len;
    uint32_t flags;
    uint8_t  reserved[32];
} fx_record_hdr_t;

typedef struct fx_entry {
    int      in_use;
    int      unemitted;           /* sink отказал; ждёт re-emit               */
    fx_record_hdr_t hdr;
    uint8_t  entry_digest[32];    /* SHA-256(hdr wire || payload)             */
    uint8_t  payload[FX_PAYLOAD_MAX];
} fx_entry_t;

typedef struct fx_source {
    int      in_use;
    uint32_t source_chain_id;
    char     name[32];
    uint64_t last_seq;
    uint8_t  last_digest[32];
} fx_source_t;

/* Sink: получает запись ПОСЛЕ commit. Возврат ≠ 0 — потеря sink: запись
 * остаётся в кольце, помечается unemitted, повтор — fx_chain_reemit(). */
typedef int (*fx_sink_fn)(const fx_record_hdr_t *hdr, const uint8_t *payload, void *ud);
/* Signer: контракт platx.signer. Возврат 0 и подпись, либо -1 (нет ключа). */
typedef int (*fx_sign_fn)(void *ctx, const uint8_t *data, size_t len, uint8_t sig[64]);

typedef struct fx_chain {
    uint32_t schema_version;
    uint32_t n_live, head;
    uint64_t sequence;            /* следующий номер                          */
    uint8_t  boot_id[16];
    uint64_t host_id;
    uint8_t  last_digest[32];
    /* счётчики (R2) */
    uint64_t appended, evicted, gaps, checkpoints, sink_failures, reemitted,
             refused_full, refused_args, external_records;
    uint32_t n_sources, n_unemitted;
    fx_source_t sources[FX_SOURCES_MAX];
    fx_sink_fn sink; void *sink_ud;
    fx_sign_fn sign; void *sign_ctx;
    uint32_t auto_checkpoint_pending;
    uint32_t _pad;
    fx_entry_t ring[FX_CHAIN_LIVE];
} fx_chain_t;

typedef struct fx_append_req {
    uint32_t type;
    uint32_t observation_level;
    uint32_t source_chain_id;
    uint32_t flags;
    const uint8_t *causal_parent_id;   /* NULL — нет родителя                */
    const uint8_t *corr_id;            /* NULL — нули                        */
    const uint8_t *payload; uint32_t payload_len;
    uint64_t monotonic_ns, wall_ns;
} fx_append_req_t;

typedef struct fx_verify_report {
    uint64_t checked, first_seq, last_seq;
    uint32_t gaps_seen, unverified_decisions, unemitted, auto_records;
    uint64_t failing_seq;            /* 0 — нет                              */
    int      rc;                     /* edr_reason_t: OK / TAMPER / GAP      */
} fx_verify_report_t;

int  fx_chain_init(fx_chain_t *c, const uint8_t boot_id[16], uint64_t host_id,
                   fx_sink_fn sink, void *sink_ud, fx_sign_fn sign, void *sign_ctx);
int  fx_chain_source_register(fx_chain_t *c, uint32_t source_chain_id, const char *name);
/* Запись источника: только digest и sequence источника; тело остаётся у
 * владельца (EXF-F-01). Разрыв sequence источника → GAP с его id. */
int  fx_chain_external(fx_chain_t *c, uint32_t source_chain_id, uint64_t src_seq,
                       const uint8_t src_digest[32], uint64_t monotonic_ns, uint64_t wall_ns,
                       uint8_t record_id_out[16]);
int  fx_chain_append(fx_chain_t *c, const fx_append_req_t *req, uint8_t record_id_out[16]);
int  fx_chain_checkpoint(fx_chain_t *c, uint64_t monotonic_ns, uint64_t wall_ns,
                         uint8_t record_id_out[16]);
/* Повтор невыпущенных записей по record_id; новых записей не создаёт. */
int  fx_chain_reemit(fx_chain_t *c, uint32_t *reemitted_out);
int  fx_chain_verify(const fx_chain_t *c, fx_verify_report_t *out);
const fx_entry_t *fx_chain_get(const fx_chain_t *c, uint64_t sequence);
const fx_entry_t *fx_chain_find(const fx_chain_t *c, const uint8_t record_id[16]);
/* wire заголовка; digest записи = SHA-256(hdr wire || payload). */
int  fx_hdr_wire(const fx_record_hdr_t *h, uint8_t out[FX_HDR_WIRE_BYTES]);
int  fx_hdr_unwire(const uint8_t *in, fx_record_hdr_t *out);   /* ровно FX_HDR_WIRE_BYTES */
int  fx_entry_digest(const fx_record_hdr_t *h, const uint8_t *payload, uint8_t out[32]);
const char *fx_record_type_name(uint32_t t);
include/platx/forensic_custody.h
/* platx/forensic_custody.h — custody: владение объектом доказательства.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §6.3, §6.9 (EXF-F-06…09, EXF-F-25…26).
 *
 * Байты лежат в forensic_facade (fx_seal/fx_open/fx_verify) — здесь их нет.
 * Здесь метаданные и ИСТОРИЯ владения: кто запечатал, с какого уровня
 * наблюдал, какой privacy-класс, какой taint, кто открывал и зачем, что
 * экспортировано, что под hold, что и по чьей authority уничтожено. Каждое
 * событие — запись цепочки класса CUSTODY_EVENT/RETENTION_EVENT.
 *
 * Правила, проверяемые тестом:
 *  - seal сверх квоты — отказ с кодом, не вытеснение улики (EXF-F-08);
 *  - open с несовпавшим digest — отказ + TAMPER событие + finding в EDR
 *    (EXF-F-07); объект не выдаётся;
 *  - purge без authority или под hold — отказ; purge — событие с receipt
 *    (EXF-F-25); «удалилось само» — дефект;
 *  - SECRET открывается только под lease (EXF-F-09/26).
 *
 * Ограничение facade (честно): у forensic_facade нет операции удаления
 * байтов — purge помечает объект и закрывает доступ; физическое
 * освобождение слота — DEFER до расширения facade (EXF-D-LIM-01).
 */

#define FX_CUSTODY_MAX     64u     /* = FX_HANDLES_MAX facade                 */

/* Классы объектов сверх forensic_ref.h (facade принимает любой kind ≠ 0). */
#define FX_KIND_FILE          6u
#define FX_KIND_REGISTRY      7u
#define FX_KIND_LOG           8u
#define FX_KIND_MEMORY_RANGE  9u
#define FX_KIND_SNAPSHOT     10u
#define FX_KIND_CUSTODY_MAX  11u

typedef enum fx_privacy { FX_PRIV_PUBLIC = 0, FX_PRIV_INTERNAL = 1, FX_PRIV_PII = 2,
    FX_PRIV_SECRET = 3, FX_PRIV_MAX } fx_privacy_t;

typedef enum fx_custody_event { FX_CUST_NONE = 0, FX_CUST_SEALED = 1, FX_CUST_OPENED,
    FX_CUST_VERIFIED, FX_CUST_EXPORTED, FX_CUST_HELD, FX_CUST_RELEASED,
    FX_CUST_PURGED, FX_CUST_TAMPER_DETECTED, FX_CUST_OPEN_REFUSED,
    FX_CUST_MAX } fx_custody_event_t;

/* Причина открытия — закрытый список; свободного текста нет. */
typedef enum fx_open_why { FX_WHY_NONE = 0, FX_WHY_INVESTIGATION = 1, FX_WHY_VERIFY,
    FX_WHY_EXPORT, FX_WHY_SANDBOX_SUBMIT, FX_WHY_OPERATOR, FX_WHY_MAX } fx_open_why_t;

#define FX_TAINT_ATTACKER_CONTROLLED (1u<<0)   /* C27 */
#define FX_TAINT_SYNTHETIC           (1u<<1)   /* MIRAGE */
#define FX_TAINT_REDACTED            (1u<<2)

typedef struct fx_object_meta {
    int      in_use;
    int      purged;
    witness_evidence_ref_t ref;
    uint64_t size;
    uint32_t privacy_class;
    uint32_t taint;
    plat_owner_t sealed_by;
    uint32_t observation_level;
    uint32_t hold_count;
    uint64_t incident_id;
    uint8_t  seal_record_id[16];   /* запись цепочки SEALED                  */
    uint32_t opens, exports, verifies;
    uint32_t _pad;
} fx_object_meta_t;

typedef struct fx_quota {
    uint64_t max_bytes;
    uint32_t max_handles;
    uint32_t _pad;
} fx_quota_t;

/* EDR получает findings о целостности улик (EXF-F-23). */
typedef int (*fx_finding_fn)(uint32_t kind, const witness_evidence_ref_t *ref, void *ud);
#define FX_FINDING_EVIDENCE_TAMPER   1u
#define FX_FINDING_CUSTODY_BROKEN    2u
#define FX_FINDING_RETENTION_VIOL    3u

typedef struct fx_custody {
    fx_chain_t *chain;
    fx_quota_t  quota;
    uint64_t    used_bytes;
    uint32_t    n_objects;
    uint32_t    _pad;
    uint64_t    refused_quota, tamper_detected, refused_privacy, refused_retention,
                purged, held;
    fx_finding_fn finding; void *finding_ud;
    fx_object_meta_t objects[FX_CUSTODY_MAX];
} fx_custody_t;

typedef struct fx_seal_req {
    uint32_t kind;                 /* FX_KIND_*                             */
    uint32_t privacy_class;
    uint32_t taint;
    uint32_t observation_level;
    plat_owner_t sealed_by;        /* generation 0 — отказ                  */
    uint64_t incident_id;
    const uint8_t *blob; size_t len;
    uint64_t monotonic_ns, wall_ns;
} fx_seal_req_t;

int  fx_custody_init(fx_custody_t *c, fx_chain_t *chain, const fx_quota_t *q,
                     fx_finding_fn finding, void *ud);
int  fx_custody_seal(fx_custody_t *c, const fx_seal_req_t *req, witness_evidence_ref_t *out);
/* Открыть под записью: why из закрытого списка; lease_id обязателен для
 * privacy ≥ PII; при несовпадении digest — TAMPER, объект не выдаётся. */
int  fx_custody_open(fx_custody_t *c, const witness_evidence_ref_t *ref, uint32_t why,
                     uint64_t lease_id, uint64_t monotonic_ns, uint64_t wall_ns,
                     const uint8_t **blob_out, size_t *len_out);
int  fx_custody_verify(fx_custody_t *c, const witness_evidence_ref_t *ref,
                       uint64_t monotonic_ns, uint64_t wall_ns);
int  fx_custody_mark_exported(fx_custody_t *c, const witness_evidence_ref_t *ref,
                              uint64_t authority_id, uint64_t package_id,
                              uint64_t monotonic_ns, uint64_t wall_ns);
int  fx_custody_hold(fx_custody_t *c, const witness_evidence_ref_t *ref, uint64_t authority_id,
                     uint64_t monotonic_ns, uint64_t wall_ns);
int  fx_custody_release(fx_custody_t *c, const witness_evidence_ref_t *ref, uint64_t authority_id,
                        uint64_t monotonic_ns, uint64_t wall_ns);
/* purge: authority обязателен; под hold — отказ; событие RETENTION_EVENT. */
int  fx_custody_purge(fx_custody_t *c, const witness_evidence_ref_t *ref, uint64_t authority_id,
                      uint32_t reason, uint64_t monotonic_ns, uint64_t wall_ns);
const fx_object_meta_t *fx_custody_meta(const fx_custody_t *c, const witness_evidence_ref_t *ref);
const char *fx_custody_event_name(uint32_t e);
const char *fx_privacy_name(uint32_t p);
include/platx/forensic_host.h
/* platx/forensic_host.h — CLI домена FORENSIC (host-часть).
 *
 * Ядро FORENSIC (src/forensic/fx_*.c) не делает I/O и не читает часы;
 * файлы, каталоги пакетов и печать живут здесь и в
 * src/forensic/{forensic_cli,cmd_forensic,forensicctl_main}.c. Тот же путь
 * используется офлайн-верификатором tools/forensic/verify: у `forensic
 * verify` нет второй реализации.
 *
 * Инварианты §10.2: usage с глагола; --json тот же путь; чтение не пишет;
 * intent без authority — REFUSED; байты улик не печатаются — только
 * handles и digest'ы; байты уходят только в `export` с authority.
 *
 * Пакет на диске (EXF-F-20): каталог с MANIFEST.bin и файлами по путям
 * манифеста. Файл вне манифеста — дефект EXTRA_SECTION, не «лишнее».
 */
int  forensic_cli(int argc, char **argv, FILE *out, FILE *err);
int  forensic_cli_script(const char *path, FILE *out, FILE *err);
void forensic_register_console(void);
void forensic_cli_reset(void);
include/platx/forensic_package.h
/* platx/forensic_package.h — evidence package: манифест, верификатор, WHY,
 * timeline, рецепты сбора.
 *
 * ТЗ TZ_PLATX_EDR_XDR_FORENSIC_V2 §6.4, §6.5, §6.7 (EXF-F-10…16, EXF-F-21…22).
 *
 * Package здесь — структура в памяти: секции ссылаются на буферы, которые
 * даёт хост (forensicctl читает и пишет файлы; ядро не делает I/O). Порядок
 * проверок верификатора фиксирован (Приложение B ТЗ), первый провал не
 * прерывает — печатаются все дефекты, rc=1:
 *   MANIFEST полнота и лишние файлы → SHA каждой секции → ANCHOR inclusion →
 *   CHAIN (prev digest, sequence) → CUSTODY ↔ ARTIFACTS → VERDICT ↔ HYPOTHESES
 *   ↔ gate → REDACTIONS ↔ MANIFEST → INCIDENT replay.
 * Верификатор не принимает собственную печать бандла как основание: ANCHOR
 * проверяется против ВНЕШНЕГО корня (.anchor вне бандла), если он дан.
 */

#define FX_PKG_SECTIONS_MAX 32u
#define FX_PKG_PATH_MAX     64u
#define FX_PKG_MANIFEST_ENTRY (FX_PKG_PATH_MAX + 4u + 4u + 8u + 32u)
#define FX_PKG_MANIFEST_MAX (4u + 8u + FX_PKG_SECTIONS_MAX * FX_PKG_MANIFEST_ENTRY)

typedef enum fx_section_kind {
    FX_SEC_NONE = 0, FX_SEC_ENVIRONMENT = 1, FX_SEC_CHAIN, FX_SEC_CUSTODY, FX_SEC_TIMELINE,
    FX_SEC_COVERAGE, FX_SEC_INCIDENT, FX_SEC_HYPOTHESES, FX_SEC_VERDICT, FX_SEC_PLAN,
    FX_SEC_RECEIPT, FX_SEC_ARTIFACT, FX_SEC_REDACTIONS, FX_SEC_LIMITS, FX_SEC_AI,
    FX_SEC_ANCHOR, FX_SEC_MAX
} fx_section_kind_t;

typedef struct fx_section {
    char     path[FX_PKG_PATH_MAX];
    uint32_t kind, privacy_class;
    uint64_t len;
    const uint8_t *data;          /* буфер хоста; ядро не владеет            */
    uint8_t  sha256[32];
} fx_section_t;

typedef struct fx_package {
    uint64_t package_id;
    uint32_t n, _pad;
    fx_section_t s[FX_PKG_SECTIONS_MAX];
} fx_package_t;

/* Дефекты верификатора — маска; каждый бит назван. */
#define FX_VD_MANIFEST_PARSE     (1u<<0)
#define FX_VD_MISSING_SECTION    (1u<<1)
#define FX_VD_EXTRA_SECTION      (1u<<2)   /* файл вне манифеста — отказ      */
#define FX_VD_SHA_MISMATCH       (1u<<3)
#define FX_VD_ANCHOR             (1u<<4)
#define FX_VD_CHAIN              (1u<<5)
#define FX_VD_CUSTODY_ARTIFACT   (1u<<6)
#define FX_VD_VERDICT_GATE       (1u<<7)   /* вердикт не следует из гипотез   */
#define FX_VD_REDACTION          (1u<<8)
#define FX_VD_INCIDENT_REPLAY    (1u<<9)
#define FX_VD_AI_AS_EVIDENCE     (1u<<10)  /* AI-запись в секции доказательств */
#define FX_VD_NO_ANCHOR_INPUT    (1u<<11)  /* внешний корень не дан — не проверено */

typedef struct fx_verify_result {
    uint32_t defects;             /* FX_VD_*                                 */
    uint32_t checks_run;
    char     first_path[FX_PKG_PATH_MAX];
    uint32_t chain_records, chain_gaps, incident_revisions, artifacts;
    int      rc;                  /* 0 — соответствует; 1 — дефекты          */
} fx_verify_result_t;

int  fx_pkg_init(fx_package_t *p, uint64_t package_id);
int  fx_pkg_add(fx_package_t *p, const char *path, uint32_t kind, uint32_t privacy_class,
                const uint8_t *data, uint64_t len);
/* Манифест: записи в порядке путей (детерминированно), BE. */
int  fx_pkg_manifest_wire(const fx_package_t *p, uint8_t *out, size_t cap);
int  fx_pkg_manifest_parse(const uint8_t *in, size_t len, fx_package_t *out);
/* digest package = SHA-256(manifest wire); лист якоря — от него. */
int  fx_pkg_digest(const fx_package_t *p, uint8_t out[32]);
int  fx_pkg_anchor_entry(const fx_package_t *p, uint64_t issued_ns, uint32_t policy_version,
                         fx_anchor_entry_t *out);
/* Верификатор офлайн. manifest — как прочитан; external_root/proof —
 * из .anchor ВНЕ бандла (NULL → FX_VD_NO_ANCHOR_INPUT, не PASS). */
int  fx_pkg_verify(const fx_package_t *view, const uint8_t *manifest, size_t manifest_len,
                   const uint8_t *external_root, const fx_inclusion_proof_t *proof,
                   fx_verify_result_t *out);
const fx_section_t *fx_pkg_find(const fx_package_t *p, uint32_t kind, uint32_t nth);
const char *fx_section_kind_name(uint32_t k);
const char *fx_verify_defect_name(uint32_t bit);

/* ── WHY: детерминированное объяснение из package (EXF-F-22) ─────────── */
int  fx_why_render(const fx_package_t *view, char *out, size_t cap);

/* ── Timeline: частичный порядок (EXF-F-12) ───────────────────────────── */
typedef struct fx_timeline_out {
    uint32_t n;                   /* записей в порядке                       */
    uint32_t two_orders;          /* 1 — откат wall-часов, порядки расходятся */
    uint32_t truncated_by_cutoff; /* сколько отрезано AS_KNOWN_THEN          */
    uint32_t _pad;
} fx_timeline_out_t;
/* hdrs — заголовки записей; order_out — индексы в причинно-монотонном
 * порядке; wall_order_out — порядок по wall-claim (заполняется только при
 * two_orders). cutoff_mono — 0 = без отсечки. */
int  fx_timeline_build(const fx_record_hdr_t *hdrs, uint32_t n, uint64_t cutoff_mono,
                       uint32_t *order_out, uint32_t *wall_order_out, size_t cap,
                       fx_timeline_out_t *out);

/* ── Рецепты сбора по волатильности (EXF-F-10/11) ─────────────────────── */
typedef enum fx_collect_op {
    FX_COP_NONE = 0,
    FX_COP_PROCESS_MEMORY_RANGE = 1, /* волатильность 1 (после регистров)   */
    FX_COP_SOCKET_TABLE,             /* 2                                    */
    FX_COP_PROCESS_ANCESTRY,         /* 3                                    */
    FX_COP_PROCESS_MAPS,             /* 3                                    */
    FX_COP_PROCESS_FDS,              /* 3                                    */
    FX_COP_MODULE_LIST,              /* 3                                    */
    FX_COP_BPF_PROGS,                /* 3                                    */
    FX_COP_FILE_IDENTITY,            /* 4                                    */
    FX_COP_FILE_CONTENT,             /* 4                                    */
    FX_COP_REGISTRY_KEY,             /* 4 (Windows)                          */
    FX_COP_LOG_RANGE,                /* 5                                    */
    FX_COP_MEASURE,                  /* 6 (IMA/TPM)                          */
    FX_COP_PAGE_HV,                  /* 1 (HV page)                          */
    FX_COP_MAX
} fx_collect_op_t;

#define FX_COLLECT_STEPS_MAX 16u
typedef struct fx_collect_step { uint32_t op; uint32_t bounded_bytes; uint64_t target_id, evidence_id; } fx_collect_step_t;
typedef struct fx_collect_recipe {
    char     name[32];
    uint32_t n, _pad;
    fx_collect_step_t steps[FX_COLLECT_STEPS_MAX];
} fx_collect_recipe_t;

int  fx_collect_init(fx_collect_recipe_t *r, const char *name);
int  fx_collect_add(fx_collect_recipe_t *r, uint32_t op, uint32_t bounded_bytes,
                    uint64_t target_id, uint64_t evidence_id);
uint32_t fx_collect_volatility(uint32_t op);
/* Порядок по волатильности неубывающий, операции из списка, bounded_bytes ≠ 0. */
int  fx_collect_validate(const fx_collect_recipe_t *r);
/* В Plan IR: только COLLECT/OBSERVE READ_ONLY — admit проходит без authority. */
int  fx_collect_to_plan(const fx_collect_recipe_t *r, uint64_t mission_id, uint64_t seed,
                        platx_plan_ir_t *out);
const char *fx_collect_op_name(uint32_t op);
include/platx/forensic_ref.h
/* platx/forensic_ref.h — evidence handles, not evidence (D091).
 *
 * A witness that found something usually has bytes to show for it: a page
 * image, a module section, a packet. Those bytes must not travel through the
 * observation plane.
 *
 * Two reasons, and the second one is the one that matters:
 *   1. size — SENSE records are bounded and its ring is volatile;
 *   2. custody — evidence that has been copied into a ring has no chain of
 *      custody. Anyone who can read the ring has a copy, nobody can say which
 *      copy is the original, and a consumer that re-emits it becomes a
 *      custodian of material it cannot seal.
 *
 * So FORENSIC keeps the bytes and hands out a handle: an opaque reference plus
 * the digest of what it refers to. A claim carries the handle. Verifying the
 * claim means asking FORENSIC, which still holds the object and can prove the
 * digest still matches.
 */

#define FX_HANDLES_MAX   64u
#define FX_BLOB_MAX    4096u   /* per object, in this in-process facade */

/* Evidence classes. The class travels with the handle so a consumer can decide
 * whether it is allowed to ask for the object at all. */
#define FX_KIND_NONE     0u
#define FX_KIND_PAGE     1u
#define FX_KIND_MODULE   2u
#define FX_KIND_TASK     3u
#define FX_KIND_PACKET   4u
#define FX_KIND_MEASURE  5u

int  fx_facade_init(void);
void fx_facade_fini(void);

/* Seal `len` bytes under `kind`. Fills `out` with an opaque handle, the
 * SHA-256 of the object and the seal timestamp. The bytes stay here. */
int fx_seal(uint32_t kind, const uint8_t *blob, size_t len,
            witness_evidence_ref_t *out);

/* Custody read: hand back the object a handle refers to. Fails when the handle
 * is unknown or the stored object no longer matches its digest. */
int fx_open(const witness_evidence_ref_t *ref, const uint8_t **blob_out,
            size_t *len_out);

/* Does the stored object still match the digest in the reference.
 * Returns PROV_OK (0) when it matches, a negative PROV_ERR_* when it does not
 * or the handle is unknown — the same polarity as fx_seal/fx_open, so that
 * `if (fx_verify(&ref) != PROV_OK) reject();` is the correct use.  It is NOT a
 * boolean predicate; it returned one until A4-P03-111 (GAP-11). */
int fx_verify(const witness_evidence_ref_t *ref);

/* Bytes of evidence this facade has ever copied INTO an observation record.
 * It is always zero, and it is a counter rather than a comment so that a test
 * can hold the invariant rather than trust it. */
uint64_t fx_bytes_into_observations(void);

/* Deliberate corruption of a stored object — test-only, and named so that it
 * cannot be mistaken for part of the API. */
int fx_test_corrupt(const witness_evidence_ref_t *ref);
16

fsx

Файловое исследование, контроль и оркестрация экспериментов
src/fsx/Наблюдение и исследование6 файлов1 API headers

Файловое исследование, контроль и оркестрация экспериментов

Граница ответственности

  • Read-only evidence collection отделена от mutation/quarantine operations и имеет разные permissions.
  • FUSE/eBPF являются providers через contracts; FSX не включает их private implementation.

Устройство подсистемы

  • Session descriptor содержит target/context, profile, policy, quotas, state, correlation и evidence root.
  • Collector adapters нормализуют filesystem events; policy engine принимает fact и выдаёт decision, но mutation executor применяет отдельную authorized transaction.
  • Mount/quarantine/canary operations имеют prepare/commit/rollback и resource scope.

Поток работы

  • Create session → validate target/profile/permissions.
  • Start collectors → normalized events → policy/timeline.
  • Optional mutation transaction → audit/evidence.
  • Stop → flush report → detach/unmount → release scope.

Отказ и восстановление

  • Collector unavailable → session DEGRADED с coverage map, не fake healthy.
  • Partial mount/quarantine rollback and explicit NEEDS_OPERATOR если rollback невозможен.
  • Evidence storage full останавливает mutation раньше потери audit trail.
  • Target disappears → terminal fact, collectors detach идемпотентно.

Основные возможности

  • Session/profile/policy model with filesystem telemetry.
  • FUSE/eBPF/eventbus integrations and event filtering.
Архитектурные детали и инварианты

Назначение

Перехват VFS-событий через **fanotify** (FAN_CLASS_NOTIF): access, modify, create,

delete, moved, attrib, open, close. Разрешение путей через /proc/self/fd/<fd>.

Особенности

- fanotify_init(FAN_CLASS_NOTIF|FAN_NONBLOCK) — только наблюдение, без блокировки

- fanotify_mark(FAN_MARK_FILESYSTEM) — весь mount-point

- Статический буфер s_buf[4096] для одного события

Управление и диагностика

Корневые команды: fsx. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / fsx →
Состав подсистемы / 6 файлов
Файл / компонентНазначение и граница
src/fsx/cmd_fsx.cCLI for the FSX laboratory filesystem.
src/fsx/fsx.cFSX session manager, policy, telemetry, snapshot, artifacts.
src/fsx/fsx.hFSX (FUSE Sandbox eXtension) public API. Userspace filesystem view for laboratory isolation, artifact capture, telemetry and canary/deception. FSX is not a stealth/rootkit layer: mounts, processes and PlatX files stay visible to the administrator.
src/fsx/fsx_fuse.cVisible laboratory filesystem. Does not hide mounts, processes or PlatX.
src/fsx/fsx_internal.hprivate types for the FSX subsystem.
src/fsx/fsx_probe.cРеализация fsx / probe
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/fsx_events.h
/* platx/fsx_events.h — FSX events → Audit / MBus (INT-097).
 * Drains fsx_events() and fans out to audit + mbus topic.
 */
#define PLAT_FSX_TOPIC_MAX  64
#define PLAT_FSX_DRAIN_MAX  32   /* events per drain call */

typedef enum {
    PLAT_FSX_EV_OK     = 0,
    PLAT_FSX_EV_NOLINK = 1,
    PLAT_FSX_EV_ERROR  = 2,
} plat_fsx_ev_status_t;

/* Drain up to PLAT_FSX_DRAIN_MAX events from session, publish each to
 * mbus topic "fsx.event.<session_id>" and write to audit.
 * Returns number of events published, -1 on error. */
int plat_fsx_events_drain(const char *session_id);

/* Register session for periodic draining (audit + mbus).  0/-1. */
int plat_fsx_events_bind(const char *session_id);
17

fuse

Жизненный цикл файловых workers на FUSE
src/fuse/Исполнение и расширения4 файлов1 API headers

FUSE module управляет lifecycle внешних filesystem workers и mounts. Он должен гарантировать, что mount, process, IPC/key material и status представляют один generation-aware объект и корректно переживают partial start/worker crash.

Граница ответственности

  • Kernel mount и child process — resources instance; raw helper process не живёт вне child/resource accounting.
  • VFS/FSX потребляют mount capability/handle, а не pid/path globals.
  • Key reload — explicit operation с version, не сигнал в неизвестный процесс.

Устройство подсистемы

  • Mount instance хранит desired mountpoint/options, child handle, control IPC, key generation, state и cleanup strategy.
  • Prepare проверяет path, namespace, permissions и отсутствие conflicting mount. Start spawn-ит worker, ждёт READY и подтверждает kernel mount.
  • Stop блокирует admission, просит unmount, завершает worker и проверяет исчезновение mount; force policy отдельна.
  • Mount watcher превращает unexpected exit/unmount в recovery event.

Поток работы

  • CLI/FSX request → validate/prepare.
  • Child spawn/handshake → mount confirm → capability publish.
  • I/O идет kernel↔worker; control отдельно.
  • Stop/crash → revoke → unmount/kill → release.

Отказ и восстановление

  • Worker READY без mount confirm не считается RUNNING.
  • Mount существует, child умер: revoke и controlled unmount/recovery, не spawn поверх старого.
  • Unmount busy → DEGRADED/timeout policy с точной диагностикой.
  • Key reload failure сохраняет old generation либо fully rolls back.

Основные возможности

  • Starts/stops and tracks external FUSE process instances.
  • Mount inventory and key reload operations.
  • Core-side lifecycle helper exists for ownership integration.
Архитектурные детали и инварианты

Назначение

Мониторинг операций FUSE-файловых систем: lookup, open, read, write, unlink, mkdir,

rmdir, rename, mknod. Используется для обнаружения подозрительных overlay-FS и FUSE-руткитов.

Реализация

Открывает /dev/fuse в non-blocking режиме. platx_fuse_monitor_poll() читает

fuse_in_header из статического буфера s_buf[65536] и транслирует opcode в

platx_fuse_op_t. Timestamp: CLOCK_MONOTONIC.

Управление и диагностика

Корневые команды: fuse. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / fuse →
Состав подсистемы / 4 файлов
Файл / компонентНазначение и граница
src/fuse/cmd_fuse.cCLI для FUSE-менеджера. fuse mount [opts] fuse umount [--force] fuse reload-key [--key-hex ]
src/fuse/fuse_driver.cменеджер жизненного цикла FUSE-процесса. 1. Создаём анонимный pipe(key_pipe[2]). 2. fork() → дочерний процесс: a. закрывает write-конец (key_pipe[1]) b. exec "memfuse_like_driver --mountpoint --keyfd [--cipher ] ..." 3. Родитель: записывает 32-байтный ключ в key_pipe[1], закрывает его.
src/fuse/fuse_driver.hменеджер жизненного цикла FUSE-процесса. FUSE is VFS frontend, not XIO backend. Управляет внешним бинарником memfuse_like_driver: - Запуск процесса (fork + exec) - Передача ключа шифрования через анонимный pipe (не через env/args) - Мониторинг состояния монтирования через /proc/mounts
src/fuse/fuse_monitor.cРеализация fuse / monitor
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/fuse_lifecycle.h
/* platx/fuse_lifecycle.h — FUSE lifecycle with Core resources/Keyring (INT-096).
 * Thin capability gate → fuse_driver_mount, tracks resource slot.
 */
#define PLAT_FUSE_LC_CAP       "fuse.mount:v1"
#define PLAT_FUSE_LC_ID_MAX    128
#define PLAT_FUSE_LC_REASON    128

typedef enum {
    PLAT_FUSE_LC_OK     = 0,
    PLAT_FUSE_LC_NOLINK = 1,
    PLAT_FUSE_LC_DENIED = 2,
    PLAT_FUSE_LC_ERROR  = 3,
} plat_fuse_lc_status_t;

typedef struct {
    plat_fuse_lc_status_t status;
    char mount_id[PLAT_FUSE_LC_ID_MAX];
    int  state;   /* fuse_mnt_state_t cast */
    char reason[PLAT_FUSE_LC_REASON];
} plat_fuse_lc_result_t;

/* Gate via lease then fuse_driver_mount.  mountpoint required.
 * lease_id=0 skips gate.  Returns 0/-1; out.mount_id filled on success. */
int plat_fuse_lc_mount(uint64_t ctx_id, uint32_t ctx_gen,
                       uint64_t lease_id, const char *mountpoint,
                       uint32_t flags, plat_fuse_lc_result_t *out);

/* fuse_driver_umount wrapper with audit.  force=1 → SIGKILL. */
int plat_fuse_lc_umount(const char *mount_id, int force);
18

gadget

Каталог возможностей и выбор операции
src/gadget/Управление и контракты2 файлов1 API headers

Каталог возможностей и выбор операции

Граница ответственности

  • Source truth — descriptors/capreg/operation catalog. Gadget не копирует service rows.
  • Selection read-only; execute вызывает typed platform operation после отдельного authz.
  • Explain не раскрывает secrets/private provider state.

Устройство подсистемы

  • Operation schema описывает name, input/output version, required capability, state/profile/isolation constraints и risk class.
  • Explain tree хранит accepted/rejected candidates и причины.
  • Execution handle pin-ит selected provider generation и передаёт correlation.

Поток работы

  • Request schema/input → validate.
  • Resolve candidates → policy filter/rank.
  • Explain/approval → typed operation dispatch.
  • Result/audit + release lease.

Отказ и восстановление

  • Нет provider → explicit UNAVAILABLE с rejected reasons.
  • Provider revoked после selection → stale lease, operation retry только policy.
  • Ambiguous equal candidates → deterministic tie rule или require operator.
  • Catalog drift с handler блокирует registration.

Основные возможности

  • Catalogs operations independently of provider implementation.
  • Selects an eligible provider using capability/policy context.
  • Explain output makes rejection/selection visible to operator.

Управление и диагностика

Корневые команды: gadget. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / gadget →
Состав подсистемы / 2 файлов
Файл / компонентНазначение и граница
src/gadget/cmd_gadget.cexplain or request one capability. Selection lives in gadget.c. This file resolves the context, optionally issues the operator self-lease, and prints the decision. Grant/revoke stay on the lease command — a second issuer here would disagree. it cannot be minted. Explain never asks the lease table.
src/gadget/gadget.ccatalog, explain, one selected execute. Two gadgets, one capability. The catalog is a static table, not a registry: plat_capreg still owns published names. Explain probes; request picks one survivor and runs only that one. If nothing survives policy, the result is a named refuse, not a quieter gadget.
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/gadget.h
/* platx/gadget.h — one capability, allowed strategies, explicit select.
 *
 * Capability is the promise (process.snapshot:v1). A gadget is one legal
 * way to keep it. Selection is explainable and fail-closed: fallback never
 * raises risk or quality silently, and no gadget means refuse — not an
 * empty OK. This table is not plat_capreg; names stay where they are.
 *
 * Probe asks Surface. Execute is a different call.
 */

#define PLAT_GADGET_CAP_PROCESS_SNAPSHOT "process.snapshot:v1"
#define PLAT_GADGET_ID_PROCFS            "native.procfs"
#define PLAT_GADGET_ID_PIDFD             "native.pidfd"

#define PLAT_GADGET_NAME_MAX     64
#define PLAT_GADGET_REASON_MAX   160
#define PLAT_GADGET_DETAIL_MAX   256
#define PLAT_GADGET_MAX_CANDIDATES 4

/* SNAPSHOT < EXACT. min_quality=EXACT must not accept SNAPSHOT. */
typedef enum plat_gadget_quality {
    PLAT_GADGET_QUALITY_SNAPSHOT = 0,
    PLAT_GADGET_QUALITY_EXACT    = 1
} plat_gadget_quality_t;

typedef enum plat_gadget_risk {
    PLAT_GADGET_RISK_SAFE_QUERY = 0,
    PLAT_GADGET_RISK_OBSERVATION,
    PLAT_GADGET_RISK_CONTROLLED_EXECUTION,
    PLAT_GADGET_RISK_MUTATING,
    PLAT_GADGET_RISK_LAB_ONLY
} plat_gadget_risk_t;

typedef enum plat_gadget_status {
    PLAT_GADGET_OK = 0,
    PLAT_GADGET_ERR_ARGS,
    PLAT_GADGET_ERR_NOTFOUND,     /* unknown capability */
    PLAT_GADGET_ERR_NO_PROVIDER,  /* capability known, catalog empty */
    PLAT_GADGET_ERR_POLICY,       /* none survive min_quality / max_risk */
    PLAT_GADGET_ERR_UNAVAILABLE,  /* selected gadget not usable */
    PLAT_GADGET_ERR_EXECUTE,
    PLAT_GADGET_ERR_DENIED,       /* no live lease for (cap, context, gen) */
    PLAT_GADGET_ERR_STALE,        /* revoked, or context generation moved */
    PLAT_GADGET_ERR_EXPIRED,
    PLAT_GADGET_ERR_CONSUMED      /* one-shot already used */
} plat_gadget_status_t;

typedef struct plat_gadget_desc {
    char                  gadget_id[PLAT_GADGET_NAME_MAX];
    char                  capability[PLAT_GADGET_NAME_MAX];
    char                  runtime[PLAT_GADGET_NAME_MAX];
    char                  context[PLAT_GADGET_NAME_MAX];
    char                  requirement[PLAT_GADGET_NAME_MAX];
    plat_gadget_quality_t quality;
    plat_gadget_risk_t    risk;
} plat_gadget_desc_t;

typedef struct plat_gadget_candidate {
    plat_gadget_desc_t desc;
    char               status[16];   /* AVAILABLE / UNAVAILABLE / … */
    char               reason[PLAT_GADGET_REASON_MAX];
    int                usable;       /* 1 only when status is AVAILABLE or DEGRADED */
} plat_gadget_candidate_t;

typedef struct plat_gadget_explain {
    char                    capability[PLAT_GADGET_NAME_MAX];
    unsigned                n_candidates;
    plat_gadget_candidate_t candidates[PLAT_GADGET_MAX_CANDIDATES];
} plat_gadget_explain_t;

/* Default (NULL policy): min_quality=SNAPSHOT, max_risk=OBSERVATION.
 * That observes without inventing EXACT, and refuses MUTATING fallback. */
typedef struct plat_gadget_policy {
    plat_gadget_quality_t min_quality;
    plat_gadget_risk_t    max_risk;
} plat_gadget_policy_t;

/* S512: ответ обязан назвать ИСПОЛНИТЕЛЯ, ВЕРСИЮ, ПОКОЛЕНИЕ и РЕШЕНИЕ.
 *
 * Раньше здесь были только статус, имя возможности, id исполнителя и
 * качество. Этого мало для трёх разных вопросов, которые задают потом:
 *
 *   «кто это сделал»   — id мало: за одним id стоит конкретный runtime,
 *                        и именно он объясняет поведение;
 *   «против чего»      — ответ выдан под конкретный контекст и его
 *                        поколение. Без них запись в доказательствах нельзя
 *                        связать с арендой, по которой её выдали;
 *   «почему так»       — политика, ПРИМЕНЁННАЯ на самом деле (после
 *                        подстановки умолчаний), а не та, что передали.
 *                        Отказ «не прошёл по политике» без самой политики
 *                        не проверить и не оспорить.
 *
 * Поля добавлены в конец: старый читатель, читающий по прежним смещениям,
 * видит ровно то же, что видел. */
typedef struct plat_gadget_result {
    plat_gadget_status_t  status;
    char                  capability[PLAT_GADGET_NAME_MAX];
    char                  gadget_id[PLAT_GADGET_NAME_MAX];
    plat_gadget_quality_t quality;
    char                  detail[PLAT_GADGET_DETAIL_MAX];
    char                  reason[PLAT_GADGET_REASON_MAX];

    char                  runtime[PLAT_GADGET_NAME_MAX];  /* чем исполнено */
    uint32_t              gadget_version;                 /* версия записи каталога */
    uint64_t              context_id;                     /* против какого контекста */
    uint32_t              context_generation;             /* и какого его поколения */
    uint64_t              lease_id;                       /* по какой аренде */
    plat_gadget_policy_t  applied_policy;                 /* что применили на деле */
    plat_gadget_risk_t    risk;                           /* риск выбранного */
} plat_gadget_result_t;

int  plat_gadget_init(void);
void plat_gadget_fini(void);

/* Lists every catalog gadget for the capability, with Surface probe
 * status. Does not execute. Unknown capability → 0 candidates, return 0. */
int plat_gadget_explain(const char *capability, plat_gadget_explain_t *out);
int plat_gadget_explain_render(const plat_gadget_explain_t *e,
                               char *buf, size_t n);

/* Select exactly one gadget and execute it. Result always names the
 * capability; gadget_id is set only when a gadget was chosen.
 * process.snapshot:v1 needs a live lease on the host PROCESS context.
 * No lease is DENIED, not an empty OK. */
int plat_gadget_request(const char *capability,
                        const plat_gadget_policy_t *policy,
                        plat_gadget_result_t *out);

/* Same gate, explicit context generation. Zero handle is DENIED. */
int plat_gadget_request_h(const char *capability,
                          plat_context_handle_t ctx,
                          const plat_gadget_policy_t *policy,
                          plat_gadget_result_t *out);

/* Documented product context: this platx process (pid + birth + ns).
 * Creates if missing, reuses if live. Cannot mint → fail, not a skip. */
int plat_gadget_host_context(plat_context_handle_t *out);

const char *plat_gadget_quality_str(plat_gadget_quality_t q);
const char *plat_gadget_risk_str(plat_gadget_risk_t r);
const char *plat_gadget_status_str(plat_gadget_status_t s);

/* Test/host inject. NULL restores plat_surface_probe. The hook must
 * fill `out` the same way Surface does; it must not execute. */
typedef int (*plat_gadget_probe_hook_t)(const char *id,
                                        plat_surface_result_t *out);
void plat_gadget_set_probe_hook(plat_gadget_probe_hook_t fn);
19

hook

Общий слой перехватчиков и их провайдеров
src/hook/Наблюдение и исследование14 файлов4 API headers

Hook предоставляет provider-neutral lifecycle interposition: specification → policy → prepare → activate → events → detach. Mock, uprobe и kprobe backends обязаны выглядеть одинаково на уровне ownership/state/failure.

Граница ответственности

  • Provider реализует attach/detach/read events, но не публикует собственный parallel hook registry.
  • Policy проверяется до privileged attach; event callback не применяет lifecycle.
  • Dedicated appliance — supported baseline; kernel providers platform-dependent.

Устройство подсистемы

  • Hook instance содержит spec version, target context/generation, provider, state, TTL, resources и counters.
  • Transaction states CREATED/PREPARED/ACTIVE/DETACHING/DEAD сериализуют target exit, TTL и operator detach.
  • Provider resources claim-ятся scope; publication/event subscription после successful attach.
  • TTL task managed Core Task API и инициирует detach intent.

Поток работы

  • create spec → resolve target/provider/policy.
  • prepare resources → attach → ACTIVE event.
  • Provider records → normalized bounded events.
  • detach/exit/TTL → revoke → provider cleanup.

Отказ и восстановление

  • Target exits during attach → generation check rollback.
  • Detach called twice idempotent; provider error не оставляет row ACTIVE.
  • Event flood quota/drop metrics, no lifecycle lock in callback.

Основные возможности

  • Common lifecycle over mock, uprobe and kprobe providers.
  • Policy gates activation and event publication.
  • Dedicated hook appliance adds audit and trace without full monolith.
Архитектурные детали и инварианты

Overview

The Hook Executor provides a prioritised, timeout-safe event dispatch mechanism.

Subsystems register hooks against event masks; the dispatcher invokes them in

descending priority order. Misbehaving hooks (timeout or panic) are isolated

without crashing the process.

Security Invariants

**INV-HOOK-01** | If a hook's wall-clock execution time meets or exceeds its timeout_ms, the hook is **disabled** and its timeout_count is incremented. Default timeout: HOOK_DEFAULT_TTL_MS = 500 ms.

**INV-HOOK-02** | If a hook returns rc < -100 (panic indicator), it is **isolated** (disabled, disable_count++) immediately. The remaining hooks in the dispatch chain still execute.

**§SEC-3** | No malloc(). Hook table: hook_entry_t g_hooks[HOOK_MAX_REGISTERED] (64 slots) in static BSS.

Components

Defines g_hooks[64], g_n_hooks, g_hlock (pthread_mutex).

hook_register(), hook_unregister(), hook_enable(), hook_disable().

Builds a priority-sorted invocation order (bubble sort over ≤64 slots — O(n²)

is acceptable for N ≤ 64). Highest priority value fires first (0–255 scale).

Enforces INV-HOOK-01 (elapsed ≥ timeout → disable) and INV-HOOK-02 (rc < -100

→ isolate). hook_stats_get() returns aggregate call / timeout / disable counts.

Управление и диагностика

Корневые команды: hook, kprobe. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / hook →
Состав подсистемы / 14 файлов
Файл / компонентНазначение и граница
src/hook/cmd_hook.coperator CLI for the Hook Engine: create | activate | detach | The engine is booted by hook_boot; this file is the verbs. create resolves and attaches a point but leaves it watching nothing until activate commits it -- the fact of an ATTACH is the commit, not the attach, and that is why the
src/hook/hook_boot.cturning the Hook Engine on, and owning the operator's hooks. Two honesties shape this file. The mock provider is registered unconditionally because it needs nothing from the kernel and lets an operator prove the whole create-activate-attach-fact path anywhere. The uprobe provider is registered
src/hook/hook_boot.hthe operator's end of the Hook Engine. The engine resolves, attaches, commits and reports; K0-K4 proved all of it, but only tests ever called plat_hook_init, so the object code sat dead in a booted platform. This brings it up once and registers the providers a running
src/hook/hook_core.cinstances, attachments, and the frame that must never lie. The table is bounded and internal, in the same spirit as the XIO fd table. It is not a fifth platform registry: descriptors and capabilities stay where they already live, and nothing here is discoverable by name from outside.
src/hook/hook_dispatch.cINV-HOOK-01: hook exceeding timeout_ms → disabled + audit. INV-HOOK-02: hook returning negative panic code → isolated, not crash. Dispatches in priority order (highest first). No malloc.
src/hook/hook_flipswitch.csyscall interception via seccomp user-space notification. This is a full plat_hook_provider_t. Unlike hook_kprobe (read-only tracefs) and hook_uprobe (perf-fd breakpoint), flipswitch uses SECCOMP_RET_USER_NOTIF to intercept syscalls and optionally block or observe their arguments:
src/hook/hook_flipswitch.hsyscall interception via seccomp user-space notification. Provides a plat_hook_provider_t named "syscall.flipswitch" that intercepts Scope: self-only. The seccomp filter is installed on the calling thread and is inherited by threads created after installation. Cross-process interception
src/hook/hook_kprobe.ckprobe-based syscall tracing via tracefs Uses the standard Linux tracefs kprobe interface: Register: write probe_spec to .../kprobe_events Enable: write "1" to .../events/kprobes//enable Disable: write "0" to .../events/kprobes//enable
src/hook/hook_kprobe.hkprobe-based syscall tracing via tracefs Attaches kernel probes through the standard tracefs ABI (/sys/kernel/debug/tracing/kprobe_events) for debugging and monitoring. Design constraints: • READ-ONLY observation — probes log events, never modify arguments or
src/hook/hook_mock.ca provider with no machinery, so the ABI can be judged on its own. There is no trampoline here and nothing is patched. The caller wraps a direct call: enter, the real function, leave. That is enough to hold the engine to its central promise -- in TAP the value the caller receives is the function's
src/hook/hook_policy.cthe runtime that applies a decision, so the callback never has to. Level one is exactly this separation. A callback inspects and says what it wants; something else decides whether that is allowed and carries it out. If the callback called the original itself, or blocked by reaching for
src/hook/hook_priority.cDefines g_hooks[] shared with hook_dispatch.c.
src/hook/hook_timeout.cINV-HOOK-01: hook that times out is disabled automatically. This module provides the timeout management API separately from dispatch.
src/hook/hook_uprobe.cthe first provider that watches something real. A uprobe is a kernel breakpoint on a byte offset inside a file, so this splits cleanly in two. Turning a symbol name into that offset is arithmetic over the ELF: find the symbol, find the segment that loads it, subtract. It
Контракты API / 4 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/hook.h
/* platx/hook.h — Hook Engine: interposition, not an injector.
 *
 * The engine answers "what did that target just do". It does not execute I/O,
 * it does not decide restart or lifecycle, and it is not a place to hang a
 * second supervisor. A callback inspects, classifies, and produces a decision;
 * whether that decision is applied at all is the runtime's business and, for
 * anything past observation, a capability's.
 *
 * Four names are kept apart on purpose. The target is what gets interposed. An
 * attachment is one point on it. A provider is the mechanism that reaches that
 * point. A policy is what to do about what comes back. Collapsing them is how
 * a hook library turns into an injector.
 *
 * Three numbers travel with every fact and they are not interchangeable:
 * hook_id names the instance until it is destroyed, attachment_id names one
 * point on it, and the owner's generation says which epoch the instance
 * belongs to. A stale generation kills a callback; it does not merely warn.
 *
 * Nothing in this file loads code into another process. The first production
 * mode is observation, and observation never changes what the target returns.
 */
#define PLAT_HOOK_ID_INVALID     0u
#define PLAT_HOOK_ATTACH_INVALID 0u
#define PLAT_HOOK_MAX_INSTANCES  8u
#define PLAT_HOOK_MAX_ATTACH     8u
#define PLAT_HOOK_MAX_PROVIDERS  4u
#define PLAT_HOOK_NAME_MAX       64u
#define PLAT_HOOK_ARGS_MAX       4u

typedef uint32_t plat_hook_id_t;
typedef uint32_t plat_hook_attach_id_t;

/* Channel modes. Which of them an instance may use is a capability question,
 * not a preference: a mode with no capability behind it is not a mode. */
typedef enum plat_hook_mode {
    PLAT_HOOK_TAP = 0,     /* the original runs, unchanged, always */
    PLAT_HOOK_MIRROR,
    PLAT_HOOK_REPLACE,
    PLAT_HOOK_PROXY,
    PLAT_HOOK_SIMULATE,
    PLAT_HOOK_FAULT,
    PLAT_HOOK_MODE__COUNT
} plat_hook_mode_t;

/* Capabilities, named once. observe and inspect are granted to every mode,
 * because a mode that cannot look at anything has nothing to report. */
#define PLAT_HOOK_CAP_OBSERVE    (1u << 0)
#define PLAT_HOOK_CAP_INSPECT    (1u << 1)
#define PLAT_HOOK_CAP_INTERCEPT  (1u << 2)  /* modify args or result, REPLACE */
#define PLAT_HOOK_CAP_LIMIT      (1u << 3)  /* block, DENY */
#define PLAT_HOOK_CAP_ROUTE      (1u << 4)  /* pick the I/O path; not yet */

/* Why something was refused, as a field. A new reason is cheaper and more
 * honest than a new event type for every failure. */
typedef enum plat_hook_reason {
    PLAT_HOOK_OK = 0,
    PLAT_HOOK_RESOLVE_FAILED,
    PLAT_HOOK_PERMISSION_DENIED,
    PLAT_HOOK_UNSUPPORTED,
    PLAT_HOOK_TARGET_EXITED,
    PLAT_HOOK_GENERATION_STALE,
    PLAT_HOOK_ATTACH_FAILED,
    PLAT_HOOK_TRANSACTION_ABORTED,
    PLAT_HOOK_DRAIN_TIMEOUT,
    PLAT_HOOK_PROVIDER_ERROR,
    PLAT_HOOK_CAPABILITY_DENIED
} plat_hook_reason_t;

/* ATTACHED without ACTIVE means the transaction has not been committed.
 * DRAINING refuses new frames and waits for the ones already inside. */
typedef enum plat_hook_state {
    PLAT_HOOK_STATE_FREE = 0,
    PLAT_HOOK_STATE_CREATED,
    PLAT_HOOK_STATE_PREPARED,
    PLAT_HOOK_STATE_ATTACHED,
    PLAT_HOOK_STATE_ACTIVE,
    PLAT_HOOK_STATE_DRAINING,
    PLAT_HOOK_STATE_DETACHED,
    PLAT_HOOK_STATE_DESTROYED
} plat_hook_state_t;

/* Which threads of the target are interposed. This is about whom to watch;
 * recursion suppression is a different thing and lives in the runtime. */
typedef enum plat_hook_scope {
    PLAT_HOOK_SCOPE_ALL = 0,
    PLAT_HOOK_SCOPE_CURRENT,
    PLAT_HOOK_SCOPE_EXCLUDE,
    PLAT_HOOK_SCOPE_EXPLICIT
} plat_hook_scope_t;

/* What a callback may ask for. Everything past OBSERVE needs a capability,
 * and the runtime applies it -- the callback never acts on its own. */
typedef enum plat_hook_decision {
    PLAT_HOOK_PASS = 0,
    PLAT_HOOK_OBSERVE,
    PLAT_HOOK_MODIFY,
    PLAT_HOOK_REPLACE_RESULT,
    PLAT_HOOK_DENY
} plat_hook_decision_t;

/* A target is described, not addressed. An address is the last resort, and a
 * pid of -1 -- every process on the box -- is not available here at all. */
typedef struct plat_hook_target_spec {
    int                pid;              /* explicit pid, or 0 for self */
    char               binary_path[PLAT_HOOK_NAME_MAX];
    char               symbol[PLAT_HOOK_NAME_MAX];
    unsigned long long offset;
    unsigned long long address;
    unsigned long long cookie;           /* tells two attach points apart */
} plat_hook_target_spec_t;

/* What the callback sees. Copied for the duration of the call; retaining it
 * or freeing it is not the callback's to do. */
typedef struct plat_hook_observation {
    plat_hook_id_t        hook_id;
    plat_hook_attach_id_t attachment_id;
    uint32_t              generation;
    unsigned long long    cookie;
    unsigned long long    tid;
    unsigned long long    ts_ns;
    unsigned              n_args;
    unsigned long long    args[PLAT_HOOK_ARGS_MAX];
    long long             result;        /* on_leave only, as telemetry */
} plat_hook_observation_t;

/* Five callbacks, not one pointer. None of them may take a core lock, call
 * lifecycle or recovery, load a plugin, or block on I/O. */
typedef struct plat_hook_callbacks {
    void                 (*on_attach)(plat_hook_id_t, plat_hook_attach_id_t, void *ud);
    plat_hook_decision_t (*on_enter)(const plat_hook_observation_t *, void *ud);
    void                 (*on_leave)(const plat_hook_observation_t *, void *ud);
    void                 (*on_error)(plat_hook_id_t, plat_hook_reason_t, void *ud);
    void                 (*on_detach)(plat_hook_id_t, plat_hook_attach_id_t, void *ud);
} plat_hook_callbacks_t;

/* A PIC handler table is data found by the existing plugin loader, not a
 * second loader and not an implicit callback.  The hook core accepts it only
 * with provenance that says the image was verified and is ET_DYN. */
#define PLAT_HOOK_OPS_VERSION 1u

typedef enum plat_hook_pic_image {
    PLAT_HOOK_PIC_IMAGE_NONE = 0,
    PLAT_HOOK_PIC_IMAGE_ET_DYN,
    PLAT_HOOK_PIC_IMAGE_ET_REL
} plat_hook_pic_image_t;

typedef struct plat_hook_pic_provenance {
    uint32_t              size;
    uint32_t              verified;
    plat_hook_pic_image_t image;
    const char           *origin;
} plat_hook_pic_provenance_t;

typedef struct plat_hook_ops {
    uint32_t version;
    plat_hook_decision_t (*classify)(const plat_hook_observation_t *, void *ud);
} plat_hook_ops_t;

typedef enum plat_hook_health {
    PLAT_HOOK_AVAILABLE = 0,
    PLAT_HOOK_DEGRADED,
    PLAT_HOOK_UNAVAILABLE
} plat_hook_health_t;

/* One provider ABI for every mechanism. Not one header per implementation. */
typedef struct plat_hook_provider {
    const char *name;
    uint32_t    modes;    /* bit per plat_hook_mode_t it can actually do */
    uint32_t    caps;     /* bit per capability it implements */
    uint32_t    scopes;   /* bit per thread scope it implements */

    int  (*resolve)(const plat_hook_target_spec_t *spec, void **point_out);
    int  (*attach)(plat_hook_id_t id, plat_hook_attach_id_t aid, void *point);
    int  (*detach)(plat_hook_id_t id, plat_hook_attach_id_t aid);
    int  (*transaction_begin)(plat_hook_id_t id);
    int  (*transaction_commit)(plat_hook_id_t id);
    int  (*transaction_abort)(plat_hook_id_t id);
    plat_hook_health_t (*health)(void);
} plat_hook_provider_t;

/* query() output, three levels, all of it numbers and names. It never hands
 * back a trampoline, a live argument frame, or anything from the target's
 * address space. */
typedef struct plat_hook_provider_info {
    char               name[PLAT_HOOK_NAME_MAX];
    uint32_t           modes;
    uint32_t           caps;
    uint32_t           scopes;
    plat_hook_health_t health;
} plat_hook_provider_info_t;

typedef struct plat_hook_info {
    plat_hook_id_t     hook_id;
    plat_owner_t       owner;
    uint32_t           generation;
    plat_hook_state_t  state;
    plat_hook_mode_t   mode;
    plat_hook_scope_t  scope;
    uint32_t           caps;
    char               provider[PLAT_HOOK_NAME_MAX];
    uint32_t           n_attachments;
    uint64_t           inflight;
    plat_hook_reason_t last_reason;
} plat_hook_info_t;

typedef struct plat_hook_attach_info {
    plat_hook_attach_id_t attachment_id;
    char                  symbol[PLAT_HOOK_NAME_MAX];
    unsigned long long    offset;
    unsigned long long    cookie;
    plat_hook_state_t     state;
    uint64_t              enters;
    uint64_t              leaves;
    uint64_t              suppressed;   /* re-entries that did not recurse */
    uint64_t              inflight;
    plat_hook_reason_t    last_reason;
} plat_hook_attach_info_t;

/* What goes out on the event ring. Numbers only: no pointers, nothing from
 * the target's address space, and small enough for the bounded payload. Both
 * ids travel together so a refusal can be joined to the attach it belongs to
 * without guessing from ring order. */
typedef struct plat_hook_fact {
    uint32_t hook_id;
    uint32_t attachment_id;   /* 0 when there is no point yet */
    uint32_t module_id;
    uint32_t instance_id;
    uint32_t generation;
    uint32_t mode;
    uint32_t reason;
    int32_t  rc;
    unsigned long long cookie;
    char     provider[PLAT_HOOK_NAME_MAX];
} plat_hook_fact_t;

int  plat_hook_init(void);
void plat_hook_fini(void);

int  plat_hook_provider_register(const plat_hook_provider_t *provider);

/* generation 0 refuses. A mode the provider cannot do, or a capability set the
 * mode is not entitled to, refuses with CAPABILITY_DENIED rather than quietly
 * downgrading to observation. */
plat_hook_id_t plat_hook_create(plat_owner_t owner, const char *provider,
                                plat_hook_mode_t mode, plat_hook_scope_t scope,
                                uint32_t caps,
                                const plat_hook_callbacks_t *cb, void *ud);

/* Bind HOOK_OPS to the same on_enter slot used by native callbacks.  Binding
 * is a pre-activation operation.  Missing/unsigned provenance, ET_REL, stale
 * generation, or a missing classify callback refuses without activating the
 * instance. */
int plat_hook_bind_pic(plat_hook_id_t id, const plat_hook_ops_t *ops,
                       const plat_hook_pic_provenance_t *provenance,
                       void *ud);

/* T11: bind HOOK_OPS from an already-loaded image, named through the
 * Plugin/Hook bridge (platx/hook_plugin.h).  The engine does not know what a
 * plugin is: it asks the registered bridge to describe the name and gets back
 * a HOOK_OPS pointer plus the owner's provenance verdict.
 *
 * Refuses if no bridge is registered (the capability is UNAVAILABLE and
 * absence of a provider is never success), if the bridge does not know the
 * name (T11), if the image exports no HOOK_OPS (T14), if provenance is not
 * verified (T12), if the image is ET_REL (T13), if the generation is stale
 * (T17), or if the slot is ACTIVE/DRAINING (T15).  All refusal logic flows
 * through plat_hook_bind_pic - no second PIC stack (T20). */
int plat_hook_bind_plugin(plat_hook_id_t id, const char *plugin_name,
                           void *ud);

plat_hook_attach_id_t plat_hook_attach(plat_hook_id_t id,
                                       const plat_hook_target_spec_t *spec);

/* Commit. Until this returns, attachments exist but no frame runs. */
int  plat_hook_activate(plat_hook_id_t id);

/* ── transaction ───────────────────────────────────────────────────────────
 *
 * Several points on one target go live together or not at all. Half a set is
 * the worst outcome available: the operator reads coverage from the ones that
 * took, and the gaps are exactly where nobody looks.
 *
 * An attach that fails while a transaction is open poisons it. Commit then
 * refuses, and abort is the way out -- rolling back every point staged in this
 * transaction, including the ones that attached cleanly.
 */
int  plat_hook_tx_begin(plat_hook_id_t id);
int  plat_hook_tx_commit(plat_hook_id_t id);
int  plat_hook_tx_abort(plat_hook_id_t id);

/* Refuses new frames, waits for the ones inside, then detaches. Freeing under
 * a live frame is the one bug this design cannot survive. */
int  plat_hook_detach(plat_hook_id_t id, plat_hook_attach_id_t aid);
int  plat_hook_destroy(plat_hook_id_t id);

/* New epoch. Frames carrying the old generation stop being delivered. */
int  plat_hook_retarget(plat_hook_id_t id, plat_owner_t owner);

/* The dispatch a provider calls around the interposed point. */
plat_hook_decision_t plat_hook_enter(plat_hook_id_t id,
                                     plat_hook_attach_id_t aid,
                                     const unsigned long long *args,
                                     unsigned n_args);
void plat_hook_leave(plat_hook_id_t id, plat_hook_attach_id_t aid,
                     long long result);

/* Unknown ids are errors. An empty struct that reads as healthy is worse than
 * a failure, because nobody checks it. */
int  plat_hook_query(plat_hook_id_t id, plat_hook_info_t *out);
int  plat_hook_query_attachment(plat_hook_id_t id, plat_hook_attach_id_t aid,
                                plat_hook_attach_info_t *out);
int  plat_hook_query_provider(const char *name, plat_hook_provider_info_t *out);

/* ── mock provider ─────────────────────────────────────────────────────────
 *
 * No patching, no kernel, no trampoline: the caller wraps a direct call. It
 * exists so the ABI above is testable everywhere, and so the first real
 * provider has something to be compared against.
 */
const plat_hook_provider_t *plat_hook_mock_provider(void);

/* Runs fn through the attachment: enter, the real call, leave. In TAP the
 * value returned is always fn's own. */
long long plat_hook_mock_invoke(plat_hook_id_t id, plat_hook_attach_id_t aid,
                                long long (*fn)(long long), long long arg);

/* ── uprobe provider ───────────────────────────────────────────────────────
 *
 * The first provider that touches a real target. A uprobe is a kernel-side
 * breakpoint on a file offset in a binary, so the work splits in two: turning
 * a symbol name into that offset, which is arithmetic over the ELF and needs
 * no privilege, and asking the kernel to watch it, which needs plenty.
 *
 * It observes and nothing else. There is no return probe and no way to write a
 * value back, which is what keeps the promise that the target cannot tell.
 *
 * The kernel places a uprobe per process, not per thread, so a request to
 * watch one thread is reported as unsupported rather than widened to all of
 * them. And every process on the machine is not a target: a pid must be
 * explicit or self.
 */
int hook_uprobe_supported(void);

/* The reason the last attach failed, kept because "no permission" and "no such
 * symbol" call for completely different responses from whoever is reading. */
int hook_uprobe_last_errno(void);

/* Symbol to file offset, on its own. Exposed because this is the half that
 * fails silently: a wrong offset makes the kernel watch the wrong byte and
 * report nothing, which looks exactly like a quiet target. */
int hook_uprobe_resolve(const char *binary_path, const char *symbol,
                        unsigned long long *offset_out);

const plat_hook_provider_t *plat_hook_uprobe_provider(void);

/* Hits accumulate in the kernel. This drains them into the engine's own
 * dispatch, so a uprobe attachment reports through exactly the same counters
 * as any other. Returns the number of frames delivered, or -1. */
int hook_uprobe_poll(plat_hook_id_t id, plat_hook_attach_id_t aid);

/* ── decision runtime ──────────────────────────────────────────────────────
 *
 * The callback says what it wants; this applies it. That separation is the
 * whole of level one. A callback that called the original itself, or that
 * reached for lifecycle to make its refusal stick, would be a second decision
 * maker inside a component that is not allowed to have one.
 *
 * A refusal here is a syscall-shaped no: the original does not run and the
 * caller is told. It is not a kill, not a restart, and not a request to any
 * other subsystem.
 */
typedef struct plat_hook_outcome {
    plat_hook_decision_t decision;   /* what was applied, not what was asked */
    plat_hook_reason_t   reason;
    int                  ran;        /* did the original actually run */
    long long            result;
} plat_hook_outcome_t;

/* Runs fn under the attachment and applies the verdict. With no rights to
 * block, a callback asking for it is refused and fn runs exactly as it would
 * have. Returns 0 when the outcome is filled, -1 when the call was not ours
 * to make at all. */
int plat_hook_policy_invoke(plat_hook_id_t id, plat_hook_attach_id_t aid,
                            long long (*fn)(long long), long long arg,
                            long long deny_result,
                            plat_hook_outcome_t *out);
include/platx/hook_hades_ctx.h
/* platx/hook_hades_ctx.h — INT-127: Hook async context via Hades/Flow (observer). */

#define PLAT_HOOK_HADES_NAME_MAX  64
#define PLAT_HOOK_HADES_SLOTS_MAX 16
typedef struct {
    uint64_t       ctx_id;
    uint32_t       ctx_gen;
    plat_corr_id_t corr_id;
    uint64_t       flow_id;
    uint64_t       bound_at_ms;
} plat_hook_hades_ctx_t;
int plat_hook_hades_ctx_bind(const char *hook_name,
                              const plat_hook_hades_ctx_t *ctx);
int plat_hook_hades_ctx_get(const char *hook_name,
                             plat_hook_hades_ctx_t *out);
int plat_hook_hades_ctx_unbind(const char *hook_name);
include/platx/hook_plugin.h
/* platx/hook_plugin.h — the one narrow Plugin -> Hook bridge.
 *
 * Why this header exists
 * ----------------------
 * hook_core.c used to `#include "plugin/plugin.h"` and read plugin_t's
 * fields (hook_ops, load_addr, image_size) directly.  That made the core
 * Hook Engine depend on an internal header of a neighbouring module and on
 * that module's struct layout: the hook build broke as soon as the plugin
 * header was not on the include path, and no hook unit could link without
 * the whole plugin type in scope.
 *
 * The Hook Engine does not need to know what a plugin is.  It needs exactly
 * two facts about an already-loaded image:
 *
 *   1. the HOOK_OPS table the image exports (or that it exports none), and
 *   2. whether the owner of that image verified its provenance.
 *
 * This header is that contract and nothing else.  It is a versioned vtable
 * registered at runtime by whoever owns the plugin registry.  Hook never
 * dereferences a plugin descriptor, never dlsym()s, and never loads.
 *
 * Fail-closed
 * -----------
 * There is no weak fallback that succeeds.  With no bridge registered the
 * capability is UNAVAILABLE and plat_hook_bind_plugin() refuses; a name the
 * registry does not know is NOTFOUND and also refuses.  Absence of a
 * provider is never reported as success.
 *
 * Standard C only.  This header pulls in no module-private type.
 */

/* Bump only on an incompatible change to the structs below.  A bridge whose
 * abi does not match is refused at registration time rather than called
 * with a layout neither side agrees on. */
#define PLAT_HOOK_PLUGIN_BRIDGE_ABI  1u

/* describe() return codes.  Anything other than PLAT_HOOK_PLUGIN_OK means
 * the caller learned nothing and must refuse. */
#define PLAT_HOOK_PLUGIN_OK            0
#define PLAT_HOOK_PLUGIN_NOTFOUND    (-2)  /* registry has no such name   */
#define PLAT_HOOK_PLUGIN_UNAVAILABLE (-3)  /* no registry / cannot answer */

/* One already-loaded image, described in the only terms the Hook Engine is
 * entitled to know.  Everything here is borrowed: the bridge owns the
 * storage.  `ops` must stay valid for as long as the image stays loaded --
 * that lifetime is the plugin owner's guarantee, and plat_hook_destroy() is
 * what the owner must wait for before unmapping.
 *
 * `verified` is the plugin owner's provenance verdict, not a hint.  The
 * bridge sets it to 0 whenever it could not check the bytes -- for example a
 * dlopen-backed image whose raw bytes are not mapped where the archive
 * provenance table can see them.  Hook treats 0 as a refusal. */
typedef struct plat_hook_plugin_image {
    uint32_t               size;      /* sizeof(*out), written by the bridge */
    const plat_hook_ops_t *ops;       /* HOOK_OPS, or NULL if not exported   */
    uint32_t               verified;  /* 1 = provenance checked and good     */
    plat_hook_pic_image_t  image;     /* ET_DYN / ET_REL / NONE              */
    const char            *origin;    /* image name, borrowed, may be NULL   */
} plat_hook_plugin_image_t;

/* The vtable.  One entry point: answer a name.  No load, no unload, no
 * enumeration -- those stay entirely on the plugin side. */
typedef struct plat_hook_plugin_bridge {
    uint32_t size;   /* sizeof(plat_hook_plugin_bridge_t) at build time */
    uint32_t abi;    /* PLAT_HOOK_PLUGIN_BRIDGE_ABI                     */

    /* Fill @out for @plugin_name.  Returns PLAT_HOOK_PLUGIN_OK and a fully
     * initialised @out, or one of the negative codes above. */
    int (*describe)(const char *plugin_name, plat_hook_plugin_image_t *out);
} plat_hook_plugin_bridge_t;

/* Install the bridge.  @bridge must outlive the process (static storage is
 * the expected form).  Returns 0, or -1 if the abi does not match, the size
 * is short, or describe is NULL.  Passing NULL clears a previously
 * registered bridge and returns 0 -- that is the UNAVAILABLE state, and it
 * is what a test uses to prove bind refuses without a provider. */
int plat_hook_plugin_bridge_register(const plat_hook_plugin_bridge_t *bridge);

/* The bridge currently in force, or NULL when none is registered. */
const plat_hook_plugin_bridge_t *plat_hook_plugin_bridge_get(void);
include/platx/platx_hook.h
/* include/platx/platx_hook.h — hook dispatch/priority/timeout public API (task 3.40).
 *
 * INV-HOOK-01: hook timeout → hook disabled + audit event.
 * INV-HOOK-02: hook panic → isolated, process continues.
 * §SEC-3: no malloc in dispatch/priority/timeout paths.
 */

#define HOOK_MAX_REGISTERED   64u
#define HOOK_NAME_MAX         64u
#define HOOK_DEFAULT_TTL_MS   500u   /* INV-HOOK-01: default timeout       */
#define HOOK_PRIORITY_MAX     255u
#define HOOK_PRIORITY_DEFAULT 128u

typedef enum {
    HOOK_EVENT_IO      = 1,
    HOOK_EVENT_AUDIT   = 2,
    HOOK_EVENT_POLICY  = 3,
    HOOK_EVENT_HADES   = 4,
    HOOK_EVENT_MESH    = 5,
    HOOK_EVENT_FABRIC  = 6,
} hook_event_type_t;

typedef struct {
    void     *data;
    size_t    datasz;
    uint64_t  source_id;
    uint32_t  event_type;
    uint32_t  flags;
} hook_event_t;

typedef int (*hook_fn_t)(const hook_event_t *ev, void *userdata);

typedef struct {
    char          name[HOOK_NAME_MAX];
    hook_fn_t     fn;
    void         *userdata;
    uint32_t      event_mask;   /* bitmask of HOOK_EVENT_* */
    uint8_t       priority;     /* 0=lowest, 255=highest    */
    uint32_t      timeout_ms;   /* INV-HOOK-01              */
    int           enabled;
    uint64_t      call_count;
    uint64_t      timeout_count;
    uint64_t      disable_count;
} hook_entry_t;

/* registration */
int  hook_register(const char *name, hook_fn_t fn, void *userdata,
                   uint32_t event_mask, uint8_t priority, uint32_t timeout_ms);
int  hook_unregister(const char *name);
int  hook_enable(const char *name);
int  hook_disable(const char *name);

/* dispatch: calls all matching hooks in priority order (highest first).
 * INV-HOOK-01: hook that exceeds timeout_ms is disabled. */
int  hook_dispatch(const hook_event_t *ev);

/* stats */
typedef struct {
    uint64_t hook_calls;
    uint64_t hook_timeouts;     /* INV-HOOK-01 trips */
    uint64_t hook_panics;       /* INV-HOOK-02 catches */
    uint64_t hook_disables;
} hook_stats_t;
void hook_stats_get(hook_stats_t *out);
20

integrity

Наблюдение целостности и управление проверками модулей
src/integrity/Доверие и защита16 файлов1 API headers

Наблюдение целостности и управление проверками модулей

Граница ответственности

  • Verifier вычисляет facts и не скрывает/patch-ит target.
  • LKM/BPF enforcement research paths отдельны от supported read-only capability.
  • Rules versioned/signed; CLI не имеет обхода policy.

Устройство подсистемы

  • Integrity fact schema: subject context, object identity/hash/metadata, source, time, policy version и verdict.
  • Rule compiler строит immutable matcher; reload atomic generation.
  • Sensor adapters file/process/net/module публикуют facts.
  • Enforcement operation требует lease, isolation и transaction; report aggregates immutable facts.

Поток работы

  • Rule load/verify → active generation.
  • Sensor/input → normalize/hash → evaluate.
  • Verdict event/audit/report.
  • Optional authorized response via separate operation.

Отказ и восстановление

  • Невозможно прочитать объект → UNKNOWN/COVERAGE_GAP, не clean.
  • Rule parse/reload failure сохраняет old generation.
  • Sensor loss degrades coverage map.
  • Enforcement partial failure audited and reconciled.

Основные возможности

  • File/module/process/network integrity rules and reports.
  • LKM load/unload/attach paths plus BPF integration.
  • Script/DSL surface can drive monitoring policy.
Архитектурные детали и инварианты

Обзор

Модуль integrity реализует userspace-интерфейс к IMA/EVM (Integrity Measurement Architecture).

Компоненты

- **ima_measure.c** — SHA-256 измерение файла, самодостаточная реализация без внешних зависимостей. Статический буфер 4KB (§SEC-3). Graceful degradation при отсутствии securityfs.

- **ima_appraise.c** — чтение xattr security.ima через getxattr(2). Различает: файл не существует (IMA_APPR_NOFILE), нет xattr (IMA_APPR_NOSIG), короткий xattr (IMA_APPR_BADSIG), OK.

- **ima_audit.c** — emit событий в PLATX audit ring через слабый шов plat_audit_write. Формат: IMA|event=X|path=Y|result=Z|ts=S.us.

Инварианты

- §INV-INTEGRITY-01: файл без подписи → IMA_APPR_NOSIG (не OK)

- §SEC-3: нет malloc в hot path

- §SEC-1: -Werror при компиляции

Тесты (TAP)

- t_ima_measure: 6 тестов — NULL args, nonexistent, /etc/hostname, path/sha256 filled

- t_ima_appraise: 4 теста — NULL, nonexistent, real file, range check

Управление и диагностика

Корневые команды: integrity. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / integrity →
Состав подсистемы / 16 файлов
Файл / компонентНазначение и граница
src/integrity/cmd_integrity.cuserspace CLI для LKM integrity. integrity load [--all|--module=] integrity unload [--module=] integrity scan [--type=proc|net|syscall|hide|all] integrity report [--format=json|text] integrity watch [--period=SEC] integrity syscall verify|log|hook|set|restore|list
src/integrity/ima_appraise.cРеализация ima / appraise
src/integrity/ima_audit.cРеализация ima / audit
src/integrity/ima_measure.cЧитает /sys/kernel/security/ima/ascii_runtime_measurements (securityfs). SHA-256 вычисляется без malloc: файл читается в static buffer. При отсутствии securityfs — graceful degradation (log WARN, return 0).
src/integrity/integrity_api.cреализация C vtable для LKM integrity. Все функции открывают /dev/integrity через XIO, выполняют ioctl и закрывают. Статический глобальный vtable g_integrity_if публикуется через platform_provide("integrity", PLAT_IF_VERSION(1,0), &g_integrity_if) при вызове integrity_api_init().
src/integrity/integrity_api.hпубличный C API модуля LKM integrity. Userspace door to /dev/integrity. Not the working path (connect → handshake → send/recv → switch → disconnect) and not an XIO backend. Регистрируется в реестре платформы через platform_provide(): platform_provide("integrity", PLAT_IF_VERSION(1,0), &g_integrity_if);
src/integrity/integrity_dsl.cпрямой DSL-обработчик шагов command:"integrity". integrity_dsl_run_step() — dispatch шага через vtable (без CLI-моста) integrity_dsl_provision() — обработка блока provision.integrity integrity_dsl_init() — инициализация (идемпотентна) Подключается к dsl_exec.c через #pragma weak — если этот файл не собран,
src/integrity/integrity_dsl.hDSL-интеграция модуля LKM integrity. Обеспечивает прямой вызов integrity_api_t vtable из исполнителя DSL (dsl_exec.c), минуя CLI-мост. Это позволяет DSL-шагам с command: integrity использовать параметры из YAML-блока params напрямую (без строкового разбора командной строки), получать структурированные ошибки и вести
src/integrity/integrity_mil.cРеализация integrity / mil
src/integrity/integrity_mil.hподсистема integrity не была связана с загрузкой модулей ни одним вызовом: ima_appraise_file() не вызывался нигде за пределами собственных тестов msx_mil_verify, elf_verify_signature) проверяла ТОЛЬКО ELF/MSX-подпись и про IMA не знала. То есть инвариант существовал в тексте задания и нигде
src/integrity/integrity_script.cбиндинги LKM integrity для скриптового движка (.ms). Регистрирует namespace "integrity" в msx_ctx_t: integrity.status() → объект с полями version, hooks_active, ... integrity.scan(type) → null (async; результат через report) integrity.scan_all() → null
src/integrity/lkm_integrity.hshared kernel/userspace interface for integrity LKM Communication: /dev/integrity — char device, ioctl Status read : /proc/integrity_status
src/integrity/tests/t_ima_appraise.cРеализация t / ima / appraise
src/integrity/tests/t_ima_measure.cГлавное: собственная SHA-256 в ima_measure.c сверяется с векторами FIPS 180-4 и с системным sha256sum. Реализация хеша, не сверенная с эталоном, — это не измерение целостности, а генератор случайных байт.
src/integrity/tests/t_int_integrity_msx.cЧто здесь настоящее и что — стенд, честно: - настоящее: логика platx_integrity_check_module(), порядок ступеней, обработка вердикта верификатора, счётчики; - стенд: сам msx_verify_path(). Реальная реализация живёт в src/msx — это зона полосы A3, её файлы не правятся и в тест не тянутся
src/integrity/tests/t_integrity_mil.cКлючевая ветка — «подписи нет → отказ» — проверяется НАСТОЯЩИМ ima_appraise_file() на настоящем файле, без подмен. Подменный оценщик используется только там, где ветка недостижима без CAP_SYS_ADMIN (запись xattr security.ima даёт EPERM — измерено), и каждый такой случай
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/platx_integrity.h
/* platx_integrity.h — PlatX Integrity: публичный API (Wave 5, §5.10).
 *
 * IMA (Integrity Measurement Architecture) userspace interface:
 *   ima_measure  — записать измерение файла в IMA measurement list
 *   ima_appraise — проверить подпись (IMA_APPRAISE)
 *   ima_audit    — emit IMA события в PLATX audit ring
 *
 * §SEC-3: нет malloc в hot path.
 * SPDX-License-Identifier: GPL-2.0
 */

/* ── IMA measurement ─────────────────────────────────────────────────────── */
#define IMA_HASH_LEN  32   /* SHA-256 */
#define IMA_PATH_MAX 256

typedef struct {
    char     path[IMA_PATH_MAX];
    uint8_t  sha256[IMA_HASH_LEN];
    uint32_t pcr;     /* PCR index (обычно 10) */
} ima_measurement_t;

/* ima_measure_file — вычисляет SHA-256 файла, append в measurement log.
 * Возвращает 0 или -1. §SEC-3: static buffers. */
int ima_measure_file(const char *path, ima_measurement_t *out);

/* ima_measure_get_log — читает /sys/kernel/security/ima/ascii_runtime_measurements.
 * buf: static буфер вызывающего, max: его размер.
 * Возвращает прочитанных байт или -1. */
ssize_t ima_measure_get_log(char *buf, size_t max);

/* ── IMA appraisal ───────────────────────────────────────────────────────── */
typedef enum {
    IMA_APPR_OK      = 0,
    IMA_APPR_NOSIG   = 1,  /* нет подписи */
    IMA_APPR_BADSIG  = 2,  /* невалидная подпись */
    IMA_APPR_NOFILE  = 3,  /* файл не найден */
    IMA_APPR_ERROR   = -1
} ima_appr_result_t;

/* ima_appraise_file — проверяет xattr security.ima (или симуляция в test mode).
 * Возвращает ima_appr_result_t. */
ima_appr_result_t ima_appraise_file(const char *path);

/* ── IMA audit ────────────────────────────────────────────────────────────── */
void ima_audit_emit(const char *event, const char *path, ima_appr_result_t result);

/* ── Counters (§5.48) ────────────────────────────────────────────────────── */
typedef struct {
    uint64_t measurements;
    uint64_t appraised;
    uint64_t appr_failed;
} ima_counters_t;

void ima_counters_get(ima_counters_t *out);
21

keyring

Сервис ключевого материала и защищённых значений
src/keyring/Доверие и защита13 файлов3 API headers

Keyring предоставляет versioned key/value secret service с TTL, allocation-aware retrieval, list/prefix delete и daemon/client IPC. Он должен быть единым источником secret retrieval для consumers, но не заменяет encrypted vault persistence или protocol authentication.

Граница ответственности

  • Public contract storage.keyring v0x00010001; internal store layout скрыт.
  • Daemon IPC serializes values and validates bounds/version; no pointers.
  • List metadata не раскрывает values; logs redact keys по policy.

Устройство подсистемы

  • Store entry содержит key, secret buffer, owner/ACL, generation, created/expiry и flags. Hash/index bounded или имеет quotas.
  • get_alloc/take_alloc явно определяют ownership; TTL sweep/revoke publish events.
  • Daemon wraps same store API; client handles reconnect/generation.
  • Capability adapter публикуется после store/daemon ready и revoke-ится до wipe.

Поток работы

  • Consumer resolve/authz → get/put request.
  • Validate key/size/ACL/TTL → store transaction.
  • Return copy/owned handle → consumer wipe/release.
  • Expiry/revoke → zeroize → event.

Отказ и восстановление

  • OOM/duplicate/update semantics explicit, no partial value.
  • Daemon disconnect makes proxy stale; reconnect new generation.
  • Clock uses monotonic TTL basis, persisted expiry converts carefully.

Основные возможности

  • In-process store plus daemon/client IPC facade.
  • TTL, allocation-aware retrieval, prefix delete/list and revoke sweep contracts.
  • Mesh exchange/discovery and vault seeding integrations.

Управление и диагностика

Корневые команды: keyring. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / keyring →
Состав подсистемы / 13 файлов
Файл / компонентНазначение и граница
src/keyring/cmd_keyring.cnamespace "keyring": административный CLI и управление демоном KeyringDaemon. Работает как keyring-cli (по сокету) и как in-process C-порт (прямой доступ к store), выбирая путь автоматически: - если демон отвечает на PING по сокету → используем протокол (keyring-cli);
src/keyring/keyring.cUnified keyring interface with compiler-safe key zeroing. STAB-115: All key material zeroed with explicit_bzero(), never memset(), including unwrap_buf, retire buffers, and all error paths.
src/keyring/keyring.hKeyringDaemon + keyring-cli: централизованное хранилище секретов, ключей и токенов для компонентов платформы. Три уровня API. Каждый живёт сам по себе: store не требует демона, демон не отдаёт сокет без готового store, клиент — сокет или in-process store, без «подключились наполовину».
src/keyring/keyring_add.cINV-KEYRING-01: key without TTL (ttl_sec == 0) automatically receives MAX_KEY_TTL. Enforced here before calling into keyring_store.
src/keyring/keyring_client.cклиентский слой (keyring_client_t) и низкоуровневый UNIX-socket транспорт (kr_wire_request), общий с cmd_keyring/keyring-cli. Клиент умеет работать в двух режимах: - socket: подключается к KeyringDaemon по UNIX socket; - local: если сокет недоступен и allow_local!=0 — напрямую использует
src/keyring/keyring_daemon.cпроцесс-демон KeyringDaemon. START → init security → init keyring store → install master key → ensure encryption key → create UNIX socket → READY → {client commands | TTL | periodic GC} → SHUTDOWN. LOCAL — не поднимает сокет (обслуживание идёт через глобальный store).
src/keyring/keyring_ipc.cреализация IPC-поверх-keyring (см. keyring_ipc.h).
src/keyring/keyring_ipc.hиспользование keyring как транспорта вместо классического IPC. Три механизма: 1. Mailbox (чистая keyring-схема): POST кладёт сообщение в хранилище как объект mbox::; RECV забирает самое старое сообщение и удаляет его. Никаких сокетов — получатель просто читает свой ящик из keyring.
src/keyring/keyring_mil.cIn MIL mode all keyring operations are audited and access to
src/keyring/keyring_revoke.cРеализация keyring / revoke
src/keyring/keyring_search.cРеализация keyring / search
src/keyring/keyring_store.cслой хранилища KeyringDaemon. Два backend'а под единым API: KERNEL — Linux kernel keyring через syscalls add_key/keyctl. Данные секретов живут в ядре; в процессе только метаданные. MEMORY — in-process таблица (fallback, если ядро недоступно). Страницы с секретами по возможности mlock() и wipe при удалении.
src/keyring/keyring_util.cобщие хелперы KeyringDaemon: - строковые имена enum'ов и kr_strerror; - hex encode/decode; - загрузка/сохранение конфига (INI-подобный); - глобальный in-process store (для local mode и SERVICE).
Контракты API / 3 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/platx_audit.h
/* include/platx/platx_audit.h — audit ring public API (task 6.45).
 *
 * This header is the ONE definition of the §MIL-9 audit record.  It used to
 * be one of five: src/audit/audit_ring.h carried a second, audit_ring.c and
 * audit_query.c carried two more, and tests/audit/t_audit_append.c a fifth
 * at 80 bytes while asserting 88.  Everything that needs the layout now
 * includes this file.
 *
 * Publication
 * -----------
 * A producer claims a slot, fills it, signs it, and only then publishes it
 * by storing a non-zero publish_seq with release ordering.  A reader that
 * sees publish_seq == 0 is looking at a slot that is being written and must
 * skip it.  Before publication existed, a reader could be handed a slot
 * between the cursor increment and the field writes, and the record it read
 * was whatever the previous wrap had left there — with no way to tell.
 *
 * publish_seq occupies the eight bytes that were `reserved[8]`.  The record
 * is still exactly 88 bytes, and the field is 8-byte aligned in the ring
 * because 88 is a multiple of 8.
 */
#define PLATX_AUDIT_RING_SIZE  8192u   /* §MIL-9 */
#define PLATX_AUDIT_RECORD_SZ  88u

_Static_assert(PLATX_AUDIT_RING_SIZE == 8192, "§MIL-9: audit ring must be 8192 slots");
typedef struct {
typedef struct __attribute__((packed)) {
    uint64_t ts_mono_us;    /*  0 */
    uint32_t type;          /*  8 */
    uint32_t flags;         /* 12 */
    uint64_t corr_id;       /* 16 */
    uint64_t source_id;     /* 24 */
    uint8_t  hmac[32];      /* 32 — §MIL-9: HMAC-SHA256; non-zero once signed */
    uint8_t  payload[16];   /* 64 */
    uint64_t publish_seq;   /* 80 — 0 = not published; else global seq + 1   */
} platx_audit_record_t; /* 88 bytes */

_Static_assert(sizeof(platx_audit_record_t) == 88,
    "platx_audit_record_t must be 88 bytes (§MIL-9)");

/* Offsets the signer and the publication protocol rely on.  Asserted rather
 * than assumed: a layout change that moved them silently would produce
 * records that verify against nothing. */
#define PLATX_AUDIT_HMAC_OFF     32u
#define PLATX_AUDIT_PAYLOAD_OFF  64u
#define PLATX_AUDIT_PUBSEQ_OFF   80u
/* The smallest record shape the signer accepts: everything up to and
 * including the payload.  The legacy check-audit set uses exactly this
 * shape, without publish_seq. */
#define PLATX_AUDIT_SIGNED_MIN   80u

/* Bytes fed to the MAC, in canonical big-endian order: ts, type, flags,
 * corr_id, source_id, payload.  Neither the hmac itself nor publish_seq is
 * covered — the first is the output, the second is set after signing. */
#define PLATX_AUDIT_MAC_INPUT_SZ 48u

/* Signing state, carried IN the record (JUDGE-GAP-1).
 *
 * Before these existed, audit_ring_append() discarded the signer's return
 * value and a record that could not be signed was appended looking exactly
 * like one that was: same zeroed HMAC field, same everything.  A consumer
 * reading the ring back had no per-record way to tell an authenticated
 * record from an unauthenticated one, and audit_sign_ready() is no help —
 * it is a process-wide flag read at a moment unrelated to when any given
 * record was written.
 *
 * Two bits, and the distinction between them matters as much as their
 * presence:
 *
 *   SIGNED       the MAC in this record was computed and is checkable.
 *   SIGN_FAILED  a key WAS installed and signing still failed.  That is a
 *                defect in progress, not a normal unsigned record, and it
 *                must not be quiet.
 *
 * Neither bit set, with a zero MAC, means "no key was installed" — the
 * ordinary non-MIL case, now stated rather than inferred.
 *
 * The bits live in the top of `flags`, which the MAC covers, and the ring
 * CLEARS them out of any caller-supplied flags before it uses them: a
 * producer that could set SIGNED itself would be able to declare its own
 * records authenticated, which is the same hole one level up. */
#define PLATX_AUDIT_F_SIGNED      0x80000000u
#define PLATX_AUDIT_F_SIGN_FAILED 0x40000000u
#define PLATX_AUDIT_F_SIGN_MASK   (PLATX_AUDIT_F_SIGNED | \
                                   PLATX_AUDIT_F_SIGN_FAILED)

/* Whether THIS record carries a checkable MAC.  A consumer that wants to
 * grade anything on the audit trail asks this per record. */
int platx_audit_is_signed(const platx_audit_record_t *r); /* inline interface */

/* Append — lock-free, never blocks > 1µs (INV-AUDIT-03).  Signs the record
 * and publishes it; a reader never observes it half-written. */
void     audit_ring_append(uint32_t type, uint32_t flags,
                           uint64_t corr_id, uint64_t source_id,
                           const uint8_t *payload, size_t plen);
uint64_t audit_ring_cursor(void);

/* Read one published record by its global sequence number.  Returns 0 on
 * success, -1 when the slot is unpublished, was overwritten during the
 * copy, or has already been wrapped past. */
int audit_ring_read(uint64_t seq, platx_audit_record_t *out);

/* Query.  Both skip unpublished slots. */
int audit_query_type(uint32_t type, platx_audit_record_t *out, int max);
int audit_query_corr(uint64_t corr_id, platx_audit_record_t *out, int max);

/* Export `count` published records starting at `from_seq`.  0 on success. */
int audit_export_to_file(const char *path, uint64_t from_seq, uint64_t count);

/* MIL signing (INV-AUDIT-02).  audit_sign_record signs in place: it writes
 * the 32 bytes at PLATX_AUDIT_HMAC_OFF.  audit_sign_verify returns 0 when
 * the record matches, non-zero otherwise. */
int audit_sign_set_key(const uint8_t *key, size_t len);
int audit_sign_record(void *rec, size_t rec_len);
int audit_sign_verify(const void *rec, size_t rec_len);

/* --- typed audit records (A4-P03-102) --------------------------------
 * The 16-byte payload is not free-form.  A record that cannot be tied back
 * to the mission, plan generation and step that caused it is a log line,
 * not an audit trail. */
typedef struct {
typedef struct __attribute__((packed)) {
    uint64_t mission_id;   /* 0 when the event has no mission             */
    uint32_t plan_gen;     /* replan generation                            */
    uint16_t step_id;      /* Plan IR step, 0 when not step-scoped         */
    uint16_t op;           /* platx_plan_op_t, 0 when not an action        */
} platx_audit_cause_t;     /* 16 bytes — exactly the payload */

_Static_assert(sizeof(platx_audit_cause_t) == 16,
    "cause payload must fill the 16-byte record payload exactly");

/* Event type codes for the mission and plan chain.  Public because the
 * consumers that read these records — the Judge, the export reader, the
 * traceability roll-up — live outside src/audit. */
#define AUDIT_TYPE_MISSION_ADMIT    0x4D414449u  /* "MADI" */
#define AUDIT_TYPE_MISSION_REFUSE   0x4D524546u  /* "MREF" */
#define AUDIT_TYPE_PLAN_SIGNED      0x50534947u  /* "PSIG" */
#define AUDIT_TYPE_PLAN_DISPATCH    0x50444953u  /* "PDIS" */
#define AUDIT_TYPE_STEP_RECEIPT     0x53524350u  /* "SRCP" */
#define AUDIT_TYPE_MISSION_REPLAN   0x4D52504Cu  /* "MRPL" */
#define AUDIT_TYPE_MISSION_TERMINAL 0x4D54524Du  /* "MTRM" */

/* ACT records (A4-P05).  A refusal is a first-class event: the reason an
 * action did NOT happen is the half of the trail an operator reads after an
 * incident, and a trail that only holds successes cannot answer "why did
 * nothing happen".  `flags` carries the exact platx_mact_rc_t, so the reason
 * is machine-readable and does not have to be recovered from prose. */
#define AUDIT_TYPE_ACT_REFUSE       0x41524546u  /* "AREF" */
#define AUDIT_TYPE_ACT_UNDO         0x41554E44u  /* "AUND" */
#define AUDIT_TYPE_ACT_KILL         0x414B494Cu  /* "AKIL" */

/* A2 request H9 (wave 4): a sustained drop in the accepted-frame rate on an
 * RA2C link.  Distinct from PROTO_DROP because the two are answered
 * differently: a protocol drop names one bad frame, a rate drop names a
 * link that is still nominally up while delivering less than it did. */
#define AUDIT_TYPE_RA2C_RATE_DROP   0x52415244u  /* "RARD" */

/* SENSE findings (A4-P08-360).  A recovery scenario has to be able to show
 * WHAT IT SAW before it acted, and a finding that lives only in an agent's
 * memory is not evidence anyone else can read.  `flags` carries the
 * dominant symptom, and the cause payload carries the mission, the service
 * incarnation the finding was about, how many observations it rests on, and
 * whether they confirmed anything — so a record that claims a cause always
 * names the count that justified it.
 *
 * Additive: a new code, no change to any existing one. */
#define AUDIT_TYPE_SENSE_FINDING    0x53464E44u  /* "SFND" */

void audit_ring_append_cause(uint32_t type, uint32_t flags,
                             uint64_t corr_id, uint64_t source_id,
                             const platx_audit_cause_t *cause);

/* Read the cause back out of a record.  0 on success. */
int audit_record_cause(const platx_audit_record_t *r,
                       platx_audit_cause_t *out);
include/platx/platx_keyring.h
/* platx_keyring.h — public API forwarding header for the keyring subsystem.
 *
 * Platform consumers include THIS header only; internal keyring.h is an
 * implementation detail and must not be included outside src/keyring/.
 *
 * Subsystem invariants:
 *   INV-KEYRING-01: ttl_sec == 0 on add → assigned MAX_KEY_TTL (86400 s)
 *   §SEC-3:         no malloc on hot path; store slots are static BSS
 *   §MIL-9:         MIL-mode gate blocks non-"user" types; every add audited
 */
/* ── Public API surface exposed to platform components ───────────────── */

/* INV-KEYRING-01 sentinel — default TTL assigned when caller passes 0 */
#define MAX_KEY_TTL  86400   /* 24 hours in seconds */

/* High-level add with automatic audit + INV-KEYRING-01 enforcement.
 * Prefer this over keyring_store_add() in platform code. */
kr_err_t keyring_add(keyring_store_t *st,
                     const char *type, const char *desc,
                     const uint8_t *data, size_t len,
                     time_t ttl_sec, int32_t *serial_out);

/* High-level search by description; audits cache misses. */
kr_err_t keyring_search(keyring_store_t *st, const char *desc,
                        int32_t *serial_out);

/* Search + read in one call; wipes+frees internal temp buffer. */
kr_err_t keyring_search_read(keyring_store_t *st, const char *desc,
                             uint8_t *buf, size_t buf_len, size_t *out_len);

/* Collect serials whose type starts with type_prefix; returns count. */
int      keyring_search_by_type(keyring_store_t *st, const char *type_prefix,
                                int32_t *serials, int max);

/* Revoke by serial; audits operation. */
kr_err_t keyring_revoke(keyring_store_t *st, int32_t serial);

/* Revoke first key matching description. */
kr_err_t keyring_revoke_by_desc(keyring_store_t *st, const char *desc);

/* GC pass — revoke all expired keys; calls keyring_store_gc internally. */
kr_err_t keyring_revoke_expired(keyring_store_t *st);

/* ── MIL-mode gate (§MIL-9) ──────────────────────────────────────────── */

/* Enable / disable MIL enforcement (default: disabled). */
void keyring_mil_set(int enabled);
int  keyring_mil_enabled(void);

/* MIL-mode add: blocks types other than "user"; audits rejections
 * with code 0x4B524D4Cu (KRML). */
kr_err_t keyring_mil_add(keyring_store_t *st,
                         const char *type, const char *desc,
                         const uint8_t *data, size_t len,
                         time_t ttl_sec, int32_t *serial_out);

/* MIL-mode read; audits every access. */
kr_err_t keyring_mil_read(keyring_store_t *st, int32_t serial,
                          uint8_t **out, size_t *out_len);

/* ── Audit type codes (for reference by callers) ─────────────────────── */
#define KR_AUDIT_ADD    0x4B524144u   /* KRAD — key added                  */
#define KR_AUDIT_SEARCH 0x4B525343u   /* KRSC — search cache miss          */
#define KR_AUDIT_REVOKE 0x4B525256u   /* KRRV — key revoked                */
#define KR_AUDIT_MILBLK 0x4B524D4Cu   /* KRML — MIL-mode block             */
include/platx/services/keyring_v1.h
/* platx/services/keyring_v1.h — public contract, not the implementation.
 * Modules #include this. They never #include src/keyring/keyring.h.
 */
#define PLAT_CAP_KEYRING     PLAT_NAME_STORAGE_KEYRING
#define PLAT_KEYRING_V1      0x00010001u

typedef struct plat_keyring_info {
    const char *desc;
    const char *type;
    size_t      size;
    int32_t     serial;
    int32_t     ttl_sec;
} plat_keyring_info_t;

typedef int (*plat_keyring_list_fn)(const plat_keyring_info_t *info, void *ud);

typedef struct plat_keyring_v1 {
    uint32_t struct_size;
    int (*get)(const char *desc, void *out, size_t cap, size_t *out_len);
    int (*put)(const char *desc, const void *key, size_t len);
    int (*del)(const char *desc);
    int (*put_ttl)(const char *desc, const void *data, size_t len, uint32_t ttl_sec);
    int (*get_alloc)(const char *desc, void **out, size_t *out_len);
    int (*take_alloc)(const char *desc, void **out, size_t *out_len);
    int (*list)(plat_keyring_list_fn fn, void *ud);
    int (*del_prefix)(const char *prefix);
} plat_keyring_v1_t;
22

lease

Ограниченные полномочия, контекст и поколение
src/lease/Управление и контракты2 файлов1 API headers

Lease — компактный AUTH-lite слой, связывающий разрешение с capability/operation, context и generation на ограниченное время. Он дополняет command authz и предотвращает перенос старого grant на новый process/provider.

Граница ответственности

  • Lease не хранит secret и не выполняет operation.
  • Grant привязан к actor, target/context generation, scope, expiry и policy version.
  • Default deny; wildcard scope ограничен privileged policy.

Устройство подсистемы

  • Lease record имеет opaque id, subject, action/capability, object context, generations, issued/expiry, uses и revoke reason.
  • Issue проходит policy engine и audit; check read-only/fast path validates all dimensions.
  • Optional single-use/max-use atomically decrements.
  • Context/provider loss/restart bulk-revokes indexed leases.

Поток работы

  • Actor issue request → policy → record/audit.
  • Operation check lease → validate generation/time/scope.
  • Consume optional use → allow/deny reason.
  • Expiry/revoke/loss → tombstone/audit.

Отказ и восстановление

  • Wall-clock rollback не продлевает lease: monotonic deadlines.
  • Race revoke/check uses atomic state/serialized shard.
  • Unknown policy version invalidates according to explicit rule.

Основные возможности

  • Issues short-lived authorization grants.
  • Generation binding prevents a lease surviving provider restart.
  • Explicit revoke/check/list supports operator reasoning.

Управление и диагностика

Корневые команды: lease. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / lease →
Состав подсистемы / 2 файлов
Файл / компонентНазначение и граница
src/lease/cmd_lease.cprint one grant decision. Does not execute a gadget. Issue / revoke / check talk to lease.c. A missing grant is DENIED plus a reason, never an empty OK. VALID is the operator word for a live grant; the table still stores OK. This file does not own a context
src/lease/lease.cAUTH-lite table. A grant is (capability, context, generation). Invariant: no live row means DENIED, never a quiet proceed. Context generation 0 is not an epoch. expires_at 0 means "until revoke", so a ttl=0 issue must store a non-zero expiry or it would live forever.
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/lease.h
/* platx/lease.h — a gadget is not a forever-right.
 *
 * Surface says a path exists. Gadget says how. A lease says the request
 * may proceed, now, in this context generation. No lease is DENIED, not
 * an empty OK. Capability belongs to a runtime inside (context_id, gen),
 * not to "C can snapshot".
 *
 * AUTH-lite / Capability Lease v1. Only process.snapshot:v1. Delegation
 * DAG, AI trust domains, and XIO authority_handle are not this slice.
 *
 * context_id + generation are a plat_context_handle_t. This file does
 * not own a second context table: when plat_context_explain is linked,
 * a dead or bumped context fails closed here too.
 */

#define PLAT_LEASE_CAP_PROCESS_SNAPSHOT "process.snapshot:v1"

#define PLAT_LEASE_NAME_MAX    64
#define PLAT_LEASE_REASON_MAX 160
#define PLAT_LEASE_SLOTS_MAX    8

/* Same numbers as plat_gadget_quality. Lease does not include gadget.h. */
#define PLAT_LEASE_Q_SNAPSHOT 0
#define PLAT_LEASE_Q_EXACT    1

typedef enum plat_lease_status {
    PLAT_LEASE_OK = 0,
    PLAT_LEASE_ERR_ARGS,
    PLAT_LEASE_ERR_DENIED,      /* no matching grant */
    PLAT_LEASE_ERR_STALE,       /* revoked, or context generation moved */
    PLAT_LEASE_ERR_EXPIRED,
    PLAT_LEASE_ERR_CONSUMED,    /* one-shot already used */
    PLAT_LEASE_ERR_FULL,
    PLAT_LEASE_ERR_UNKNOWN_CAP
} plat_lease_status_t;

typedef struct plat_lease {
    uint64_t          id;            /* 0 = invalid */
    char              capability[PLAT_LEASE_NAME_MAX];
    plat_context_id_t context_id;
    uint32_t          generation;
    uint64_t          issued_at;     /* CLOCK_MONOTONIC ms */
    uint64_t          expires_at;    /* 0 = until revoke */
    int               has_min_quality;
    int               min_quality;   /* SNAPSHOT / EXACT when has_min_quality */
    int               one_shot;
    int               uses_left;     /* 1→0 on one-shot; -1 unlimited */
} plat_lease_t;

typedef struct plat_lease_issue {
    const char           *capability;
    plat_context_handle_t context;   /* id 0 / gen 0 refused */
    int64_t               ttl_ms;    /* <0 until revoke; 0 already dead; >0 */
    int                   has_min_quality;
    int                   min_quality;
    int                   one_shot;  /* use_count=1 */
} plat_lease_issue_t;

typedef struct plat_lease_decision {
    plat_lease_status_t status;
    uint64_t            lease_id;    /* 0 when none */
    char                reason[PLAT_LEASE_REASON_MAX];
} plat_lease_decision_t;

int  plat_lease_init(void);
void plat_lease_fini(void);

/* Grant. Unknown capability and generation 0 refuse. Does not execute. */
int plat_lease_issue(const plat_lease_issue_t *req, plat_lease_t *out);

/* Live grant becomes STALE. Unknown id is not success. */
int plat_lease_revoke(uint64_t id);

int plat_lease_get(uint64_t id, plat_lease_t *out);

/* Explain-deny: always fills `out` when non-NULL. Does not consume. */
int plat_lease_check(const char *capability,
                     uint64_t context_id,
                     uint64_t generation,
                     plat_lease_decision_t *out);

/*
 * Thin hook for cmd/gadget later:
 *   if (plat_lease_allow(cap, ctx, gen) != 0) refuse;
 * 0 only when a live lease matches. Else -1, errno=EPERM (EINVAL on args).
 * One-shot is consumed here: that was the successful request grant.
 */
int plat_lease_allow(const char *capability,
                     uint64_t context_id,
                     uint64_t generation);

/* Same gate, typed handle. */
int plat_lease_allow_h(const char *capability, plat_context_handle_t h);

/*
 * In-flight result: `generation` is the context epoch at accept time.
 * If it no longer matches the lease, the payload is STALE even if
 * allow() already passed under the old gen.
 */
int plat_lease_accept_result(uint64_t id, uint64_t generation,
                             plat_lease_decision_t *out);

const char *plat_lease_status_str(plat_lease_status_t s);
23

log

Журналы исполнения, редактирование чувствительных полей и ротация
src/log/Наблюдение и исследование6 файлов1 API headers

Log даёт человеку оперативную диагностику с severity, component, correlation, sinks, rotation и redaction. В отличие от Audit, log может быть sampling/drop-oriented и не является доказательным журналом.

Граница ответственности

  • No secrets before formatting; redaction central.
  • Logging from locks/error paths bounded and nonblocking according to severity policy.
  • Module cannot register arbitrary global sink without owner lifecycle.

Устройство подсистемы

  • Structured record содержит time/severity/component/owner/correlation/message и bounded fields.
  • Formatter adapters text/JSON; sinks stderr/file/ring with independent health/counters.
  • Emergency path writes minimal async-safe record when normal logger unavailable.

Поток работы

  • Callsite constructs typed/bounded fields.
  • Severity/filter/redaction.
  • Fan-out to healthy sinks.
  • Sink errors update status/audit threshold.

Отказ и восстановление

  • Recursive logging guarded; sink failure cannot recurse indefinitely.
  • Slow sink bounded queue/drop counters.
  • Rotation/disk full explicit degraded.
  • Malformed UTF/input escaped and bounded.

Основные возможности

  • Central severity and sink control.
  • Redaction protects sensitive values before output.
  • Rotation/search/tail support operations and audits.
Архитектурные детали и инварианты

Overview

Thread-safe multi-sink logging with per-subsystem level control and a

lock-based in-memory ring (1 024 entries).

Invariants

- **INV-LOG-01**: every LOG_FATAL (CRITICAL) entry is forwarded to the audit

ring via log_audit_sink_register() (type 0x4C4F4700).

Levels

TRACE(0) DEBUG(1) INFO(2) WARN(3) ERROR(4) FATAL(5)

Управление и диагностика

Корневые команды: log. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / log →
Состав подсистемы / 6 файлов
Файл / компонентНазначение и граница
src/log/cmd_log.cCLI namespace "log" for the platform logging system. log level show global level log level --level= set global level log level --subsys= --level= set per-subsystem level log tail [--count=N] [--level=warn] show recent ring-buffer entries
src/log/log.cintegrated platform logging implementation. - Single mutex protects the entire state (ring buffer + sink dispatch). Sink callbacks are called under the lock so output lines don't interleave. Sinks must not call log_write() themselves (deadlock). - Level filtering happens before the lock for a fast early-exit path.
src/log/log.hintegrated platform logging. Thread-safe, multi-sink, per-subsystem log levels. No external dependencies — libc + pthread only. LOG_I("mesh", "peer %s connected", peer_id); LOG_E("mbus", "queue full (dropped %d msgs)", n); The default stderr sink is added automatically by log_init().
src/log/log_export.cINV-LOG-01: every LOG_FATAL (CRITICAL) entry is forwarded to the audit ring via log_audit_sink, which must be registered at startup.
src/log/log_redact.cLog redaction policy. STAB-132: redact_psk, redact_token, redact_path, redact_payload.
src/log/log_rotate.cLog rotation with ENOSPC/EACCES handling. STAB-133: ENOSPC, EACCES, rename_atomic, rotation_lock.
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/log_severity.h
/* platx/log_severity.h — Log severity + Audit outcome bridge (INT-113). */
typedef enum {
    PLAT_LOG_TRACE  = 0,
    PLAT_LOG_DEBUG  = 1,
    PLAT_LOG_INFO   = 2,
    PLAT_LOG_WARN   = 3,
    PLAT_LOG_ERROR  = 4,
    PLAT_LOG_FATAL  = 5,
} plat_log_level_t;

/* Map platform log level to audit level. */
audit_level_t plat_log_to_audit_level(plat_log_level_t lvl);

/* Write a log entry; bridges to audit if level >= INFO. 0/-1. */
int plat_log_write(plat_log_level_t lvl, const char *comp,
                   const char *msg);

/* Set minimum log level for this component (thread-safe). */
void plat_log_set_min(plat_log_level_t min_lvl);
24

mbus

Асинхронные акторы, сообщения и почтовые ящики
src/mbus/Связь и ввод-вывод14 файлов2 API headers

MBus — asynchronous actor/message bus для тяжёлых module messages и optional network delivery. Он обеспечивает bounded mailboxes, backpressure и at-least-once semantics там, где маленького Core event ring недостаточно.

Граница ответственности

  • Core никогда не зависит от MBus для lifecycle correctness. Bridge только Core→MBus.
  • Actor callbacks не удерживают bus global lock и создаются как managed tasks.
  • At-least-once требует idempotency/dedup у consumers; exactly-once не обещается.

Устройство подсистемы

  • Actor registry owner-scoped; mailbox bounded с выбранной overflow policy.
  • Message envelope version, id, correlation, sender/receiver owner, deadline, attempt и payload type/size.
  • Dispatcher fairness prevents one mailbox monopolizing worker.
  • Network adapter имеет spool/ack/retry/dedup window и отдельный recovery state.

Поток работы

  • Producer validate/enqueue.
  • Mailbox scheduling → actor callback.
  • Ack/result/dead-letter.
  • Optional network encode/send/ack/retry.

Отказ и восстановление

  • Mailbox full returns backpressure/reject, no unbounded allocation.
  • Actor crash produces owner failure event and unacked messages policy.
  • Network duplicate delivered with stable message id.
  • Shutdown drains or explicitly dead-letters within deadline.

Основные возможности

  • Bounded mailbox and explicit backpressure.
  • Network path targets at-least-once delivery.
  • One-way Core event-to-MBus bridge avoids making Core depend on MBus.
Архитектурные детали и инварианты

Overview

MBUS is the platform's asynchronous message backbone. It has two layers:

1. **MPSC ring** (platx_mbus.h) — 65536 × 72-byte static BSS ring (§SEC-3: no malloc). Used by XIO and HADES producers; consumed exclusively by the Policy executor.

2. **Actor bus** (mbus.h) — higher-level actor model with named mailboxes, topics, request/reply, and remote gateways.

Invariants

INV-MBUS-01 | Ring overflow → oldest event dropped silently. Producer never blocks.

INV-MBUS-02 | Consumer lag > MBUS_WARN_THRESHOLD (25% capacity) → watermark warns; caller raises ThreatLevel.

§SEC-3 | No malloc in publish/consume hot path. All state in static BSS.

Subsystems

- mbus_watermark.c — lag monitoring, no malloc

- mbus_stats.c — atomic ring counters (published/consumed/dropped/warn_events)

Управление и диагностика

Корневые команды: mbus. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / mbus →
Состав подсистемы / 14 файлов
Файл / компонентНазначение и граница
src/mbus/cmd_mbus.cnamespace `mbus`: интроспекция и управление актор-шиной из REPL/IPC/batch/скриптов, плюс интеграция в контекст платформы. mbus status сводка: узел, воркеры, акторы, счётчики mbus actors список акторов (mailbox, обработано, drop)
src/mbus/mbus.cреализация асинхронной шины сообщений и актор-модели. Синхронизация: одна шина-мьютекс (g bus->lock) защищает реестр акторов, их mailbox'ы, состояние, run-queue, таблицу ожидающих запросов и счётчики. Обработчики акторов и колбэки future вызываются ВНЕ этого мьютекса, поэтому
src/mbus/mbus.hасинхронная шина сообщений и актор-модель платформы platx. Единая асинхронная точка взаимодействия подсистем. Вместо прямых синхронных вызовов (platform_require → vtable) и сериализации всего через глобальный platform_dispatch_lock, модули общаются сообщениями:
src/mbus/mbus_actor.cMBus actor lifecycle with generation binding. STAB-122: Actors bound to owner generation; drain and unsubscribe on stop; stale generation actors rejected. A2-P04: this file previously declared its own `mbus_actor_t`, colliding with the opaque handle in mbus.h, and called five functions that no header
src/mbus/mbus_actor.hgeneration-guarded actor endpoint. An actor name can be reused. What must not be reused is the delivery right: a message addressed to generation N of an endpoint may never be handed to generation N+1, even though both answer to the same id.
src/mbus/mbus_event.cplat_event → mbus "plat.event". One way: no mbus→event path. Attach is idempotent: live slots stay, no second generation. A2-P04-187: the fan-out used to publish the bare event id and throw the payload away, so every typed fact arrived on the bus as an untyped number
src/mbus/mbus_mailbox.cMBus bounded mailbox with backpressure. STAB-121: Bounded queue; backpressure policy rejects on overflow without blocking the Core thread. A2-P04-179: the fullness test now compares against the capacity this mailbox was actually created with. The previous version compared against
src/mbus/mbus_mailbox.hbounded actor mailbox primitive. The bound is the capacity the owner was configured with at init, not a compile-time constant: a mailbox created with a load-time policy of 8 slots must refuse the 9th message, not the 4097th. MBUS_MAILBOX_MAX is only the
src/mbus/mbus_net.cузловой линк (at-least-once) и центральный брокер-релей.
src/mbus/mbus_net.hсетевой слой актор-шины: узловой линк и центральный брокер. ТОПОЛОГИЯ (звезда с центральным брокером) node "n1" ──┐ ┌── node "n3" │ ┌──────────────────┐ │ node "n2" ──┼───TCP──▶ mbus_broker ◀──TCP─┤ │ │ relay node→node │ │
src/mbus/mbus_platform.hинтеграция актор-шины в контекст платформы platx. Шина живёт как единый сервис процесса: лениво создаётся, стартует пул воркеров, публикуется в реестре сервисов под именем "mbus" и снимается единым teardown платформы (platform_register_cleanup). Другие подсистемы получают шину без прямой линковки:
src/mbus/mbus_ring.cMPSC event ring declared by platx/platx_mbus.h. A2-P04-190/191/192/194: the public header has declared mbus_ring_publish() and mbus_ring_consume() since wave 2, but no translation unit in the tree defined them — every tests/mbus fixture supplied its own private ring, so
src/mbus/mbus_stats.cSeparate from mbus_get_stats() (actor stats): this module tracks the
src/mbus/mbus_watermark.cTracks the high-water mark of unread events in the MPSC ring. When lag exceeds MBUS_WARN_THRESHOLD, a flag is set that the caller (Policy executor) can check to raise ThreatLevel.
Контракты API / 2 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/mbus_ra2c_adapter.h
/* platx/mbus_ra2c_adapter.h — MBus → protocol.ra2c:v1, not internal RA2C.
 *
 * Bounded queue. Backpressure / deadline / correlation id are mandatory.
 * No RA2C provider → explicit degradation, never weak-success.
 *
 * A2-P04: enqueue acceptance and remote delivery are two different facts and
 * are reported separately.  route() returning OK means "admitted to a bounded
 * queue", never "the peer has it".  Delivery is observable only through
 * plat_mbus_ra2c_stats().delivered.
 */
#define PLAT_MBUS_RA2C_TOPIC_MAX  64
#define PLAT_MBUS_RA2C_PAY_MAX   256
#define PLAT_MBUS_RA2C_QMAX       32
#define PLAT_MBUS_RA2C_REASON    128

typedef enum {
    PLAT_MBUS_RA2C_OK           =  0,
    PLAT_MBUS_RA2C_NOLINK       = -1, /* protocol.ra2c absent or no push */
    PLAT_MBUS_RA2C_ARGS         = -2,
    PLAT_MBUS_RA2C_FULL         = -3, /* backpressure: queue full */
    PLAT_MBUS_RA2C_EXPIRED      = -4,
    PLAT_MBUS_RA2C_DEGRADED     = -5, /* provider health not OK */
    PLAT_MBUS_RA2C_ERROR        = -6,
    PLAT_MBUS_RA2C_STOPPED      = -7, /* bridge shut down; no new effects */
} plat_mbus_ra2c_status_t;

/* Observable bridge accounting.  A2-P04-175: the balance
 *   accepted == delivered + expired + dropped + queued
 * must hold for any workload; a violation is a lost or duplicated message.
 * A2-P04-192: every counter is monotonic across a run except `queued`
 * and `high_water` (depth gauges) which are reset only by reset(). */
typedef struct {
    uint64_t accepted;        /* route() returned OK                        */
    uint64_t rejected_args;   /* route() returned ARGS                      */
    uint64_t rejected_nolink; /* route() returned NOLINK                    */
    uint64_t rejected_full;   /* route() returned FULL (backpressure)       */
    uint64_t rejected_stopped;/* route() returned STOPPED                   */
    uint64_t delivered;       /* provider push() returned 0                 */
    uint64_t expired;         /* deadline elapsed before push               */
    uint64_t dropped;         /* removed without send (shutdown / merge)    */
    uint64_t degraded;        /* push() failed or provider lost mid-drain   */
    uint64_t clock_faults;    /* monotonic clock unusable — fail closed     */
    uint64_t provider_epochs; /* observed distinct provider vtables         */
    uint64_t full_notices;    /* bounded FULL facts published on MBus       */
    unsigned queued;          /* current depth                              */
    unsigned high_water;      /* max depth seen since reset                 */
} plat_mbus_ra2c_stats_t;

/* Enqueue one event. corr_id==0 or deadline_ms==0 → ARGS.
 * Missing RA2C vtable / push → NOLINK. Full queue → FULL (no drop). */
int plat_mbus_ra2c_route(const char *topic,
                         const void *payload, size_t len,
                         uint64_t corr_id, uint32_t deadline_ms);

/* Drain due items through protocol.ra2c push. Expired items are not sent.
 * Provider lost mid-drain → DEGRADED, remaining stay queued. */
int plat_mbus_ra2c_drain(void);

/* Forward RA2C output onto MBus. Missing emit → NOLINK, not 0. */
int plat_ra2c_mbus_forward(const char *topic,
                           const void *payload, size_t len);

unsigned plat_mbus_ra2c_queued(void);
void     plat_mbus_ra2c_reset(void); /* test/teardown: drop queued, no send */

/* A2-P04-171: controlled shutdown. After this call the bridge accepts
 * nothing and sends nothing; queued items are dropped and counted.  It is
 * idempotent and does not free the caller's memory (queue is static BSS). */
void plat_mbus_ra2c_shutdown(void);

/* A2-P04-193: public degradation view.  OK / NOLINK / DEGRADED / STOPPED
 * without any src/transport include. */
int plat_mbus_ra2c_health(void);

/* A2-P04-175/192: snapshot of the counters above.  Bounded work, no locks
 * held across a caller callback. */
void plat_mbus_ra2c_stats(plat_mbus_ra2c_stats_t *out);
include/platx/platx_mbus.h
/* include/platx/platx_mbus.h — public MPSC mbus ring API (task 2.43).
 *
 * MPSC ring: 65536 × 72 bytes static BSS (§SEC-3: no malloc).
 * INV-MBUS-01: ring overflow → oldest event dropped (producer never blocks).
 * INV-MBUS-02: consumer lag > MBUS_WARN_THRESHOLD → ThreatLevel raise.
 */

/* ── ring geometry ──────────────────────────────────────────────────── */
#define MBUS_RING_CAPACITY   65536u      /* slot count                   */
#define MBUS_EVENT_SIZE      72u         /* bytes per slot (§SEC-3)      */
#define MBUS_WARN_THRESHOLD  16384u      /* INV-MBUS-02: 25% capacity    */

/* ── event structure: exactly 72 bytes ─────────────────────────────── */
typedef struct __attribute__((packed)) platx_mbus_event {
    uint32_t  type;           /* event class                             */
    uint32_t  flags;          /* MBUS_EF_ENCRYPTED=0x1, MIL=0x2         */
    uint64_t  ts_mono_us;     /* CLOCK_MONOTONIC timestamp (µs)          */
    uint64_t  source_id;      /* producer identity                       */
    uint64_t  seq;            /* monotonic producer sequence             */
    uint8_t   payload[40];    /* inline payload                          */
} platx_mbus_event_t;

_Static_assert(sizeof(platx_mbus_event_t) == 72,
               "platx_mbus_event_t must be exactly 72 bytes");

/* ── event type codes ───────────────────────────────────────────────── */
#define MBUS_EV_NONE         0u
#define MBUS_EV_XIO_IO       1u    /* XIO I/O completion                */
#define MBUS_EV_HADES_ALERT  2u    /* HADES detection                   */
#define MBUS_EV_POLICY       3u    /* Policy executor decision           */
#define MBUS_EV_TRANSPORT    4u    /* RA2C transport event               */
#define MBUS_EV_MESH_HEALTH  5u    /* Mesh health change                 */
#define MBUS_EV_FABRIC       6u    /* Fabric operation                   */
#define MBUS_EV_AUDIT        7u    /* Audit ring event (§MIL-9)          */

/* ── event flags ────────────────────────────────────────────────────── */
#define MBUS_EF_ENCRYPTED    0x0001u
#define MBUS_EF_MIL          0x0002u
#define MBUS_EF_OVERFLOW_DROP 0x0004u  /* set on dropped-oldest copy     */

/* ── ring publish / consume ─────────────────────────────────────────── */
/* Push event into ring. INV-MBUS-01: if full, drops oldest silently.   */
int  mbus_ring_publish(const platx_mbus_event_t *ev);
/* Pop up to `max` events. Returns count (0 = empty).                   */
int  mbus_ring_consume(platx_mbus_event_t *out, int max);

/* ── watermark API ──────────────────────────────────────────────────── */
typedef struct {
    uint32_t lag_current;
    uint32_t lag_hwm;
    uint32_t warn_trips;
    int      warn_active;
    uint32_t threshold;
} mbus_watermark_t;

void mbus_watermark_update(uint32_t current_lag);
int  mbus_watermark_warn_active(void);
void mbus_watermark_get(mbus_watermark_t *out);
void mbus_watermark_reset_hwm(void);

/* ── ring statistics API ─────────────────────────────────────────────── */
typedef struct {
    uint64_t published;
    uint64_t consumed;
    uint64_t dropped;
    uint64_t warn_events;
    uint32_t in_flight;
} mbus_ring_stats_t;

void mbus_stats_inc_published(void);
void mbus_stats_inc_consumed(void);
void mbus_stats_inc_dropped(void);
void mbus_stats_inc_warn(void);
void mbus_ring_stats_get(mbus_ring_stats_t *out);
void mbus_ring_stats_reset(void);
25

memfd

Управляемые анонимные объекты памяти
src/memfd/Исполнение и расширения5 файлов7 API headers

Memfd управляет anonymous memory objects как owner-scoped resources: create/write/read/map/protect/seal/truncate/save/load/close. Он должен централизовать executable-memory policy и не оставлять raw fd вне accounting.

Граница ответственности

  • Core resource registry владеет fd; memfd registry хранит domain metadata.
  • Executable mapping/exec operations research/policy gated.
  • Raw getfd либо выдаёт duplicated scoped fd, либо lease; internal fd не отдаётся без transfer.

Устройство подсистемы

  • Object state описывает size, seals, mappings/protections, owner/generation, content provenance и closed flag.
  • State machine проверяет допустимость write/truncate/seal/mprotect; kernel result reconciled с metadata.
  • Mapping record отдельный resource, закрывается до fd.
  • Save/load use bounded streams/XIO and optional integrity hash.

Поток работы

  • Create → claim fd/object row.
  • Mutate/read/map operations validate owner/state.
  • Seal transitions to immutable constraints.
  • Close → unmap/revoke handles → close resource → tombstone.

Отказ и восстановление

  • Stale handle after close/restart rejected.
  • Partial save/load has temp/rollback and size limits.

Основные возможности

  • Thread-safe object registry with fd ownership.
  • Explicit state transitions for seal, mprotect, truncate and close.
  • Save/load/hexdump/cdata utilities support experiments.
Архитектурные детали и инварианты

Назначение

Хранение ключей, токенов и других секретов EDR-агента в анонимных файлах ядра

с защитой от перезаписи через F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW.

Жизненный цикл секрета

1. memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING) → fd

2. ftruncate(fd, size) → выделить место

3. mmap(PROT_WRITE, MAP_SHARED) → записать секрет → munmap

4. fcntl(F_ADD_SEALS, F_SEAL_WRITE|SHRINK|GROW) → заморозить

5. platx_memfd_secret_read(fd, buf, max) → pread(fd, ...)

После запечатывания любой mmap(MAP_SHARED|PROT_WRITE) возвращает MAP_FAILED (EPERM).

Управление и диагностика

Корневые команды: memfd. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / memfd →
Состав подсистемы / 5 файлов
Файл / компонентНазначение и граница
src/memfd/artifact_buffer_ext_linux.incРеализация artifact / buffer / ext / linux
src/memfd/cmd_memfd.cthread-safe memfd manager as universal console commands. Maintains its own in-process memfd table (g_tab), protected by a POSIX mutex so that multiple threads (or IPC_CONCURRENT children sharing the address space) can call these handlers safely. Reference convention (user-visible):
src/memfd/memfd_cli_json_linux.incPresentation adapter for legacy Linux objects; keeps their identity/ABI.
src/memfd/memfd_seal.cmemfd seal / mprotect / truncate / close state machine. STAB-118: Verify that after sealing a memfd with F_SEAL_WRITE, all subsequent mutation attempts are refused (post_seal check), mprotect is applied to enforce PROT_READ-only access, and the sealed_state is tracked through the object's lifetime.
src/memfd/memfd_secret.cРеализация memfd / secret
Контракты API / 7 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/artifact_buffer_ext.h
/* Versioned data-plane extension. artifact.buffer:v1 layout is unchanged. */

#define PLAT_CAP_ARTIFACT_BUFFER_EXT "artifact.buffer.ext"
#define PLATX_CAPABILITY_ID_ARTIFACT_BUFFER_EXT UINT64_C(0x4152544255460002)
#define PLAT_ARTIFACT_BUFFER_EXT_V1 0x00010000u
#define PLAT_ARTIFACT_BUFFER_EXT_ABI 1u
#define PLAT_ARTIFACT_BUFFER_GUARANTEE_API 1u
#define PLAT_ARTIFACT_BUFFER_GUARANTEE_KERNEL 3u
typedef struct plat_artifact_buffer_info {
    plat_artifact_buffer_result_t result;
    uint64_t capacity;
    uint32_t frozen;
    uint32_t guarantee;
    uint32_t mapped;
    uint32_t protection; /* portable bits: read=1, write=2, execute=4 */
    uint64_t mapping_size;
    /* Local C ABI only: never serialize this address. Valid until close.
     * Callers synchronize direct access against protect/freeze/close. */
    void *mapping_address;
    uint32_t mapping_shared;
    char name[PLAT_ARTIFACT_BUFFER_NAME_MAX];
} plat_artifact_buffer_info_t;
typedef struct plat_artifact_buffer_ext_v1 {
    uint32_t struct_size, abi_version;
    int (*read)(plat_artifact_buffer_ref_t, void *, size_t, uint64_t,
                size_t *, plat_artifact_buffer_result_t *);
    int (*resize)(plat_artifact_buffer_ref_t, uint64_t,
                  plat_artifact_buffer_result_t *);
    int (*freeze)(plat_artifact_buffer_ref_t, plat_artifact_buffer_info_t *);
    int (*stat)(plat_artifact_buffer_ref_t, plat_artifact_buffer_info_t *);
    /* Enumeration is owner scoped. Cursor is opaque, NOT an artifact id. */
    int (*next)(plat_artifact_buffer_owner_t, uint64_t *,
                plat_artifact_buffer_info_t *);
    int (*map)(plat_artifact_buffer_ref_t, uint32_t, uint32_t, uint64_t,
               plat_artifact_buffer_info_t *); /* shared=1, private/COW=0 */
    int (*protect)(plat_artifact_buffer_ref_t, uint32_t,
                   plat_artifact_buffer_info_t *);
} plat_artifact_buffer_ext_v1_t;
const plat_artifact_buffer_ext_v1_t *plat_artifact_buffer_extension_v1(void);
include/platx/artifact_memfd.h
/* platx/artifact_memfd.h — typed anonymous-artifact API.
 *
 * This is the C contract behind MSX artifact.memfd.*.  The legacy memfd CLI
 * remains a compatibility surface; typed artifacts stay in the same manager
 * table but are fenced by an opaque lease and an exact owner generation.
 * No operation exposes the process-local fd.
 */

#define PLAT_ARTIFACT_MEMFD_ABI          1u
#define PLAT_CAP_ARTIFACT_MEMFD          "artifact.memfd"
#define PLAT_ARTIFACT_MEMFD_V1           0x00010000u
#define PLAT_ARTIFACT_MEMFD_NAME_MAX     96u
#define PLAT_ARTIFACT_MEMFD_REASON_MAX  160u
#define PLAT_ARTIFACT_MEMFD_DEFAULT_MAX (16u * 1024u * 1024u)
#define PLAT_ARTIFACT_MEMFD_HARD_MAX    (64u * 1024u * 1024u)

/* Public seal bits intentionally do not copy kernel numeric values. */
#define PLAT_ARTIFACT_SEAL_SHRINK       (1u << 0)
#define PLAT_ARTIFACT_SEAL_GROW         (1u << 1)
#define PLAT_ARTIFACT_SEAL_WRITE        (1u << 2)
#define PLAT_ARTIFACT_SEAL_FUTURE_WRITE (1u << 3)
#define PLAT_ARTIFACT_SEAL_FINAL        (1u << 4)
#define PLAT_ARTIFACT_SEAL_ALL          (PLAT_ARTIFACT_SEAL_SHRINK | \
                                         PLAT_ARTIFACT_SEAL_GROW | \
                                         PLAT_ARTIFACT_SEAL_WRITE | \
                                         PLAT_ARTIFACT_SEAL_FUTURE_WRITE | \
                                         PLAT_ARTIFACT_SEAL_FINAL)

typedef enum plat_artifact_memfd_reason {
    PLAT_ARTIFACT_MEMFD_OK = 0,
    PLAT_ARTIFACT_MEMFD_INVALID_ARGUMENT,
    PLAT_ARTIFACT_MEMFD_POLICY_DENIED,
    PLAT_ARTIFACT_MEMFD_OWNER_UNAVAILABLE,
    PLAT_ARTIFACT_MEMFD_OWNER_STALE,
    PLAT_ARTIFACT_MEMFD_LEASE_REQUIRED,
    PLAT_ARTIFACT_MEMFD_LEASE_STALE,
    PLAT_ARTIFACT_MEMFD_NOT_FOUND,
    PLAT_ARTIFACT_MEMFD_TABLE_FULL,
    PLAT_ARTIFACT_MEMFD_SIZE_LIMIT,
    PLAT_ARTIFACT_MEMFD_UNSUPPORTED,
    PLAT_ARTIFACT_MEMFD_CREATE_FAILED,
    PLAT_ARTIFACT_MEMFD_IO_FAILED,
    PLAT_ARTIFACT_MEMFD_SEALED,
    PLAT_ARTIFACT_MEMFD_SEAL_FAILED,
    PLAT_ARTIFACT_MEMFD_BUSY,
    PLAT_ARTIFACT_MEMFD_RESOURCE_FAILED
} plat_artifact_memfd_reason_t;

typedef struct plat_artifact_memfd_ref {
    uint64_t     artifact_id;
    uint64_t     lease_id;
    plat_owner_t owner;
} plat_artifact_memfd_ref_t;

typedef struct plat_artifact_memfd_create_req {
    uint32_t     abi_version;
    uint32_t     struct_size;
    const char  *name;
    size_t       initial_size;
    size_t       max_size;
    plat_owner_t owner;
} plat_artifact_memfd_create_req_t;

typedef struct plat_artifact_memfd_result {
    uint32_t                    abi_version;
    uint32_t                    struct_size;
    uint32_t                    reason_code; /* plat_artifact_memfd_reason_t */
    uint32_t                    applied;     /* world changed in this call */
    plat_artifact_memfd_ref_t   ref;
    uint64_t                    size;
    uint32_t                    seals;       /* PLAT_ARTIFACT_SEAL_* */
    uint32_t                    closed;
    char                        reason[PLAT_ARTIFACT_MEMFD_REASON_MAX];
} plat_artifact_memfd_result_t;

/* Capability registry contract: artifact.memfd @ PLAT_ARTIFACT_MEMFD_V1.
 * Consumers require this vtable rather than binding to the implementation.
 * The direct symbols below remain the in-tree provider entry points. */
typedef struct plat_artifact_memfd_v1 {
    uint32_t struct_size;
    int (*create)(const plat_artifact_memfd_create_req_t *req,
                  plat_artifact_memfd_result_t *out);
    int (*write)(plat_artifact_memfd_ref_t ref,
                 const void *data, size_t bytes, uint64_t offset,
                 size_t max_size, plat_artifact_memfd_result_t *out);
    int (*seal)(plat_artifact_memfd_ref_t ref, uint32_t seals,
                plat_artifact_memfd_result_t *out);
    int (*close)(plat_artifact_memfd_ref_t ref,
                 plat_artifact_memfd_result_t *out);
    int (*release_owner)(plat_owner_t owner);
    const char *(*reason_name)(uint32_t reason);
} plat_artifact_memfd_v1_t;

#define PLAT_ARTIFACT_MEMFD_V1_BASE_SIZE \
    (offsetof(plat_artifact_memfd_v1_t, reason_name) + \
     sizeof(((plat_artifact_memfd_v1_t *)0)->reason_name))

const char *plat_artifact_memfd_reason_name(uint32_t reason);

int plat_artifact_memfd_create(const plat_artifact_memfd_create_req_t *req,
                               plat_artifact_memfd_result_t *out);
int plat_artifact_memfd_write(plat_artifact_memfd_ref_t ref,
                              const void *data, size_t bytes, uint64_t offset,
                              size_t max_size,
                              plat_artifact_memfd_result_t *out);
int plat_artifact_memfd_seal(plat_artifact_memfd_ref_t ref, uint32_t seals,
                             plat_artifact_memfd_result_t *out);
int plat_artifact_memfd_close(plat_artifact_memfd_ref_t ref,
                              plat_artifact_memfd_result_t *out);

/* Exact-generation teardown.  Used by the MSX lifecycle; generation 0 is a
 * no-op, matching the platform resource contract. */
int plat_artifact_memfd_release_owner(plat_owner_t owner);
include/platx/memfd_cap.h
/* platx/memfd_cap.h — Memfd manager as capability provider (INT-091).
 * Thin glue: capability lease gate → memfd_create → optional seals.
 * Does NOT touch elfload.c, msx_engine.c, or vault.c.
 */
#define PLAT_MEMFD_CAP_NAME   "memfd.alloc:v1"
#define PLAT_MEMFD_CAP_REASON 128

typedef enum {
    PLAT_MEMFD_CAP_OK     = 0,
    PLAT_MEMFD_CAP_NOLINK = 1,  /* memfd_create not linked */
    PLAT_MEMFD_CAP_DENIED = 2,  /* lease gate refused */
    PLAT_MEMFD_CAP_ERROR  = 3,
} plat_memfd_cap_status_t;

typedef struct {
    plat_memfd_cap_status_t status;
    int     fd;           /* -1 on failure */
    size_t  size;
    int     sealed;       /* 1 if seals applied */
    char    reason[PLAT_MEMFD_CAP_REASON];
} plat_memfd_cap_result_t;

/* Gate via lease (cap=PLAT_MEMFD_CAP_NAME), then memfd_create + ftruncate.
 * seal_flags=0 → no seals.  lease_id=0 → skip gate (internal).
 * Returns 0 on success, -1 on failure (result.reason filled). */
int plat_memfd_cap_alloc(uint64_t ctx_id, uint32_t ctx_gen,
                         uint64_t lease_id, size_t size,
                         unsigned int seal_flags,
                         plat_memfd_cap_result_t *out);
include/platx/memfd_cli_format.h
#define PLAT_MEMFD_HELP \
 "memfd — anonymous memory-file manager\n\n" \
 "  memfd create   <name> [cloexec] [sealing] [exec] [noexec]\n" \
 "  memfd write    memfd:<ref> [off] <@file|hex|[cdata]>\n" \
 "  memfd read     memfd:<ref> [start [end]]\n" \
 "  memfd hexdump  memfd:<ref> [start [end]]      (alias for read)\n" \
 "  memfd truncate memfd:<ref> <size> [grow]\n" \
 "  memfd chmod    memfd:<ref> <octal-mode>\n" \
 "  memfd seal     memfd:<ref> <seal,grow,write,shrink,all,...>\n" \
 "  memfd mmap     memfd:<ref> <rwx> [private|shared] [len]\n" \
 "  memfd mprotect memfd:<ref> <rwx>\n" \
 "  memfd list     [filter]\n" \
 "  memfd stat     memfd:<ref>\n" \
 "  memfd close    memfd:<ref>\n" \
 "  memfd getfd    memfd:<ref>\n" \
 "  memfd load     <name> <@file|hex|[cdata]> [flags]\n" \
 "  memfd cdata    save|load|info ...\n" \
 "  memfd freeze   memfd:<ref>\n" \
 "  memfd capabilities\n\n" \
 "  ref syntax:  memfd:<name>  or  memfd:<id>\n" \
 "  machine output: --format=json (metadata and mutations)\n"
/* Formats exactly one legacy hexdump row, including the LF. */
size_t plat_memfd_hex_row(char line[96],uint64_t offset,
                                      const unsigned char *bytes,size_t count); /* inline interface */
/* Quotes UTF-8 bytes for machine output without introducing a JSON runtime. */
void plat_memfd_json_string(FILE *out,const char *s); /* inline interface */
/* Name is bounded by the ABI. All integer identities/sizes are decimal strings. */
void plat_memfd_json_info(char out[2048],
    const plat_artifact_buffer_info_t *info,const char *backend,int native_fd); /* inline interface */
include/platx/memfd_cli_grammar.h
/* One grammar for Linux/Windows. Parsing has no native side effects. */
enum plat_memfd_verb { PMFD_HELP, PMFD_CREATE, PMFD_WRITE, PMFD_READ, PMFD_TRUNCATE,
    PMFD_CHMOD, PMFD_SEAL, PMFD_MMAP, PMFD_MPROTECT, PMFD_LIST, PMFD_STAT, PMFD_CLOSE,
    PMFD_GETFD, PMFD_LOAD, PMFD_CDATA, PMFD_FREEZE, PMFD_CAPABILITIES, PMFD_UNKNOWN };
enum plat_memfd_verb plat_memfd_verb(const char *s); /* inline interface */
int plat_memfd_number(const char *s, uint64_t *out, int base); /* inline interface */
int plat_memfd_protection(const char *s); /* inline interface */
/* Linux flag values are vocabulary, never Win32 protection constants. */
int plat_memfd_flags(int argc, const char *const *argv, uint32_t *out); /* inline interface */
int plat_memfd_seals(const char *s, uint32_t *out); /* inline interface */
int plat_memfd_validate(int argc, const char *const *argv); /* inline interface */
/* Strip the common presentation option without changing the caller's argv. */
int plat_memfd_arguments(int argc,const char *const *argv,
                                       const char **out,int capacity,int *json); /* inline interface */
include/platx/memfd_cli_tokenize.h
/* Batch echo is a diagnostic channel, not a place for payloads or keys. */
const char *plat_memfd_diagnostic_command(const char *line); /* inline interface */
/* Inverse of this tokenizer, for already tokenized native argv. */
int plat_memfd_quote_arg(char *out,size_t capacity,const char *arg); /* inline interface */
/* Quoted strings preserve Windows backslashes except escaped quote/backslash
 * inside quotes. An unmatched quote or argv overflow is an error. */
int plat_memfd_tokenize(char *s,const char **argv,int capacity); /* inline interface */
include/platx/memfd_seal_policy.h
/* platx/memfd_seal_policy.h — Memfd seals + ELF policy (INT-093).
 * Verifies seal state before XIM/CHILD exec path is permitted.
 */
/* Mirror of F_SEAL_* for portability in the glue layer. */
#define PLAT_SEAL_SHRINK 0x0001u
#define PLAT_SEAL_GROW   0x0002u
#define PLAT_SEAL_WRITE  0x0004u
#define PLAT_SEAL_EXEC   0x0008u  /* F_SEAL_FUTURE_WRITE in some kernels */

typedef struct {
    unsigned int required_seals;   /* must all be set */
    unsigned int forbidden_seals;  /* must all be clear */
} plat_seal_policy_t;

/* Read seals via fcntl(fd, F_GET_SEALS) and check policy. 0/-1. */
int plat_memfd_seal_check(int fd, const plat_seal_policy_t *policy,
                          char *reason, size_t rsz);

/* Apply additional seals via fcntl(fd, F_ADD_SEALS, seals). 0/-1. */
int plat_memfd_seal_apply(int fd, unsigned int seals);

/* Convenience: default exec policy (SHRINK|GROW|WRITE all required). */
const plat_seal_policy_t *plat_memfd_exec_policy(void);
26

mesh

Peer mesh, идентичность узлов и зашифрованная связь
src/mesh/Связь и ввод-вывод23 файлов4 API headers

Mesh создаёт защищённые peer links и service/message routes между PLATX nodes. Descriptor protocol.mesh управляет node instance; crypto/keyring/transports предоставляются capabilities. Mesh не импортирует удалённые services как локальные указатели.

Граница ответственности

  • Peer identity/authentication обязательны; discovery не равна trust.
  • Remote advertisement создаёт proxy route с owner/generation/TTL.
  • Optional keyring absence имеет explicit configured-key или degraded path.

Устройство подсистемы

  • Node instance владеет listeners, peer table, handshake tasks, route advertisements и recovery.
  • Peer state DISCONNECTED/CONNECTING/AUTHENTICATING/READY/DEGRADED/CLOSING с session generation.
  • X25519/auth handshake derives channel keys через crypto service; keepalive/expiry updates health.
  • Route table bounded, signed/authenticated, TTL/revoke and tied peer generation.

Поток работы

  • Listen/connect intent → transport resolve/XIO.
  • Handshake identity/auth/features → READY peer.
  • Messages/advertisements → validated routes.
  • Link loss → revoke routes → recovery/backoff.

Отказ и восстановление

  • Invalid handshake never creates READY peer/route.
  • Link flap respects persistent budget/backoff.
  • Duplicate node identity collision explicit policy.
  • Keyring revoke triggers rekey/quiesce, not use cached stale pointer.

Основные возможности

  • Node and peer lifecycle with listener/connect paths.
  • X25519 handshake/key agreement and keepalive.
  • Persistent recovery watch and capability-aware optional keyring integration.
Архитектурные детали и инварианты

Overview

The provider mesh manages the lifecycle, health, routing, load-balancing, failover, and circuit-breaking for all registered platform providers. All state lives in static slot tables (§SEC-3: no malloc in hot paths).

Invariants

INV-MESH-01 | > 50% of providers DOWN → mesh_health_warn_active() = 1; caller raises ThreatLevel

§SEC-3 | No malloc in health/route/LB/failover/circuit paths

Components

mesh_health.c | 64-slot static provider table; UP/DEGRADED/DOWN tracking

mesh_route.c | Capability-based provider lookup; skips DOWN + open-circuit

mesh_lb.c | Weighted round-robin LB with atomic cursor

mesh_failover.c | Returns first healthy alternative; increments failover counter

mesh_circuit.c | Circuit breaker: CLOSED→OPEN (N fails)→HALF-OPEN (probe after 30 s)→CLOSED

Управление и диагностика

Корневые команды: mesh. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / mesh →
Состав подсистемы / 23 файлов
Файл / компонентНазначение и граница
src/mesh/cmd_mesh.cnamespace `mesh`: CLI + интеграция в платформу. Один узел mesh на процесс (хук plat_mesh_mod_live, не локальный g_node). Приватный ключ (identity) хранится в keyring платформы под "mesh:privkey" — персистентная идентичность между запусками. Интерфейс mesh_if_t публикуется как protocol.mesh. CLI verb остаётся mesh.
src/mesh/mesh.hвнутренние структуры плагина mesh (узел, пир, кадры).
src/mesh/mesh_api.hпубличный ABI плагина mesh. Определяет: коды ошибок, конфиги, структуры статуса, mesh_if_t. Не содержит реализации (только .h).
src/mesh/mesh_attestation.cверификация аттестаций пиров, trust-кэш
src/mesh/mesh_attestation.hаттестация узлов через Mesh Каждый узел публикует pxtrust_attest_t с подписью Ed25519. Соседи верифицируют подпись, обновляют локальный trust-кэш, передают в роевую таблицу stigmergy (угрозы → феромоны).
src/mesh/mesh_circuit.cState lives in mesh_prov_tab[] (mesh_provtab.h), no extra alloc.
src/mesh/mesh_crypto.cX25519 (RFC 7748) + base64url + обёртки над crypto.c.
src/mesh/mesh_crypto.hкриптографические примитивы mesh-плагина. X25519 Diffie-Hellman + HKDF-SHA-256 ключевой вывод + ChaCha20-Poly1305 AEAD + base64url. Реализация — в mesh_crypto.c (обёртки над src/crypto/crypto.c).
src/mesh/mesh_failover.cWhen a provider is marked DOWN/circuit-open, returns the next healthy
src/mesh/mesh_health.cINV-MESH-01: when > 50% providers are unavailable, mesh_health_warn_active() A2-P05: this file owns the one provider slot table (mesh_provtab.h). It used to define it `static` while four sibling TUs declared it `extern`, so the
src/mesh/mesh_identity.cMesh node identity lifecycle. STAB-125: key_rotate, duplicate_id, stale_peer, keyring_unavail. A2-P05: this file previously operated on a `mesh_ctx_t` with a `cfg`, a `kr` handle and a `peers` list macro, and called mesh_crypto_key_rotate(), mesh_peer_find_by_id(), mesh_peer_remove(), mesh_log(), mesh_id_str() and
src/mesh/mesh_lb.cРеализация mesh / lb
src/mesh/mesh_mod.cthin platx adapt for existing mesh. Invariant: this file does not speak the mesh wire protocol and does not create threads. Node I/O stays in mesh_node.c. Restart is plat_recovery_choose/apply → plat_lifecycle_request only. After STOP the cap is gone and the live node (if any) is stopped.
src/mesh/mesh_mod.hprotocol.mesh as a platx module. Not a new mesh protocol.
src/mesh/mesh_node.cядро P2P-узла: listener, dialer, рукопожатие, keepalive,
src/mesh/mesh_peer.cуправление пирами: создание, поиск, учёт.
src/mesh/mesh_protocol.cфреймовый I/O, HELLO, вывод сессионного ключа.
src/mesh/mesh_provtab.hthe one provider slot table shared by the carried their own hand-written `extern` for this table while mesh_health.c defined it `static`. Four copies of a declaration that could never resolve:
src/mesh/mesh_raw.cреализация транспорта PXTRUST поверх mesh. Контракт и обоснование существования — в mesh_raw.h. Здесь держатся два инварианта: I1. publish возвращает OK ТОЛЬКО при фактической доставке ≥1 пиру. Ноль пиров, мёртвый узел, отказ mesh_send — это ошибка, не успех.
src/mesh/mesh_raw.hканонический транспорт PXTRUST поверх mesh. ЗАЧЕМ ЭТОТ ФАЙЛ СУЩЕСТВУЕТ До него три модуля — central_node.c, sp_mesh_bridge.c, mfbt_chaos.c — объявляли `mesh_publish_raw` / `mesh_subscribe_raw` как weak extern, НИ ОДИН не определял их, и mfbt_chaos.c объявлял расходящуюся сигнатуру
src/mesh/mesh_route.cРеализация mesh / route
src/mesh/mesh_swarm.cроевые алгоритмы: стигмергия, кворум, бифуркация
src/mesh/mesh_swarm.hроевые алгоритмы Mesh-плагина PLATX 1. Stigmergy — феромонные следы угроз, испарение, усиление 2. Quorum — скоординированный ответ при достижении порога 3. Bifurcation — детектор приближения к критической точке (dΦ/dt)
Контракты API / 4 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/mesh_health.h
/* platx/mesh_health.h — Mesh peer health + transport availability (INT-105). */
#define PLAT_MESH_PEER_MAX    32
#define PLAT_MESH_NAME_MAX    64
#define PLAT_MESH_REASON     128

typedef enum {
    PLAT_MESH_PEER_UP      = 0,
    PLAT_MESH_PEER_DEGRADED = 1,
    PLAT_MESH_PEER_DOWN    = 2,
} plat_mesh_peer_state_t;

typedef struct {
    char                  name[PLAT_MESH_NAME_MAX];
    plat_mesh_peer_state_t state;
    int                   transport_ok;
    uint64_t              last_seen_ms;
} plat_mesh_peer_info_t;

/* Check transport availability for a named peer. 0=up, 1=degraded, -1=down. */
int plat_mesh_peer_health(const char *peer_name, plat_mesh_peer_info_t *out);

/* Publish health snapshot to MBus topic "mesh.health". 0/-1. */
int plat_mesh_health_publish(void);
include/platx/names.h
/* platx/names.h — logical module/capability identity.
 *
 * Grammar: <domain>.<token>
 *   storage.*    live store APIs
 *   netstack.*   packet/net core (reserved until a module publishes)
 *   transport.*  transport module identity
 *   protocol.*   protocol modules (mesh, ra2c, …)
 *   sensors.*    eBPF sensor domain (hades). Live provide() only when owned.
 *   script       live script-engine identity (CLI verb stays script)
 *   test.*       canaries only
 *
 * This is registry identity, not a CLI verb and not a src/ path.
 * Published caps keep their strings; aliases are not invented here.
 */
#define PLAT_NS_STORAGE    "storage"
#define PLAT_NS_NETSTACK   "netstack"
#define PLAT_NS_TRANSPORT  "transport"
#define PLAT_NS_PROTOCOL   "protocol"
#define PLAT_NS_SENSORS    "sensors"
#define PLAT_NS_TEST       "test"

/* storage.* — published. Do not rename: modules already require() this. */
#define PLAT_NAME_STORAGE_KEYRING  "storage.keyring"
/* PXSECRETS development capability; product publication is gated separately. */
#define PLAT_NAME_STORAGE_SECRETS  "storage.secrets"

/* netstack.* — reserved. Live instance slots still use the old netstack: prefix. */
#define PLAT_NAME_NETSTACK_CORE    "netstack.core"

/* transport.* — one live module token. Per-adapter slots stay transport:<id>. */
#define PLAT_NAME_TRANSPORT_CORE   "transport.core"

/* protocol.* — live provide() identity. CLI verbs stay mesh / ra2c. */
#define PLAT_NAME_PROTOCOL_MESH    "protocol.mesh"
#define PLAT_NAME_PROTOCOL_RA2C    "protocol.ra2c"

/* sensors.* — published when the door owns loaded sensors. Not a CLI verb. */
#define PLAT_NAME_SENSORS_HADES    "sensors.hades"

/* sensors.sense.* — SENSE observation plane (SENSE004, 2026-08-27).
 * Registered without aliases; collision-tested in test-sense-cap-collision. */
#define PLAT_NAME_SENSE_EVENTS   "sensors.sense.events"
#define PLAT_NAME_SENSE_QUERY    "sensors.sense.query"
#define PLAT_NAME_SENSE_CONTROL  "sensors.sense.control"
/* script — live provide() identity. CLI verb stays script. */
#define PLAT_NAME_SCRIPT           "script"

/* test.* — loopback / child canaries. Not a product domain. */
#define PLAT_NAME_TEST_ECHO        "test.echo"
#define PLAT_NAME_TEST_CHILDCAP    "test.childcap"

/* Live provide() identity. No protocol handshake is defined here. */
#define PLAT_CAP_MESH              PLAT_NAME_PROTOCOL_MESH
#define PLAT_CAP_RA2C              PLAT_NAME_PROTOCOL_RA2C
#define PLAT_CAP_HADES             PLAT_NAME_SENSORS_HADES
include/platx/platx_mesh.h
/* include/platx/platx_mesh.h — public mesh health/routing/LB/failover/circuit (task 2.43).
 *
 * INV-MESH-01: > 50% providers unavailable → ThreatLevel raise.
 * All state is static BSS (§SEC-3: no malloc in hot paths).
 */

#define PLATX_MESH_MAX_PROVIDERS  64
#define PLATX_MESH_NAME_MAX       64
#define PLATX_MESH_REASON_MAX     128

/* ── provider states ────────────────────────────────────────────────── */
typedef enum {
    MESH_PROV_UP       = 0,
    MESH_PROV_DEGRADED = 1,
    MESH_PROV_DOWN     = 2,
} mesh_prov_state_t;

/* ── provider entry (static slot, no malloc) ────────────────────────── */
typedef struct {
    char               name[PLATX_MESH_NAME_MAX];
    mesh_prov_state_t  state;
    uint32_t           weight;          /* LB weight (1–100)             */
    uint64_t           last_ok_ms;
    uint64_t           fail_count;
    uint64_t           success_count;
    int                circuit_open;    /* circuit breaker state          */
    uint32_t           cb_fail_streak;  /* consecutive failures           */
    /* A2-P05-234: when the breaker tripped, on the same monotonic clock as
     * last_ok_ms. The half-open probe is measured from here. Overloading
     * last_ok_ms for this made every tripped breaker read as closed on any
     * host that had been up longer than MESH_CB_RESET_MS. */
    uint64_t           cb_open_ms;
} mesh_provider_entry_t;

/* ── mesh route result ──────────────────────────────────────────────── */
typedef struct {
    char     provider[PLATX_MESH_NAME_MAX];
    int      found;
    uint32_t weight;
    /* A2-P05-229/232: routing accepts a DEGRADED provider, so the caller has
     * to be able to tell a healthy hop from a degraded one. Without this the
     * only two answers were "some provider" and -1, and DEGRADED was
     * indistinguishable from FULL. Valid only when found != 0. */
    mesh_prov_state_t state;
} mesh_route_result_t;

/* ── mesh statistics ────────────────────────────────────────────────── */
typedef struct {
    uint32_t total;
    uint32_t up;
    uint32_t degraded;
    uint32_t down;
    uint32_t circuit_open;
    uint64_t route_calls;
    uint64_t failover_events;
    uint64_t cb_trips;
} mesh_stats_t;

/* ── health API ─────────────────────────────────────────────────────── */
int  mesh_health_register(const char *name, uint32_t weight);
void mesh_health_update(const char *name, mesh_prov_state_t state);
int  mesh_health_get(const char *name, mesh_provider_entry_t *out);
int  mesh_health_warn_active(void);   /* INV-MESH-01: >50% down → 1    */
int  mesh_health_publish(void);       /* publish to mbus "mesh.health"  */

/* ── routing API ─────────────────────────────────────────────────────── */
int  mesh_route(const char *capability, mesh_route_result_t *out);

/* ── load-balancing API ──────────────────────────────────────────────── */
int  mesh_lb_next(const char *capability, char *name_out, size_t namesz);

/* ── failover API ────────────────────────────────────────────────────── */
int  mesh_failover(const char *failed_provider,
                   char *alt_out, size_t altsz);

/* ── circuit-breaker API ─────────────────────────────────────────────── */
#define MESH_CB_FAIL_THRESHOLD  5u    /* consecutive failures to open    */
#define MESH_CB_RESET_MS        30000u /* half-open probe interval        */

int  mesh_circuit_record_fail(const char *name);
int  mesh_circuit_record_ok(const char *name);
int  mesh_circuit_is_open(const char *name);

/* ── stats ───────────────────────────────────────────────────────────── */
void mesh_stats_get(mesh_stats_t *out);

/* A2-P05-236/239: mesh_stats_t carries route_calls and failover_events, but
 * both counters live in TUs other than the one that fills the struct. These
 * accessors are how mesh_stats_get reaches them; a diagnostic that always
 * reports zero is worse than one that reports nothing. */
uint64_t mesh_route_call_count(void);
uint64_t mesh_failover_event_count(void);
include/platx/pxtrust.h
/* include/platx/pxtrust.h — PLATX Distributed Trust Protocol
 *
 * Триединая система доверия: SelfProtect ↔ Mesh ↔ Central Node
 *
 * Три канала RA2C:
 *   ra2c_mesh    — mesh gossip/broadcast (peer-to-peer рой)
 *   ra2c_central — выделенная сессия к Central Node
 *   ra2c_direct  — прямой P2P канал между двумя узлами
 *
 * §SEC-3: все структуры фиксированного размера, malloc запрещён.
 * Криптография: Ed25519 (подписи) + ChaCha20-Poly1305 (RA2C AEAD).
 *
 * SPDX-License-Identifier: GPL-2.0
 */

/* ══════════════════════════════════════════════════════════════════════
 * 1. Константы
 * ══════════════════════════════════════════════════════════════════════ */

#define PXTRUST_NODE_ID_LEN     32u   /* SHA-256 от Ed25519 pubkey          */
#define PXTRUST_PUBKEY_LEN      32u   /* Ed25519 public key                 */
#define PXTRUST_SIG_LEN         64u   /* Ed25519 signature                  */
#define PXTRUST_POLICY_MAX      4096u /* максимальный payload политики      */
#define PXTRUST_CMD_PAYLOAD_MAX 512u  /* максимальный payload команды       */
#define PXTRUST_MAX_NODES       128u  /* максимум узлов в реестре           */
#define PXTRUST_CHAOS_MAX_TGT   16u   /* максимум целевых узлов в сценарии  */

/* Версия wire-протокола */
#define PXTRUST_PROTO_VERSION   1u

/* Типы сообщений поверх RA2C */
#define PXTRUST_MSG_ATTESTATION  0x01u  /* SelfProtect → Mesh → Central     */
#define PXTRUST_MSG_COMMAND      0x02u  /* Central → Mesh → SelfProtect     */
#define PXTRUST_MSG_POLICY       0x03u  /* Central → Mesh → все узлы        */
#define PXTRUST_MSG_ALERT        0x04u  /* SelfProtect → Mesh → все узлы    */
#define PXTRUST_MSG_CHAOS        0x05u  /* Arena Judge → Mesh (CHAOS mode)  */
#define PXTRUST_MSG_RECOVER      0x06u  /* Central → повреждённый узел      */
#define PXTRUST_MSG_PHEROMONE    0x07u  /* Mesh внутренний: stigmergy update */

/* Каналы RA2C */
typedef enum {
    PXTRUST_CHAN_MESH    = 0,   /* gossip/broadcast по рою                  */
    PXTRUST_CHAN_CENTRAL = 1,   /* прямой канал к Central Node              */
    PXTRUST_CHAN_DIRECT  = 2,   /* прямой P2P канал между узлами            */
} pxtrust_channel_t;

/* ══════════════════════════════════════════════════════════════════════
 * 2. Состояние узла (SelfProtect)
 * ══════════════════════════════════════════════════════════════════════ */

typedef enum {
    PXTRUST_STATE_OK              = 0,
    PXTRUST_STATE_DEGRADED        = 1,
    PXTRUST_STATE_TAMPER_DETECTED = 2,
    PXTRUST_STATE_LOCKDOWN        = 3,
    PXTRUST_STATE_RECOVERING      = 4,
} pxtrust_node_state_t;

/* ══════════════════════════════════════════════════════════════════════
 * 3. sp_attestation_t — аттестация узла
 *
 * SelfProtect строит, подписывает Ed25519, отправляет через Mesh.
 * Central Node проверяет подпись и обновляет реестр доверия.
 * ══════════════════════════════════════════════════════════════════════ */

typedef struct __attribute__((packed)) {
    uint8_t  proto_version;                    /* PXTRUST_PROTO_VERSION      */
    uint8_t  msg_type;                         /* PXTRUST_MSG_ATTESTATION    */
    uint16_t flags;                            /* зарезервировано            */

    uint8_t  node_id[PXTRUST_NODE_ID_LEN];     /* SHA-256(pubkey)            */
    uint8_t  pubkey[PXTRUST_PUBKEY_LEN];       /* Ed25519 public key узла    */
    uint64_t epoch;                            /* поколение доверия          */
    uint64_t ts_mono_ms;                       /* монотонное время (мс)      */

    uint32_t integrity_score;                  /* 0..1000                    */
    uint32_t violations_count;                 /* накопленных нарушений      */
    uint8_t  node_state;                       /* pxtrust_node_state_t       */
    uint8_t  mil_grade;                        /* 1 = military-grade mode    */
    uint8_t  reserved[2];

    uint8_t  policy_hash[32];                  /* SHA-256 текущей политики   */
    uint8_t  kernel_hash[32];                  /* SHA-256 проверенного ядра  */

    uint64_t hook_checks;                      /* sp_hook_restore_total_checks */
    uint64_t hook_restores;                    /* sp_hook_restore_total_restores */
    uint64_t ptrace_hits;                      /* sp_memwatch_ptrace_detections */
    uint64_t memfd_hits;                       /* sp_memwatch_mem_fd_detections */

    /* Ed25519-подпись над всеми байтами до sig (включительно) */
    uint8_t  sig[PXTRUST_SIG_LEN];
} pxtrust_attest_t;

static_assert(sizeof(pxtrust_attest_t) ==
    1+1+2 + 32+32 + 8+8 + 4+4+1+1+2 + 32+32 + 8+8+8+8 + 64,
    "pxtrust_attest_t layout changed");

#define PXTRUST_ATTEST_SIGBYTES  \
    (sizeof(pxtrust_attest_t) - PXTRUST_SIG_LEN) /* байт до подписи */

/* ══════════════════════════════════════════════════════════════════════
 * 4. pxtrust_alert_t — сигнал тревоги
 *
 * SelfProtect → Mesh → все узлы. Подписан.
 * ══════════════════════════════════════════════════════════════════════ */

typedef enum {
    PXTRUST_ALERT_HOOK_TAMPER       = 0x01,
    PXTRUST_ALERT_KERNEL_MODIFIED   = 0x02,
    PXTRUST_ALERT_PTRACE_DETECTED   = 0x04,
    PXTRUST_ALERT_MEMFD_DETECTED    = 0x08,
    PXTRUST_ALERT_POLICY_TAMPER     = 0x10,
    PXTRUST_ALERT_ROOTKIT_SUSPECTED = 0x20,
} pxtrust_alert_reason_t;

typedef struct __attribute__((packed)) {
    uint8_t  proto_version;
    uint8_t  msg_type;                         /* PXTRUST_MSG_ALERT          */
    uint16_t flags;
    uint8_t  node_id[PXTRUST_NODE_ID_LEN];
    uint64_t epoch;
    uint64_t ts_mono_ms;
    uint32_t reason_mask;                      /* pxtrust_alert_reason_t OR'd */
    uint32_t severity;                         /* 0..100                     */
    uint8_t  detail[64];                       /* короткая строка            */
    uint8_t  sig[PXTRUST_SIG_LEN];
} pxtrust_alert_t;

/* ══════════════════════════════════════════════════════════════════════
 * 5. pxtrust_cmd_t — команда от Central Node
 *
 * Central → Mesh → целевой(е) узел(ы). Подписана ключом Central.
 * ══════════════════════════════════════════════════════════════════════ */

typedef enum {
    PXTRUST_CMD_NOOP            = 0,
    PXTRUST_CMD_ENHANCE_SCAN    = 1,    /* усилить сканирование              */
    PXTRUST_CMD_ROTATE_KEYS     = 2,    /* ротация ключей SelfProtect        */
    PXTRUST_CMD_UPDATE_POLICY   = 3,    /* применить новую политику          */
    PXTRUST_CMD_LOCKDOWN        = 4,    /* перейти в LOCKDOWN                */
    PXTRUST_CMD_RECOVER         = 5,    /* запустить процедуру восстановления */
    PXTRUST_CMD_QUARANTINE      = 6,    /* изолировать узел на сетевом уровне */
    PXTRUST_CMD_REPORT_STATUS   = 7,    /* немедленно прислать аттестацию    */
    PXTRUST_CMD_CHAOS_INJECT    = 8,    /* ARENA: инжектировать хаос         */
} pxtrust_cmd_type_t;

typedef struct __attribute__((packed)) {
    uint8_t  proto_version;
    uint8_t  msg_type;                         /* PXTRUST_MSG_COMMAND        */
    uint16_t flags;
    uint64_t command_id;                       /* монотонный ID команды      */
    uint8_t  target_node[PXTRUST_NODE_ID_LEN]; /* 0 = broadcast             */
    uint8_t  issuer_pubkey[PXTRUST_PUBKEY_LEN];/* pubkey Central Node       */
    uint64_t epoch;
    uint64_t ts_mono_ms;
    uint8_t  cmd_type;                         /* pxtrust_cmd_type_t         */
    uint8_t  reserved[3];
    uint32_t payload_len;                      /* 0..PXTRUST_CMD_PAYLOAD_MAX */
    uint8_t  payload[PXTRUST_CMD_PAYLOAD_MAX];
    uint8_t  sig[PXTRUST_SIG_LEN];
} pxtrust_cmd_t;

/* ══════════════════════════════════════════════════════════════════════
 * 6. pxtrust_policy_t — обновление политики
 * ══════════════════════════════════════════════════════════════════════ */

typedef struct __attribute__((packed)) {
    uint8_t  proto_version;
    uint8_t  msg_type;                         /* PXTRUST_MSG_POLICY         */
    uint16_t flags;
    uint64_t policy_id;                        /* монотонный ID политики     */
    uint8_t  issuer_pubkey[PXTRUST_PUBKEY_LEN];
    uint64_t epoch;
    uint64_t valid_from_ms;
    uint64_t valid_until_ms;
    uint32_t policy_version;
    uint32_t payload_len;                      /* 0..PXTRUST_POLICY_MAX      */
    uint8_t  policy_data[PXTRUST_POLICY_MAX];
    uint8_t  sig[PXTRUST_SIG_LEN];
} pxtrust_policy_t;

/* ══════════════════════════════════════════════════════════════════════
 * 7. trust_entry_t — запись в реестре Central Node
 * ══════════════════════════════════════════════════════════════════════ */

typedef struct {
    uint8_t               node_id[PXTRUST_NODE_ID_LEN];
    uint8_t               pubkey[PXTRUST_PUBKEY_LEN];
    uint64_t              epoch;
    uint64_t              last_seen_ms;
    uint32_t              integrity_score;
    uint32_t              violations_count;
    pxtrust_node_state_t  state;
    uint8_t               is_trusted;     /* 1 = в белом списке             */
    uint8_t               is_blacklisted; /* 1 = чёрный список              */
    uint8_t               mil_grade;
    uint8_t               reserved;
} trust_entry_t;

/* ══════════════════════════════════════════════════════════════════════
 * 8. Роевые примитивы (stigmergy)
 * ══════════════════════════════════════════════════════════════════════ */

#define SWARM_PHEROMONE_MAX      256u
#define SWARM_EVAP_ALPHA_NUM     7u     /* evap = intensity * 7/8           */
#define SWARM_EVAP_ALPHA_DEN     8u
#define SWARM_QUORUM_K           3u     /* минимум подтверждений            */
#define SWARM_QUORUM_INTENSITY   400u   /* порог интенсивности (0..1000)    */
#define SWARM_BIFURC_DERIV_TRESH 50u   /* порог dΦ/dt для HIGH_ALERT       */

typedef struct {
    uint8_t  threat_hash[32];          /* SHA-256 сигнатуры угрозы          */
    uint32_t intensity;                /* 0..1000                           */
    uint32_t reinforcement;            /* число подтверждений от соседей    */
    uint64_t ts_last_seen_ms;          /* монотонное время                  */
    uint8_t  active;                   /* 1 = слот занят                    */
} mesh_pheromone_t;

/* ══════════════════════════════════════════════════════════════════════
 * 9. CHAOS mode — сценарий для ARENA Judge
 * ══════════════════════════════════════════════════════════════════════ */

typedef enum {
    CHAOS_BIFURCATION   = 1,  /* параметр среды пересекает порог            */
    CHAOS_NODE_FAILURE  = 2,  /* случайное отключение n% узлов              */
    CHAOS_BYZANTINE     = 3,  /* узел рассылает противоречивые данные       */
    CHAOS_TIMING        = 4,  /* искусственные задержки в hot path          */
    CHAOS_SPLIT_BRAIN   = 5,  /* разрыв mesh на partitions                  */
    CHAOS_SOC_SANDPILE  = 6,  /* самоорганизующийся поиск критточки         */
} chaos_mode_t;

typedef struct __attribute__((packed)) {
    uint8_t  proto_version;
    uint8_t  msg_type;                         /* PXTRUST_MSG_CHAOS          */
    uint16_t flags;
    uint8_t  scenario_id[16];                  /* UUID сценария              */
    uint8_t  issuer_pubkey[PXTRUST_PUBKEY_LEN];/* pubkey Arena Judge         */
    uint8_t  cmd_type;                         /* chaos_mode_t               */
    uint8_t  blind_to_blue;                    /* 1 = Blue Team не знает     */
    uint16_t intensity_ppm;                    /* 0..1000 (промилле)         */
    uint32_t duration_ms;
    uint64_t seed;                             /* воспроизводимость          */
    uint32_t target_count;                     /* 0 = все узлы               */
    uint8_t  target_nodes[PXTRUST_CHAOS_MAX_TGT][PXTRUST_NODE_ID_LEN];
    uint8_t  sig[PXTRUST_SIG_LEN];
} pxtrust_chaos_t;

/* ══════════════════════════════════════════════════════════════════════
 * 10. MFBT — хаоплексические параметры
 * ══════════════════════════════════════════════════════════════════════ */

#define MFBT_PHASE_DIM           3u    /* размерность фазового пространства  */
#define MFBT_ATTRACTOR_POINTS    64u   /* глубина истории для аттрактора     */
#define MFBT_OODA_WINDOW_MS      5000u /* окно измерения OODA-петли (мс)     */

typedef struct {
    int64_t  x[MFBT_PHASE_DIM]; /* координаты в фазовом пространстве (fixed pt) */
    uint64_t ts_ms;
} mfbt_phase_point_t;

typedef enum {
    MFBT_THREAT_LOW     = 0,
    MFBT_THREAT_MEDIUM  = 1,
    MFBT_THREAT_HIGH    = 2,
    MFBT_THREAT_CRITICAL= 3,
} mfbt_threat_level_t;

typedef enum {
    MFBT_RC_NONE        = 0,   /* признаков рефлексивного управления нет   */
    MFBT_RC_SUSPECTED   = 1,   /* подозрение                               */
    MFBT_RC_CONFIRMED   = 2,   /* подтверждено                             */
} mfbt_rc_state_t;
27

mfbt

Моделирование динамики противоборства и управляемого хаоса
src/mfbt/Наблюдение и исследование4 файлов0 API headers

MFBT связывает восстановление динамики системы с анализом информационных воздействий. Strange Attractor использует вложение Такенса и оценку показателей Ляпунова; OODA Disruption описывает нарушения цикла наблюдения и решения; RC Detector ищет последовательность «информация → действие → атака». Chaos-механизмы входят в экспериментальную модель ARENA, где сценарий, полномочия и ресурсные пределы заданы явно.

Граница ответственности

  • MFBT связывает восстановление динамики системы с анализом информационных воздействий. Strange Attractor использует вложение Такенса и оценку показателей Ляпунова; OODA Disruption описывает нарушения цикла наблюдения и решения; RC Detector ищет последовательность «информация → действие → атака». Chaos-механизмы входят в экспериментальную модель ARENA, где сценарий, полномочия и ресурсные пределы заданы явно.

Устройство подсистемы

  • Strange Attractor строит представление динамики по наблюдаемому ряду с использованием вложения Такенса и оценки показателей Ляпунова.
  • OODA Disruption описывает ограниченные воздействия на цикл наблюдения, ориентирования, решения и действия.
  • RC Detector рассматривает последовательность «информация → действие → атака»; chaos-компонент связан с моделью эксперимента.

Поток работы

  • Наблюдаемый ряд и условия сценария → модель динамики.
  • Гипотеза о переходе либо реакции → ограниченный эксперимент.
  • Сигналы, временные связи и известный контекст → оценка модели.
  • Результат и его неопределённость → сценарий ARENA или исследовательский разбор.

Отказ и восстановление

  • Недостаточный ряд не даёт обоснованной оценки динамики.
  • Нарушение предела сценария прекращает эксперимент.
  • Модельная закономерность остаётся гипотезой при отсутствии подтверждающего наблюдения.

Управление и диагностика

Домен не регистрирует собственную корневую команду. Его контракт используется вызывающими подсистемами через API и интерфейсы mission, mirage.

Справочник CLI / mfbt →
Состав подсистемы / 4 файлов
Файл / компонентНазначение и граница
src/mfbt/mfbt.cMove Fast & Break Things implementation Layer 1: Strange Attractor — Takens embedding, Lyapunov estimation Layer 2: OODA Disruption — петля Бойда, rate-limited (RED_TEAM gate) Layer 3: RC Detector — паттерн «инфо→действие→атака»
src/mfbt/mfbt.hMove Fast & Break Things: хаоплексные техники PLATX Layer 1 — Strange Attractor Mapper (детектив, аналитика) Layer 2 — OODA Disruption Framework (под RED_TEAM gate) Layer 3 — Reflexive Control Detector (оборонительный, без gate) Требуется CAPABILITY_CLASS: RED_TEAM в PLATX lease для Layer 2.
src/mfbt/mfbt_chaos.cARENA CHAOS Engine implementation Требуется CAPABILITY_CLASS: RED_TEAM (red_team_gate=1 при init). Все chaos-команды верифицируются Ed25519 ключом ARENA Judge.
src/mfbt/mfbt_chaos.hДвижок хаоса для ARENA Judge (домен тестирования). верифицирует Ed25519 и исполняет один из шести режимов: CHAOS_BIFURCATION — форсированный переход через точку бифуркации CHAOS_BYZANTINE — инжекция противоречивых аттестаций CHAOS_TIMING — искусственные задержки в hot path
Контракты API / 0 заголовков

Отдельный заголовок из include/platx/ не закреплён за этим доменом в справочнике. Внутренние границы перечислены в составе файлов; управление и публичные связи описаны в соседних подсистемах.

28

mirage

Синтетические миры, Consistency Oracle и журнал взаимодействий
src/mirage/Наблюдение и исследование32 файлов1 API headers

MIRAGE предоставляет модель синтетического мира: сущности, отношения, причинное состояние, согласованность и контролируемое взаимодействие. Consistency Oracle проверяет совместимость результатов между файловой, процессной, сетевой и временной картинами. Ground truth, watermark и журнал отделены от видимой участнику проекции. Поколение мира, seed commitment и ресурсный envelope связывают каждый эффект с конкретной сессией.

Граница ответственности

  • Synthetic world не имеет права выполнять заявленный side effect в реальном host; эффекты моделируются в bounded envelope.
  • Ground truth, watermark и evidence отделены от operator-visible fiction; typed Semantic Mirage Bus не заменяет MBus/Event Hub/capreg.

Устройство подсистемы

  • mirage_abi.h задаёт ABI v1, interaction modes, fidelity/quality, capability bits, provider vtable, state и operation/result envelope.
  • mirage_world.h задаёт world identity/generation, entities, deception graph, seed commitment, truth watermark и resource/session envelope.
  • mirage_oracle.h задаёт Consistency Oracle и восемь классов инвариантов между файлами, процессами, сетью, identity, time и causal state.
  • mirage_bus.h задаёт бинарные COMMAND/FILE/PROCESS/NETWORK/PROTO/AUTH/SYSTEM/PERSIST frames с world/session/operation/causal identity.

Поток работы

  • World specification + committed seed → validate deterministic graph/invariants.
  • Start generation → publish only after provider self-test and envelope reservation.
  • Typed interaction → oracle pre-check → synthetic effect/result → ledger/evidence/watermark.
  • Envelope exhausted, inconsistency or breach → drain/revoke generation → seal checkpoint → destroy owned resources.

Отказ и восстановление

  • Oracle violation marks world violated/breached before contradictory result escapes.
  • Late interaction against retired generation is STALE and cannot mutate a replacement world.
  • Ledger overflow/backpressure is explicit; evidence loss count and checkpoint continuity remain visible.
  • Any real host side effect, unwatermarked artifact or unbounded session is a critical design failure.

Основные возможности

  • Bundle validation and sealing plus bounded CANARY provider implementation.
  • Hash-chained digest-only interaction ledger, Merkle checkpoint and envelope accounting.
  • Basic Oracle implements all eight invariant classes; typed Semantic Bus codec validates 20 frame types.
Архитектурные детали и инварианты

Security Invariants

- **TL ≥ 4 (LOCKDOWN):** all sandbox execution disabled.

- **MINIMAL mode:** MIRAGE is compiled out (PLATX_HAS_MIRAGE undefined).

Components

Applies a minimal syscall allowlist (read, write, exit, brk, mmap, munmap,

mprotect, close, openat, fstat, lseek) via seccomp(SECCOMP_SET_MODE_FILTER).

Must be called inside the child after namespace entry.

8-slot static world-model table. Each world defines root_path, work_dir,

uid/gid mapping, and readonly flag. No malloc (§SEC-3).

mirage_backend_enter(root) performs chdir + chroot. Production use of

pivot_root is noted but test stubs use chroot for portability.

mirage_exec(req, res) forks a child, enters the sandbox world, and waits

with a timeout watchdog (INV-MIRAGE-03).

Read-only probe mode. mirage_probe_is_writable() enforces INV-MIRAGE-02 via

16-slot static sandbox-process table. mirage_gc_run(now_ms, max_ttl) reaps

exited processes and SIGKILLs over-TTL ones. No malloc (§SEC-3).

mirage_watchdog_wait(pid, ttl_ms) polls with 5 ms granularity and sends

SIGKILL if deadline is exceeded (INV-MIRAGE-03). Returns -ETIMEDOUT (-110).

Управление и диагностика

Корневые команды: mirage. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / mirage →
Состав подсистемы / 32 файлов
Файл / компонентНазначение и граница
src/mirage/cmd_mirage.cрегистрация глагола `mirage` в консоли платформы.
src/mirage/mirage_abi.hPlatX MIRAGE/HONEYPOT: публичный замороженный ABI. MIR0 wave: заморожены interaction modes, fidelity levels, capability bits, semantic quality (Q0-Q4) и plat_mirage_provider vtable v1. - Ни один provider не включает internal заголовки Hook, XIM, sandbox,
src/mirage/mirage_audit.cРеализация mirage / audit
src/mirage/mirage_backend.cРеализация mirage / backend
src/mirage/mirage_bundle.cPlatX MIRAGE/HONEYPOT: world bundle validation. MIR1 wave: userspace-only; без kernel, без внешних зависимостей. SHA-256 для bundle_digest: заимствуем sp_validate.c подход — inline FIPS 180-4 без внешних зависимостей.
src/mirage/mirage_bundle.hPlatX MIRAGE/HONEYPOT: world bundle binary format. MIR1 wave: userspace-only; без kernel, без внешних зависимостей. Бандл — самоописывающийся бинарный блок, передаваемый в plat_mirage_provider_t.prepare(self, bundle, bundle_len). Валидируется провайдером до activate().
src/mirage/mirage_bus.cPlatX MIRAGE/HONEYPOT: Semantic Mirage Bus frame codec. Что этот модуль делает: - знает точный размер каждого из 20 замороженных frame-типов - заполняет и проверяет общий header - валидирует frame: magic, известный тип, payload_len, полный размер - проверяет bounded-инварианты payload-а (NUL-терминация, счётчики)
src/mirage/mirage_bus.hPlatX MIRAGE/HONEYPOT: Semantic Mirage Bus frame classes. MIR0 wave: заморожены frame type codes, common frame header и все typed payload structs Semantic Mirage Bus (SMB). КОНСТИТУЦИОННЫЙ ЗАПРЕТ: JSON bus, Event Hub, fifth registry — запрещены. SMB — typed binary frames (not JSON, not MBus, not Event Hub).
src/mirage/mirage_bus_codec.hPlatX MIRAGE/HONEYPOT: Semantic Mirage Bus codec API. Реализация поверх замороженных frame-структур из mirage_bus.h (MIR0). Сам mirage_bus.h — только типы; исполнение живёт здесь. - размер каждого из 20 frame-типов и их семейство - заполнение общего header (`mir_bus_init_hdr`)
src/mirage/mirage_canary.cPlatX MIRAGE/HONEYPOT: CANARY mode provider. MIR1 wave: userspace-only; без kernel, без внешних зависимостей.
src/mirage/mirage_canary.hОбъявления типов и интерфейсов
src/mirage/mirage_cli.cCLI домена MIRAGE (sandbox rehearsal). Использует реальный platx/platx_mirage.h: mirage_world_count/get() — перечисление sandbox-миров mirage_exec() — запуск в sandbox Слабые заглушки обеспечивают автономную сборку standalone-бинаря. SYNTHETIC ≠ OBSERVED. PARTIAL ≠ ok.
src/mirage/mirage_compare.cРеализация mirage / compare
src/mirage/mirage_exec.cРеализация mirage / exec
src/mirage/mirage_forensic.hPlatX MIRAGE/HONEYPOT: interaction ledger, truth watermark record и evidence record schema. MIR0 wave: заморожены interaction ledger record, truth watermark, evidence record classes и session envelope snapshot. MIRAGE FORENSIC ≠ SelfProtect FORENSIC: SP FORENSIC — CLAIM/DECISION/EFFECT_RECEIPT/ARTIFACT/GAP/CHECKPOINT
src/mirage/mirage_gc.cРеализация mirage / gc
src/mirage/mirage_ledger.cPlatX MIRAGE/HONEYPOT: interaction ledger implementation.
src/mirage/mirage_ledger.hPlatX MIRAGE/HONEYPOT: interaction ledger implementation. Реализует append-only hash-chained журнал взаимодействий поверх mirage_ledger_record_t (mirage_forensic.h). - Bounded ring: caller выделяет массив записей; переполнение НЕ теряет цепочку — старейшая запись вытесняется, а lost_before у следующей
src/mirage/mirage_metrics.cРеализация mirage / metrics
src/mirage/mirage_mil.cРеализация mirage / mil
src/mirage/mirage_oracle.hPlatX MIRAGE/HONEYPOT: Consistency Oracle. MIR0 wave: заморожен интерфейс Consistency Oracle (plat_mirage_oracle vtable). Oracle проверяет cross-view invariants: — до activate() (обязательно) — после каждой schema migration — опционально: heartbeat между interactions
src/mirage/mirage_oracle_basic.cPlatX MIRAGE/HONEYPOT: базовая реализация Consistency Oracle над world bundle.
src/mirage/mirage_oracle_basic.hPlatX MIRAGE/HONEYPOT: базовая реализация Consistency Oracle над world bundle. Реализует plat_mirage_oracle_t (mirage_oracle.h) поверх скомпилированного world bundle (mirage_bundle.h). Все 8 invariants проверяются по ground-truth таблице сущностей — Oracle не знает и не должен знать
src/mirage/mirage_probe.cРеализация mirage / probe
src/mirage/mirage_record.cРеализация mirage / record
src/mirage/mirage_seccomp.cРеализация mirage / seccomp
src/mirage/mirage_watchdog.cINV-MIRAGE-03: sandbox duration > MIRAGE_MAX_TTL → force kill
src/mirage/mirage_watermark.cPlatX MIRAGE/HONEYPOT: synthetic watermark derivation.
src/mirage/mirage_watermark.hPlatX MIRAGE/HONEYPOT: synthetic watermark derivation. watermark = H(seed_commitment || entity_id || world_id || generation) усечённую до MIR_WATERMARK_LEN (16 байт). ЗАЧЕМ ЭТО ОТДЕЛЬНЫЙ МОДУЛЬ До него формула существовала только в комментарии заголовка. Oracle
src/mirage/mirage_world.cРеализация mirage / world
src/mirage/mirage_world.hPlatX MIRAGE/HONEYPOT: world identity, ground-truth model, synthetic watermark и deception graph node. MIR0 wave: заморожены world identity fields, entity ground-truth types, seed derivation constants, stateful side-effect record и DGraph node. - Generation 0 — невалидно везде.
src/mirage/miragectl_main.cавтономный бинарь miragectl. gcc -std=c11 -Wall -DPLATX_HAS_MIRAGE -I include \ src/mirage/mirage_cli.c \ src/mirage/miragectl_main.c
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/mirage_host.h
/* platx/mirage_host.h — CLI host header для mirage.
 *
 * Включает реальный platx/platx_mirage.h и добавляет:
 *   — PLATX_HAS_MIRAGE (гарантирует, что типы доступны при CLI-сборке)
 *   — plat_mir_cli()  — точка входа CLI
 *   — plat_mir_register_console() — регистрация в консоли платформы
 *
 * Не переопределяет типы из platx_mirage.h.
 */
/* Включаем платформенный API. Если PLATX_HAS_MIRAGE не задан сборкой,
 * определяем здесь чтобы типы были видны в CLI-контексте. */
#define PLATX_HAS_MIRAGE

/* CLI entry point.
 * argc/argv начинаются с подкоманды (world|scenario|replay|oracle). */
int  plat_mir_cli(int argc, char **argv, FILE *out, FILE *err);

/* Регистрация глагола "mirage" в консоли платформы.
 * Вызывается из register_cmds модуля mirage. */
void plat_mir_register_console(void);
29

mission

Спецификация миссии и отдельное состояние её выполнения
src/mission/Управление и контракты5 файлов6 API headers

Mission связывает цель, область действия, полномочия, бюджеты и условия завершения. Спецификация описывает разрешённую работу, а состояние отражает наблюдаемый ход выполнения. Controller координирует переходы; Judge оценивает результат в пределах собственного источника истины.

Граница ответственности

  • Mission связывает цель, область действия, полномочия, бюджеты и условия завершения. Спецификация описывает разрешённую работу, а состояние отражает наблюдаемый ход выполнения. Controller координирует переходы; Judge оценивает результат в пределах собственного источника истины.

Устройство подсистемы

  • Plan IR описывает цель и допустимые действия в форме, пригодной для проверки до исполнения.
  • Admission связывает план с доступными полномочиями и бюджетами; FSM фиксирует переходы состояния миссии.
  • Act оформляет исполнение разрешённого шага, а Lessons сохраняет материал для последующего разбора.

Поток работы

  • Цель и контекст → план миссии.
  • Проверка области и ресурсов → допуск.
  • Контролируемые переходы → действия и наблюдения.
  • Критерий остановки → результат, lessons и debrief.

Отказ и восстановление

  • Исчерпание бюджета прекращает допуск новых действий.
  • Потеря провайдера и неизвестный исход сохраняются в состоянии миссии.
  • Оценка Judge отделена от самосвидетельства исполнителя.

Управление и диагностика

Домен не регистрирует собственную корневую команду. Его контракт используется вызывающими подсистемами через API и интерфейсы central, gadget.

Справочник CLI / mission →
Состав подсистемы / 5 файлов
Файл / компонентНазначение и граница
src/mission/mission_act.cA4-ACT adapter: Plan IR → Action Coordinator. Contract: include/platx/mission_act.h. There is no executor in this file. Every line that changes the world is a call into src/fabric/action_coord.c, which is claimed READ-ONLY by A4 and is not touched here. What this file adds is the four decisions the
src/mission/mission_admit.cA4-PLAN mission admission. Full validation before any mutable runtime exists. A refused spec must leave nothing behind: no id consumed, no snapshot half-written, no clock reading recorded. That is why the id counter is advanced last.
src/mission/mission_fsm.cA4-PLAN mission control state machine. The table below is exhaustive by construction: every cell is written, and an empty cell means INVALID, which the lookup reports as PLATX_MFSM_ERR_UNDEFINED. Nothing falls through to a default that executes. That is the difference between "this transition is not
src/mission/mission_lessons.cthe bounded lessons index. Contract: include/platx/mission_lessons.h. Nothing here stores anything. Every fact comes back out of the audit ring through its published query API, which is the same trail the Judge and the export reader see; a private copy would be a second source of truth, and
src/mission/mission_plan_ir.cA4-PLAN deterministic Plan IR. Admission, canonical serialisation, digest, bounded ordering. Everything here is a pure function of its arguments. There is no global state, no clock and no allocation, so the same plan admitted twice in two processes produces the same verdict and the same bytes. That property is
Контракты API / 6 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/mission_act.h
/* include/platx/mission_act.h — A4-ACT: the one way a Plan IR step becomes
 * an effect on the world.
 *
 * Owned by A4.  Promotion of tests/roadmap/agent4/action/act_dispatch_contract_v1.h
 * (RULE A4-ACT-001 v1).  PLATX_CORE_ABI-compatible; not a plat_abi_t extension.
 *
 * Why this file exists at all
 * ---------------------------
 * src/fabric/action_coord.c already owns idempotency, fencing, pinning and
 * reconcile for a *single descriptor*.  What it does not own — and must not,
 * because it predates the Plan IR and is claimed READ-ONLY by A4 — is the
 * question this header answers:
 *
 *     given an admitted Plan IR, which of its steps may reach a provider,
 *     under whose authority, exactly once, and what happens to the ones
 *     that were already dispatched when the operator pulls the switch.
 *
 * This adapter therefore adds NO second executor.  Every effect still goes
 * through action_coord_execute(); every undo through action_coord_rollback();
 * every restart through action_coord_reconcile().  What lives here is the
 * part the coordinator has no way to know:
 *
 *   step->descriptor  a closed mapping from platx_plan_op_t to action_verb_t.
 *                     READ_ONLY opcodes (OBSERVE, VERIFY) are refused rather
 *                     than mapped to a harmless verb: a plan that expects to
 *                     read must not be dispatched down the path that writes.
 *
 *   key derivation    the idempotency key is derived from (mission, plan
 *                     generation, step), so "the same step of the same plan"
 *                     is the same key on every host and after every restart,
 *                     and two different steps can never collide into one.
 *
 *   key binding       a key is bound to the descriptor hash it was first used
 *                     with.  The same key with a different target is a
 *                     conflict, not a replay.  The coordinator does not make
 *                     this distinction (see ACT-GAP-19); this layer does, and
 *                     refuses rather than returning the wrong receipt.
 *
 *   kill latch        one bit, set once, never cleared — there is deliberately
 *                     no function in this header that clears it.  After it is
 *                     set no new commit is admitted, through replan, through
 *                     reconnect, or through a fresh dispatch.  Cleanup of work
 *                     already in flight is a SEPARATE authority and keeps
 *                     working: C24-C28 make undo obligatory, and an operator
 *                     who has to choose between "stop" and "clean up" will
 *                     eventually stop pressing stop.
 *
 *   cancel ladder     CANCEL_REQUESTED, WAIT_TIMEOUT, DRAINING and TERMINAL
 *                     are four different things and are recorded as four
 *                     states.  Collapsing them is how a late completion finds
 *                     an owner that was already released.
 *
 *   quota             a bounded registry with a cleanup reserve.  Saturation
 *                     refuses new work; it never evicts a live transaction and
 *                     never takes the last slot away from an undo.
 *
 * VERSION: 1
 */

#define PLATX_MACT_VERSION   1u

/* Bounded registry.  A plan carries at most PLATX_PLAN_IR_STEPS_MAX steps,
 * and a mission may have one replan generation in flight while the previous
 * one drains, so the registry is sized for two plans and no more. */
#define PLATX_MACT_TXN_MAX   32u

/* Slots that a fresh dispatch may never consume.  They exist so that a
 * registry saturated by new work still has room to record the undo of work
 * that already changed the world. */
#define PLATX_MACT_CLEANUP_RESERVE_MIN 1u

typedef enum platx_mact_rc {
    PLATX_MACT_OK              = 0,
    PLATX_MACT_ERR_NULL        = 1,  /* argument missing                     */
    PLATX_MACT_ERR_VERSION     = 2,
    PLATX_MACT_ERR_STEP        = 3,  /* step index outside the plan          */
    PLATX_MACT_ERR_READ_ONLY   = 4,  /* opcode reads; it has no ACT verb     */
    PLATX_MACT_ERR_OPCODE      = 5,  /* opcode outside the closed list       */
    PLATX_MACT_ERR_EFFECT      = 6,  /* declared effect contradicts the verb  */
    PLATX_MACT_ERR_KILLED      = 7,  /* the switch is down; no new commit    */
    PLATX_MACT_ERR_CANCELLED   = 8,  /* this transaction was cancelled       */
    PLATX_MACT_ERR_FULL        = 9,  /* quota, or the cleanup reserve        */
    PLATX_MACT_ERR_KEY_CONFLICT= 10, /* same key, different descriptor       */
    PLATX_MACT_ERR_UNAVAILABLE = 11, /* provider cannot honour a needed leg  */
    PLATX_MACT_ERR_COORD       = 12, /* the coordinator refused; see status  */
    PLATX_MACT_ERR_NOT_FOUND   = 13,
    PLATX_MACT_ERR_STATE       = 14, /* not legal from the current state     */
    PLATX_MACT_ERR_NO_UNDO     = 15, /* irreversible; refusal is preserved   */
    PLATX_MACT_ERR_TOKEN       = 16, /* caller is not the coordinator        */
    PLATX_MACT_ERR_GENERATION  = 17, /* receipt belongs to a superseded plan */
    PLATX_MACT_ERR_BUFFER      = 18,
    PLATX_MACT_ERR_DIGEST      = 19  /* the plan is not the plan that was signed */
} platx_mact_rc_t;

/* Transaction lifecycle as this layer sees it.  The first five mirror
 * action_state_t; the last four are the ones the coordinator has no concept
 * of and that a cancel path needs to keep apart. */
typedef enum platx_mact_state {
    PLATX_MACT_ST_IDLE             = 0,
    PLATX_MACT_ST_DISPATCHED       = 1,  /* handed to the coordinator        */
    PLATX_MACT_ST_COMMITTED        = 2,  /* world changed, verify pending    */
    PLATX_MACT_ST_VERIFIED         = 3,  /* world read back and matched      */
    PLATX_MACT_ST_UNDONE           = 4,  /* rolled back to prior state       */
    PLATX_MACT_ST_FAILED           = 5,
    PLATX_MACT_ST_CANCEL_REQUESTED = 6,  /* asked to stop; backend still owns
                                          * the buffers                     */
    PLATX_MACT_ST_WAIT_TIMEOUT     = 7,  /* we stopped waiting; the backend
                                          * has NOT been told anything      */
    PLATX_MACT_ST_DRAINING         = 8,  /* kill switch: finishing cleanup   */
    PLATX_MACT_ST_TERMINAL         = 9,  /* released; nothing may touch it   */
    PLATX_MACT_ST_UNKNOWN          = 10, /* reconcile could not decide       */
    PLATX_MACT_ST_COUNT            = 11
} platx_mact_state_t;

/* What a provider can actually do, established by inspecting its vtable
 * rather than by trusting its description (P05-246). */
typedef struct platx_mact_conformance {
    uint32_t has_preview;
    uint32_t has_prepare;
    uint32_t has_commit;
    uint32_t has_verify;
    uint32_t has_rollback;
    uint32_t has_reconcile;
    uint32_t execute_ok;     /* preview+prepare+commit+verify all present   */
    uint32_t undo_ok;        /* rollback present                            */
    uint32_t reconcile_ok;   /* reconcile present                           */
} platx_mact_conformance_t;

typedef struct platx_mact_txn {
    uint32_t in_use;
    uint32_t state;              /* platx_mact_state_t                      */
    uint32_t step_id;
    uint32_t generation;
    uint64_t mission_id;
    uint32_t reversible;         /* preview said rollback restores it       */
    uint32_t undo_calls;         /* undo is idempotent; this counts asks    */
    char     key[ACTION_KEY_MAX];
    char     provider_id[PROV_ID_MAX];
    uint8_t  desc_hash[32];
    action_receipt_t receipt;
} platx_mact_txn_t;

typedef struct platx_mact {
    uint32_t version;
    uint32_t struct_size;

    action_coord_t *coord;       /* borrowed; this layer never owns it      */
    uint64_t coord_token;        /* what distinguishes us from the planner  */

    uint32_t killed;             /* terminal latch, set once, never cleared */
    uint32_t quota;              /* <= PLATX_MACT_TXN_MAX                   */
    uint32_t cleanup_reserve;

    /* Counters kept apart on purpose: one number that mixes refusals and
     * commits cannot answer "did saturation cost us an effect". */
    uint64_t submitted;
    uint64_t committed;
    uint64_t refused;
    uint64_t undone;
    uint64_t repeats;
    uint64_t terminal;

    platx_mact_txn_t txns[PLATX_MACT_TXN_MAX];
} platx_mact_t;

/* ── lifecycle ──────────────────────────────────────────────────────────── */

/* `coord` must already be initialised by action_coord_init().  `coord_token`
 * must be non-zero.  `quota` is clamped to PLATX_MACT_TXN_MAX and must leave
 * at least PLATX_MACT_CLEANUP_RESERVE_MIN slots reserved. */
platx_mact_rc_t platx_mact_init(platx_mact_t *m, action_coord_t *coord,
                                uint64_t coord_token, uint32_t quota,
                                uint32_t cleanup_reserve);

/* ── mapping ────────────────────────────────────────────────────────────── */

/* Closed mapping.  Returns PLATX_MACT_OK and writes an action_verb_t, or
 * PLATX_MACT_ERR_READ_ONLY / _OPCODE.  Never invents a verb. */
platx_mact_rc_t platx_mact_verb_of(uint32_t opcode, uint32_t *verb_out);

/* Canonical idempotency key for one step of one plan generation:
 *   "m<mission>.g<generation>.s<step_id>"
 * Two different steps cannot collide, and the same step after a restart
 * produces the same key.  Returns _BUFFER if `cap` is too small. */
platx_mact_rc_t platx_mact_key(uint64_t mission_id, uint32_t generation,
                               uint32_t step_id, char *out, size_t cap);

/* Build the descriptor for `ir->steps[step_idx]`.  `target` is the resolved
 * provider-specific name for step->target_id; resolution is the caller's, and
 * an empty target is refused rather than defaulted.  The descriptor_hash is
 * left zero: it is filled by platx_mact_preview so that a commit is pinned to
 * a preview that actually happened. */
platx_mact_rc_t platx_mact_descriptor(const platx_plan_ir_t *ir,
                                      uint32_t step_idx, uint64_t lease_id,
                                      const char *target,
                                      action_descriptor_t *out);

/* ── the plan the descriptors come from ────────────────────────────────
 *
 * Called before any descriptor is built.  The digest is taken over the
 * canonical serialisation (platx_planir_digest), which is exactly the byte
 * sequence every field below is read from: opcode, expected_effect,
 * authority_id, evidence_id, lease_gen, step_id, mission_id and generation.
 * So "the plan that was signed" and "the plan ACT executes" cannot diverge
 * without the digest changing.
 *
 * This is a comparison, not a signature check: who signed, and with which
 * key, is the signer capability's business (A4-P04-181).  What this settles
 * is the OTHER half — that the bytes covered by whatever signature is used
 * are the bytes ACT actually consumes. */
platx_mact_rc_t platx_mact_plan_admit(const platx_plan_ir_t *ir,
                                      const uint8_t expected_digest[32]);

/* ── the single dispatch path ───────────────────────────────────────────── */

/* Preview through the real coordinator.  Changes nothing, in any mode, and
 * fills `d->descriptor_hash` so that the following dispatch is pinned. */
platx_mact_rc_t platx_mact_preview(platx_mact_t *m, const char *provider_id,
                                   action_descriptor_t *d,
                                   action_preview_t *out, prov_status_t *st);

/* The one entry point that may change the world.  In order:
 *   kill latch → quota and cleanup reserve → key binding → coordinator.
 * On PLATX_MACT_OK, `*out` is the receipt the coordinator verified. */
platx_mact_rc_t platx_mact_dispatch(platx_mact_t *m, const char *provider_id,
                                    const action_descriptor_t *d,
                                    uint64_t mission_id, uint32_t generation,
                                    uint32_t step_id, action_receipt_t *out,
                                    prov_status_t *st);

/* Undo.  Idempotent: a second call on an already-undone transaction returns
 * PLATX_MACT_OK without asking the provider again.  Works while the kill
 * latch is down — that is the separate cleanup authority.  An irreversible
 * transaction is refused with _NO_UNDO and stays refused. */
platx_mact_rc_t platx_mact_undo(platx_mact_t *m, const char *provider_id,
                                const char *key, action_receipt_t *out,
                                prov_status_t *st);

/* ── cancel ladder ──────────────────────────────────────────────────────── */

platx_mact_rc_t platx_mact_cancel(platx_mact_t *m, const char *key);

/* We stop waiting.  The backend is NOT told and still owns its buffers, so
 * the transaction is not released here. */
platx_mact_rc_t platx_mact_timeout(platx_mact_t *m, const char *key);

/* The backend finally answered.  Legal after CANCEL_REQUESTED or
 * WAIT_TIMEOUT; refused with _STATE once the transaction is TERMINAL, which
 * is what keeps a late completion away from a released owner. */
platx_mact_rc_t platx_mact_late_completion(platx_mact_t *m, const char *key,
                                           int backend_ok);

/* Release.  Only legal once the backend has answered or the transaction was
 * never dispatched: releasing while a backend still owns the buffers is the
 * bug this whole ladder exists to prevent. */
platx_mact_rc_t platx_mact_release(platx_mact_t *m, const char *key);

/* ── kill switch ────────────────────────────────────────────────────────── */

/* Sets the latch and moves every non-terminal transaction to DRAINING.  There
 * is no inverse: a process that has been stopped is restarted, not un-stopped.
 * Returns the number of transactions left to drain in `*draining_out`.
 *
 * The latch is process-wide, not per-instance: a switch that a caller escapes
 * by constructing a second platx_mact_t — which is what a reconnect does — is
 * not a switch.  It does NOT survive a process restart; that needs durable
 * state and an owner who can lift it, and is recorded as ACT-GAP-22 rather
 * than invented here. */
platx_mact_rc_t platx_mact_kill(platx_mact_t *m, uint32_t *draining_out);

int platx_mact_is_killed(const platx_mact_t *m);

/* ── restart ────────────────────────────────────────────────────────────── */

/* Drive action_coord_reconcile and classify what came back.  A transaction
 * the provider reports as COMMITTED but not VERIFIED becomes
 * PLATX_MACT_ST_COMMITTED and is counted in `*needs_verify_out`; it never
 * becomes VERIFIED here.  A record the provider cannot describe becomes
 * PLATX_MACT_ST_UNKNOWN.  Neither is ever reported as success. */
/* How many adopted transactions are RESERVATIONS awaiting release (state
 * DRAINING) rather than effects awaiting verification.  Reconcile's
 * `needs_verify_out` counts only the latter: a caller told to verify a
 * reservation would be verifying an effect that never happened, and would
 * have no count telling it what to release instead.  A4-P06. */
uint32_t platx_mact_draining(const platx_mact_t *m);

platx_mact_rc_t platx_mact_reconcile(platx_mact_t *m, const char *provider_id,
                                     uint32_t *adopted_out,
                                     uint32_t *needs_verify_out,
                                     uint32_t *unknown_out,
                                     prov_status_t *st);

/* ── handoff to the mission machine ─────────────────────────────────────── */

/* Turn a verified ACT receipt into a mission receipt.  Refuses a receipt from
 * a superseded generation, and refuses without the coordinator token.  This
 * does NOT advance the mission and does NOT declare the mission successful:
 * only an independent validator does that (mission_fsm mark_validated). */
platx_mact_rc_t platx_mact_receipt(const platx_mact_t *m,
                                   const platx_mission_ctx_t *ctx,
                                   uint64_t caller_token, const char *key,
                                   platx_mission_receipt_t *out);

/* ── inspection ─────────────────────────────────────────────────────────── */

const platx_mact_txn_t *platx_mact_find(const platx_mact_t *m, const char *key);
uint32_t platx_mact_inflight(const platx_mact_t *m);

/* submitted - terminal.  A registry that leaks shows a balance that never
 * returns to zero, which is the only reason to keep the two counters apart. */
uint64_t platx_mact_balance(const platx_mact_t *m);

/* Vtable inspection, not self-description. */
platx_mact_rc_t platx_mact_conformance(const char *provider_id,
                                       platx_mact_conformance_t *out);

const char *platx_mact_rc_str(platx_mact_rc_t rc);
const char *platx_mact_state_str(platx_mact_state_t s);
include/platx/mission_admit.h
/* include/platx/mission_admit.h — A4-PLAN mission admission.
 *
 * Owned by A4.  Companion to include/platx/mission_spec.h, which describes
 * what a caller asks for; this header describes what the system agrees to.
 *
 * The distinction matters.  platx_mission_spec_t is caller-supplied memory:
 * it can be changed after the decision was made.  A mission_snapshot_t is
 * taken once, at admission, and carries the digest of the bytes that were
 * actually judged.  Every later question — "was this plan authorised?",
 * "has the deadline passed?" — is answered against the snapshot, never
 * against the caller's struct.
 *
 * One clock, one deadline
 * -----------------------
 * The deadline is computed once, at admission, from a monotonic reading.
 * Steps spend a single TTL; they do not restart it.  There is deliberately
 * no API to extend a deadline: an extension is a new mission with a new
 * digest, so that "the mission ran for six hours" cannot be produced by a
 * sequence of legal one-minute renewals nobody reviewed.
 *
 * VERSION: 1
 */

#define PLATX_MISSION_ADMIT_VERSION 1u

/* Serialised spec limits (A4-ADMIT-001). */
#define PLATX_MISSION_SPEC_WIRE_BYTES   40u     /* canonical, unpadded      */
#define PLATX_MISSION_GOALS_MAX         64u     /* bits available in mask   */
#define PLATX_MISSION_TTL_MIN_SECONDS   1u
#define PLATX_MISSION_TTL_MAX_SECONDS   86400u  /* 24 h                     */

typedef enum platx_mission_admit_rc {
    PLATX_MISSION_ADMIT_OK          = 0,
    PLATX_MISSION_ADMIT_ERR_NULL    = 1,
    PLATX_MISSION_ADMIT_ERR_VERSION = 2,
    PLATX_MISSION_ADMIT_ERR_SIZE    = 3,
    PLATX_MISSION_ADMIT_ERR_NO_GOAL = 4,
    PLATX_MISSION_ADMIT_ERR_TTL     = 5,
    PLATX_MISSION_ADMIT_ERR_BUDGET  = 6,
    PLATX_MISSION_ADMIT_ERR_RSVD    = 7,  /* reserved bytes not zero        */
    PLATX_MISSION_ADMIT_ERR_CLOCK   = 8,  /* monotonic reading unusable     */
    PLATX_MISSION_ADMIT_ERR_OVERFLOW= 9,  /* now + ttl does not fit         */
    PLATX_MISSION_ADMIT_ERR_METRIC  = 10  /* success metric unreachable     */
} platx_mission_admit_rc_t;

/* What was admitted.  Treated as read-only by every consumer; the runtime
 * keeps its mutable progress elsewhere (see the Action Coordinator). */
typedef struct platx_mission_snapshot {
    platx_mission_spec_t spec;          /* copy taken at admission          */
    uint8_t  digest[32];                /* SHA-256 of the canonical spec    */
    uint64_t admitted_mono_ns;          /* monotonic reading at admission   */
    uint64_t deadline_mono_ns;          /* admitted + ttl, computed once    */
    uint64_t mission_id;                /* monotone, never reused           */
    uint32_t snapshot_version;          /* PLATX_MISSION_ADMIT_VERSION      */
    uint32_t reserved;
} platx_mission_snapshot_t;

/* Canonical, unpadded, big-endian serialisation of a spec.  Returns bytes
 * written or -1.  This is what the digest is taken over. */
int platx_mission_spec_serialize(const platx_mission_spec_t *s,
                                 uint8_t *out, size_t cap);

/* Validation only: no state, no clock, no allocation.  Safe to call from a
 * caller that only wants to know why its spec would be refused. */
platx_mission_admit_rc_t platx_mission_spec_validate(
        const platx_mission_spec_t *s);

/* Admission.  `now_mono_ns` is supplied by the caller so the decision is
 * reproducible and so a test does not have to wait out a real TTL; the
 * production caller passes CLOCK_MONOTONIC.  On refusal `out` is left
 * untouched — a rejected mission leaves no half-built snapshot behind. */
platx_mission_admit_rc_t platx_mission_accept(const platx_mission_spec_t *s,
                                              uint64_t now_mono_ns,
                                              platx_mission_snapshot_t *out);

/* Remaining budget against the single admitted deadline.  0 once expired;
 * never negative, and never larger than it was a moment ago. */
uint64_t platx_mission_remaining_ns(const platx_mission_snapshot_t *snap,
                                    uint64_t now_mono_ns);

/* 1 when the mission has outlived its single TTL. */
int platx_mission_expired(const platx_mission_snapshot_t *snap,
                          uint64_t now_mono_ns);

const char *platx_mission_admit_rc_str(platx_mission_admit_rc_t rc);
include/platx/mission_fsm.h
/* include/platx/mission_fsm.h — A4-PLAN mission control state machine.
 *
 * Owned by A4.  Companion to plan_ir.h (what may be done) and
 * mission_admit.h (what was agreed to); this header is about who may say
 * that something happened.
 *
 * Two rules shape the whole interface.
 *
 *   The planner may not declare success.  Every state change that claims an
 *   effect has to arrive as a receipt from the Action Coordinator.  A
 *   planner that could write SUCCEEDED itself would make the audit trail a
 *   record of intentions, not of outcomes.
 *
 *   Attempt counts only rise.  There is no call that lowers one.  A retry
 *   budget that can be reset is not a budget, and "it eventually worked"
 *   would hide how many times it did not.
 *
 * An expired deadline is a defined input from every non-terminal state, not
 * an unhandled one.  A mission whose TTL runs out while it is waiting must
 * still reach a terminal state; otherwise it is neither running nor
 * finished, and nothing ever cleans it up.
 *
 * VERSION: 1
 */

#define PLATX_MISSION_FSM_VERSION 1u

typedef enum platx_mission_state {
    PLATX_MST_INVALID   = 0,
    PLATX_MST_OBSERVE   = 1,  /* gathering evidence; the world is read only */
    PLATX_MST_PLAN      = 2,  /* compiling a Plan IR                        */
    PLATX_MST_WAIT      = 3,  /* dispatched; awaiting a coordinator receipt */
    PLATX_MST_RESULT    = 4,  /* receipt in hand; comparing with expectation*/
    PLATX_MST_REPLAN    = 5,  /* outcome unsatisfactory; budget permitting  */
    PLATX_MST_SUCCEEDED = 6,  /* terminal: independently validated          */
    PLATX_MST_FAILED    = 7,  /* terminal: budget or attempts exhausted     */
    PLATX_MST_CANCELLED = 8,  /* terminal: cancelled by authority           */
    PLATX_MST_EXPIRED   = 9,  /* terminal: the single TTL ran out           */
    PLATX_MST_COUNT     = 10
} platx_mission_state_t;

typedef enum platx_mission_input {
    PLATX_MIN_NONE              = 0,
    PLATX_MIN_EVIDENCE_READY    = 1,
    PLATX_MIN_PLAN_BUILT        = 2,
    PLATX_MIN_DISPATCHED        = 3,
    PLATX_MIN_RECEIPT_OK        = 4,
    PLATX_MIN_RECEIPT_FAIL      = 5,
    PLATX_MIN_REPLAN_READY      = 6,
    PLATX_MIN_GOALS_VALIDATED   = 7,  /* external validator, not the planner */
    PLATX_MIN_CANCEL            = 8,
    PLATX_MIN_DEADLINE_EXPIRED  = 9,
    PLATX_MIN_BUDGET_EXHAUSTED  = 10,
    PLATX_MIN_COUNT             = 11
} platx_mission_input_t;

typedef enum platx_mission_fsm_rc {
    PLATX_MFSM_OK              = 0,
    PLATX_MFSM_ERR_NULL        = 1,
    PLATX_MFSM_ERR_UNDEFINED   = 2,  /* no transition; NOT a default execute */
    PLATX_MFSM_ERR_TERMINAL    = 3,  /* terminal states have no successor    */
    PLATX_MFSM_ERR_NO_RECEIPT  = 4,  /* effect claimed without a receipt     */
    PLATX_MFSM_ERR_STALE_GEN   = 5,  /* receipt from a superseded generation */
    PLATX_MFSM_ERR_REPLAY      = 6,  /* step already committed once          */
    PLATX_MFSM_ERR_BUDGET      = 7,  /* replan or attempt budget exhausted   */
    PLATX_MFSM_ERR_EXPIRED     = 8,  /* the mission deadline has passed      */
    PLATX_MFSM_ERR_AUTHORITY   = 9   /* caller is not the coordinator        */
} platx_mission_fsm_rc_t;

/* Proof that an effect actually happened.  Only the Action Coordinator
 * constructs one (platx_mission_receipt_issue below); the planner receives
 * it as an opaque value it cannot forge without the coordinator's token. */
typedef struct platx_mission_receipt {
    uint64_t mission_id;
    uint32_t generation;    /* plan generation this receipt belongs to     */
    uint32_t step_id;
    uint64_t coord_token;   /* the coordinator's authority, not the plan's */
    int32_t  applied;       /* 1 effect applied, 0 idempotent replay        */
    int32_t  outcome_ok;    /* the world matched the expectation           */
} platx_mission_receipt_t;

/* Mission runtime.  The planner holds a const pointer to this; only the
 * coordinator entry points below take it mutable. */
typedef struct platx_mission_ctx {
    platx_mission_snapshot_t snap;
    platx_mission_state_t    state;
    uint32_t generation;        /* current plan generation                 */
    uint32_t replans_used;
    uint32_t replan_budget;
    uint32_t attempts_used;     /* monotone; never lowered                 */
    uint32_t attempt_budget;
    uint32_t committed_mask;    /* step ids already committed, by bit      */
    uint32_t validated;         /* an external validator confirmed goals   */
    uint64_t coord_token;       /* 0 until a coordinator claims this ctx   */
    uint8_t  plan_digest[32];   /* digest of the plan currently in flight  */
} platx_mission_ctx_t;

/* Pure transition table lookup.  No context, no side effects: this answers
 * "is this transition defined", not "may this caller make it". */
platx_mission_fsm_rc_t platx_mission_fsm_next(platx_mission_state_t cur,
                                              platx_mission_input_t in,
                                              platx_mission_state_t *next_out);

int platx_mission_state_is_terminal(platx_mission_state_t s);
const char *platx_mission_state_str(platx_mission_state_t s);
const char *platx_mission_fsm_rc_str(platx_mission_fsm_rc_t rc);

/* --- coordinator-owned entry points ---------------------------------- */

/* Bind a runtime to an admitted snapshot.  `coord_token` must be non-zero:
 * it is what later distinguishes the coordinator from the planner. */
platx_mission_fsm_rc_t platx_mission_ctx_init(platx_mission_ctx_t *c,
                                              const platx_mission_snapshot_t *s,
                                              uint64_t coord_token,
                                              uint32_t replan_budget,
                                              uint32_t attempt_budget);

/* Issue a receipt.  Refuses without the coordinator token, so a planner
 * holding only the context cannot manufacture evidence of an effect. */
platx_mission_fsm_rc_t platx_mission_receipt_issue(
        const platx_mission_ctx_t *c, uint64_t coord_token,
        uint32_t step_id, int applied, int outcome_ok,
        platx_mission_receipt_t *out);

/* Advance the machine.  `rcpt` is required for inputs that claim an effect
 * (RECEIPT_OK / RECEIPT_FAIL) and must be NULL otherwise.  GOALS_VALIDATED
 * requires the coordinator token as well: nothing else may mark success. */
platx_mission_fsm_rc_t platx_mission_step(platx_mission_ctx_t *c,
                                          platx_mission_input_t in,
                                          const platx_mission_receipt_t *rcpt,
                                          uint64_t caller_token,
                                          uint64_t now_mono_ns);

/* Record that an independent validator confirmed the mission goals.  Kept
 * separate from platx_mission_step on purpose: the evidence of validation
 * and the state change that consumes it are two decisions, and only the
 * coordinator may make either.  Without this call GOALS_VALIDATED is
 * refused, so an unvalidated success stays unconfirmed. */
platx_mission_fsm_rc_t platx_mission_mark_validated(platx_mission_ctx_t *c,
                                                    uint64_t coord_token,
                                                    int validator_ok);

/* Open a new plan generation after a failure.  Refuses once the replan
 * budget is spent, and refuses a plan that repeats a committed step. */
platx_mission_fsm_rc_t platx_mission_replan(platx_mission_ctx_t *c,
                                            const platx_plan_ir_t *next_plan,
                                            uint64_t coord_token,
                                            uint64_t now_mono_ns);

/* --- planner-visible read-only view ----------------------------------- */

uint32_t platx_mission_attempts(const platx_mission_ctx_t *c);
uint32_t platx_mission_replans(const platx_mission_ctx_t *c);
platx_mission_state_t platx_mission_state(const platx_mission_ctx_t *c);
int platx_mission_step_committed(const platx_mission_ctx_t *c, uint32_t step_id);
include/platx/mission_lessons.h
/* include/platx/mission_lessons.h — A4-MEM: the bounded lessons index.
 *
 * Owned by A4.  Cards A4-P03-141…147, closed in wave 5 once ACT existed to
 * produce the records this reads.
 *
 * Why an index and not a store
 * ----------------------------
 * A "lesson" is the history of one step: what was dispatched, what came back,
 * and whether it had to be undone.  Every one of those facts is ALREADY in
 * the audit ring, written by the code that made the decision.  A second store
 * would be a copy that can disagree with the first, and when the two disagree
 * the operator has no way to tell which lied.  So this file stores nothing.
 * It reads the existing journal through its public query API and groups what
 * it finds by (mission, plan generation, step).
 *
 * Why bounded
 * -----------
 * The ring itself is bounded (PLATX_AUDIT_RING_SIZE, §MIL-9), and so is this:
 * a caller supplies the array and its capacity, and an index that would
 * exceed it is truncated with the truncation REPORTED, never silently.  An
 * index whose size depends on how long the system has been running is not a
 * bounded construction, whatever its average case looks like.
 *
 * What "complete chain" means (A4-P03-146/147)
 * --------------------------------------------
 * R7 acceptance asks whether the trail of an EXECUTED step contains all its
 * mandatory phases.  Here that is a predicate over the phases actually found,
 * not a flag anyone sets:
 *
 *   dispatched and verified   → DISPATCH and RECEIPT must both be present
 *   dispatched and undone     → DISPATCH, RECEIPT and UNDO
 *   refused                   → REFUSE alone is complete; nothing happened,
 *                               and demanding a receipt for it would make the
 *                               absence of an effect look like a gap
 *
 * A step with a DISPATCH and no RECEIPT is INCOMPLETE — which is exactly the
 * state a crash between commit and verify leaves, and it must not be readable
 * as either success or failure.
 *
 * VERSION: 1
 */

#define PLATX_LESSONS_VERSION  1u

/* Bounded by construction: one plan is at most PLATX_PLAN_IR_STEPS_MAX steps
 * (16), and a mission may hold one generation while another drains. */
#define PLATX_LESSONS_MAX      32u

/* Phases, as found in the trail.  A bitmask so that "which phase is missing"
 * is answerable, which "complete: yes/no" is not. */
#define PLATX_LESSON_PH_DISPATCH  (1u << 0)
#define PLATX_LESSON_PH_RECEIPT   (1u << 1)
#define PLATX_LESSON_PH_UNDO      (1u << 2)
#define PLATX_LESSON_PH_REFUSE    (1u << 3)

typedef enum platx_lesson_outcome {
    PLATX_LESSON_UNKNOWN    = 0,  /* nothing interpretable in the trail     */
    PLATX_LESSON_VERIFIED   = 1,  /* dispatched, receipt says the world matched */
    PLATX_LESSON_UNDONE     = 2,  /* applied and rolled back                */
    PLATX_LESSON_REFUSED    = 3,  /* never reached a provider               */
    PLATX_LESSON_INCOMPLETE = 4,  /* dispatched, no receipt: NOT a result   */
    PLATX_LESSON_FAILED     = 5   /* receipt says the world did not match   */
} platx_lesson_outcome_t;

typedef struct platx_lesson {
    uint64_t mission_id;
    uint32_t plan_gen;
    uint16_t step_id;
    uint16_t op;             /* platx_plan_op_t / action_verb_t as recorded */
    uint32_t phases;         /* PLATX_LESSON_PH_*                           */
    uint32_t outcome;        /* platx_lesson_outcome_t                      */
    uint32_t refuse_code;    /* the exact refusal, 0 when not refused       */
    uint32_t records;        /* audit records that contributed              */
} platx_lesson_t;

typedef enum platx_lessons_rc {
    PLATX_LESSONS_OK          = 0,
    PLATX_LESSONS_ERR_NULL    = 1,
    PLATX_LESSONS_ERR_BOUND   = 2,  /* capacity is zero or above the cap    */
    PLATX_LESSONS_TRUNCATED   = 3,  /* more steps than capacity; reported   */
    PLATX_LESSONS_ERR_EMPTY   = 4   /* nothing to export; refused, see below */
} platx_lessons_rc_t;

/* Build the index for one mission from the audit ring.  Reads only; writes
 * nothing anywhere.  `*n_out` is how many entries were filled.  Returns
 * PLATX_LESSONS_TRUNCATED — not OK — when the mission has more steps in the
 * trail than `max`, so a caller can never mistake a clipped index for a
 * complete one. */
platx_lessons_rc_t platx_lessons_index(uint64_t mission_id,
                                       platx_lesson_t *out, uint32_t max,
                                       uint32_t *n_out);

/* The R7 acceptance predicate over one lesson: 1 = the mandatory phases for
 * what this step actually did are all present. */
int platx_lesson_chain_complete(const platx_lesson_t *l);

/* Which phases a complete chain would need for this lesson's outcome.  Lets a
 * caller say WHICH phase is missing rather than only that one is. */
uint32_t platx_lesson_required_phases(const platx_lesson_t *l);

/* ── health and status, tied to the same trail ─────────────────────────── */

typedef struct platx_lessons_status {
    uint64_t cursor;          /* audit_ring_cursor() at the time of the call */
    uint32_t steps;           /* steps found for this mission                */
    uint32_t complete;        /* steps whose mandatory phases are all present*/
    uint32_t incomplete;      /* dispatched with no receipt                  */
    uint32_t refused;
    uint32_t undone;
    uint32_t truncated;       /* 1 = the index did not fit                   */
} platx_lessons_status_t;

/* Status of one mission's trail.  `complete == steps` is the condition an R7
 * acceptance asks about; it is computed here rather than asserted by whoever
 * would like it to be true. */
platx_lessons_rc_t platx_lessons_status(uint64_t mission_id,
                                        platx_lessons_status_t *out);

/* Export the journal to `path`, refusing an EMPTY export.
 *
 * The refusal is the point.  The legacy check-audit set contains a case that
 * passes on an empty export (GAP-18), which means a broken exporter and a
 * quiet one are indistinguishable to it.  A consumer that asks for evidence
 * and is handed nothing has been told nothing, and should hear so. */
platx_lessons_rc_t platx_lessons_export(const char *path, uint64_t from_seq,
                                        uint64_t count);

const char *platx_lessons_rc_str(platx_lessons_rc_t rc);
const char *platx_lesson_outcome_str(platx_lesson_outcome_t o);
include/platx/mission_spec.h
/* include/platx/mission_spec.h — A4-CONTRACT canonical mission spec header.
 * PLATX_CORE_ABI-compatible. Owned by A4. Not a plat_abi_t extension.
 * VERSION: 0x00010000 (1.0)
 */

#define PLATX_MISSION_SPEC_VERSION 0x00010000u

typedef struct {
    uint32_t spec_version;    /* PLATX_MISSION_SPEC_VERSION */
    uint32_t struct_size;     /* sizeof(platx_mission_spec_t) */
    uint64_t goal_mask;       /* bitmask of active objectives */
    uint32_t success_metric;  /* threshold for MISSION_SUCCESS */
    uint32_t ttl_seconds;     /* max mission lifetime */
    uint32_t budget_cycles;   /* max CPU cycles (0 = unlimited) */
    uint32_t budget_memory;   /* max memory KB (0 = unlimited) */
    uint8_t  kill_switch;     /* 1 = abort on first violation */
    uint8_t  reserved[3];
} platx_mission_spec_t;
include/platx/plan_ir.h
/* include/platx/plan_ir.h — A4-PLAN canonical Plan IR.
 *
 * Promotion of tests/roadmap/agent4/contracts/plan_ir_contract_v1.h
 * (RULE A4-PLANIR-001 v1, published in wave 1) to a product header.
 * Owned by A4.  PLATX_CORE_ABI-compatible.  Not a plat_abi_t extension.
 *
 * Why a struct and not text
 * -------------------------
 * A plan carried as a shell string can be neither checked before execution
 * nor reproduced after it.  Validating it means parsing a language that can
 * express anything, and its digest changes with a space.  Here a plan is an
 * array of fixed records whose opcodes come from a closed list.
 *
 * Why no pointers
 * ---------------
 * A native pointer makes a plan neither portable nor signable: it means
 * different things in two processes and nothing in a file.  Targets are
 * named by identifiers the executor resolves, not by addresses.
 *
 * Why the digest is taken from a canonical serialisation
 * ------------------------------------------------------
 * A digest over the in-memory struct depends on compiler padding, so "the
 * same plan" would carry two different digests on two builds.  The wire
 * form below is big-endian, unpadded, fixed field order.
 *
 * VERSION: 1
 */

#define PLATX_PLAN_IR_VERSION   1u
#define PLATX_PLAN_IR_STEPS_MAX 16u   /* bounded: a plan that cannot be read
                                       * in full cannot be admitted either */

/* Closed opcode list.  Extension = a new codec version, not "one more case
 * in the switch".  An unknown opcode is refused, never skipped. */
typedef enum platx_plan_op {
    PLATX_OP_NONE       = 0,
    PLATX_OP_OBSERVE    = 1,   /* reads state; changes nothing            */
    PLATX_OP_QUARANTINE = 2,
    PLATX_OP_FREEZE     = 3,
    PLATX_OP_ISOLATE    = 4,
    PLATX_OP_REVOKE     = 5,
    PLATX_OP_COLLECT    = 6,
    PLATX_OP_VERIFY     = 7,   /* reads the world back                    */
    PLATX_OP_MAX
} platx_plan_op_t;

/* Expected effect.  A step that does not say what it expects cannot be
 * checked against what happened. */
typedef enum platx_plan_effect {
    PLATX_EFFECT_NONE       = 0,
    PLATX_EFFECT_READ_ONLY  = 1,
    PLATX_EFFECT_REVERSIBLE = 2,
    PLATX_EFFECT_TERMINAL   = 3,  /* cannot be undone; needs explicit grant */
    PLATX_EFFECT_MAX
} platx_plan_effect_t;

/* One step.  Every field is a value: no pointer, no string. */
typedef struct platx_plan_step {
    uint32_t step_id;          /* 1..PLATX_PLAN_IR_STEPS_MAX, unique       */
    uint32_t opcode;           /* platx_plan_op_t                          */
    uint32_t prereq_mask;      /* bits of (step_id-1) that must succeed    */
    uint32_t expected_effect;  /* platx_plan_effect_t                      */
    uint64_t target_id;        /* target identifier, NOT an address        */
    uint64_t authority_id;     /* who authorised this particular step      */
    uint64_t evidence_id;      /* observation this step is grounded in     */
    uint64_t lease_gen;        /* lease generation this step is fenced to  */
} platx_plan_step_t;

typedef struct platx_plan_ir {
    uint32_t ir_version;       /* PLATX_PLAN_IR_VERSION                    */
    uint32_t struct_size;      /* sizeof(platx_plan_ir_t)                  */
    uint64_t mission_id;
    uint64_t seed;             /* the only source of ordering freedom      */
    uint32_t generation;       /* replan generation; 0 for the first plan  */
    uint32_t n_steps;
    platx_plan_step_t steps[PLATX_PLAN_IR_STEPS_MAX];
} platx_plan_ir_t;

typedef enum platx_planir_rc {
    PLATX_PLANIR_OK               = 0,
    PLATX_PLANIR_ERR_NULL         = 1,
    PLATX_PLANIR_ERR_VERSION      = 2,
    PLATX_PLANIR_ERR_SIZE         = 3,
    PLATX_PLANIR_ERR_EMPTY        = 4,
    PLATX_PLANIR_ERR_TOO_MANY     = 5,
    PLATX_PLANIR_ERR_UNKNOWN_OP   = 6,
    PLATX_PLANIR_ERR_BAD_EFFECT   = 7,
    PLATX_PLANIR_ERR_DUP_STEP     = 8,
    PLATX_PLANIR_ERR_BAD_STEP_ID  = 9,
    PLATX_PLANIR_ERR_PREREQ_RANGE = 10,
    PLATX_PLANIR_ERR_SELF_PREREQ  = 11,
    PLATX_PLANIR_ERR_CYCLE        = 12,
    PLATX_PLANIR_ERR_NO_AUTHORITY = 13,
    PLATX_PLANIR_ERR_NO_TARGET    = 14,
    PLATX_PLANIR_ERR_BUFFER       = 15,
    PLATX_PLANIR_ERR_NO_EVIDENCE  = 16,  /* step not grounded in an observation */
    PLATX_PLANIR_ERR_DEPTH        = 17   /* dependency chain deeper than the cap */
} platx_planir_rc_t;

/* Canonical wire form: header + n_steps x step, big-endian, no padding. */
#define PLATX_PLANIR_HDR_BYTES   28u  /* ver(4) mission_id(8) seed(8)
                                       * generation(4) n_steps(4)          */
#define PLATX_PLANIR_STEP_BYTES  48u  /* 4+4+4+4+8+8+8+8                      */
#define PLATX_PLANIR_WIRE_MAX \
    (PLATX_PLANIR_HDR_BYTES + PLATX_PLAN_IR_STEPS_MAX * PLATX_PLANIR_STEP_BYTES)

/* Longest dependency chain a plan may declare.  A deeper chain is refused
 * rather than walked: bounded construction is the point of the limit. */
#define PLATX_PLANIR_DEPTH_MAX  8u

/* Admission of a Plan IR.  Pure: touches nothing outside `ir`. */
platx_planir_rc_t platx_planir_admit(const platx_plan_ir_t *ir);

/* Canonical serialisation.  Returns bytes written, or -1 on refusal.
 * The same accepted plan yields the same bytes on every build. */
int platx_planir_serialize(const platx_plan_ir_t *ir, uint8_t *out, size_t cap);

/* SHA-256 over the canonical serialisation, never over the struct. */
int platx_planir_digest(const platx_plan_ir_t *ir, uint8_t out[32]);

/* Topological order of step indices honouring prereq_mask, tie-broken by
 * step_id.  Deterministic: the seed does not reorder equal candidates, it
 * only selects among alternatives offered by the caller.  Returns the count
 * written, or -1 on refusal. */
int platx_planir_order(const platx_plan_ir_t *ir, uint32_t *out, size_t cap);

/* Longest dependency chain in the plan.  -1 on refusal. */
int platx_planir_depth(const platx_plan_ir_t *ir);

const char *platx_planir_rc_str(platx_planir_rc_t rc);
30

modhost

Размещение, проверка и изолированное исполнение модулей
src/modhost/Исполнение и расширения17 файлов1 API headers

ModHost управляет границей между артефактом модуля и его рабочим экземпляром. Идентичность поставки, разрешение профиля, проверка байтов и условия запуска рассматриваются вместе. Перезагрузка создаёт новое поколение, а завершение освобождает ресурсы и отзывает прежние связи.

Граница ответственности

  • ModHost управляет границей между артефактом модуля и его рабочим экземпляром. Идентичность поставки, разрешение профиля, проверка байтов и условия запуска рассматриваются вместе. Перезагрузка создаёт новое поколение, а завершение освобождает ресурсы и отзывает прежние связи.

Устройство подсистемы

  • регистрация глагола `modhost` в консоли платформы.
  • Module Host: full PLUG archive lifecycle. State machine: VERIFY → STAGE → CREATE → START → PROVIDE → COMMIT → RUNNING soft-blacklist the artifact_id. Concurrency: mh_ctx_t.mu protects the instance and binding tables. All public functions acquire/release it around table mutations.
  • Atomic hot-swap and shadow activation (TZ_RA2C_DYNMOD_V2). 1. Find old RUNNING instance by artifact id. 2. Load new archive (full VERIFY→COMMIT lifecycle via modhost_load). 3. Transfer state: old ops->save_state() → new ops->load_state(). 4. Set old instance DRAINING; optionally observe shadow window.
Архитектурные детали и инварианты

Invariants

**INV-MODHOST-01** | Module without valid MSX signature → REFUSE

**INV-MODHOST-02** | Module without lease → REFUSE

**INV-MODHOST-03** | Unload failure → force unload + panic audit entry

Perl integration

Task 5.49 calls for perl/lib/Platx/ModHost.pm; the perl/ directory is

outside Agent 3's work zone (§DO-NOT-TOUCH). A stub must be created by the

Perl owner (Agent 0 / project maintainer).

Управление и диагностика

Корневые команды: modhost. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / modhost →
Состав подсистемы / 17 файлов
Файл / компонентНазначение и граница
src/modhost/cmd_modhost.cрегистрация глагола `modhost` в консоли платформы.
src/modhost/modhost.cModule Host: full PLUG archive lifecycle. State machine: VERIFY → STAGE → CREATE → START → PROVIDE → COMMIT → RUNNING soft-blacklist the artifact_id. Concurrency: mh_ctx_t.mu protects the instance and binding tables. All public functions acquire/release it around table mutations.
src/modhost/modhost.hModule Host: PLUG archive lifecycle manager. Implements the VERIFY→STAGE→CREATE→START→PROVIDE→COMMIT state machine Security invariants: - PLUG archive verified before any memory allocation for the payload. - No disk write: ELF images go through plugin_load_from_memory (anon mmap).
src/modhost/modhost_audit.cРеализация modhost / audit
src/modhost/modhost_cli.cCLI загрузчика модулей платформы. Использует реальный platx/platx_modhost.h: modhost_inventory_count() — число слотов modhost_inventory_find() — поиск по имени modhost_sandbox_run() — rehearsal в sandbox (mirage) modhost_lockdown_check() — проверка lockdown (TL4)
src/modhost/modhost_hotswap.cAtomic hot-swap and shadow activation (TZ_RA2C_DYNMOD_V2). 1. Find old RUNNING instance by artifact id. 2. Load new archive (full VERIFY→COMMIT lifecycle via modhost_load). 3. Transfer state: old ops->save_state() → new ops->load_state(). 4. Set old instance DRAINING; optionally observe shadow window.
src/modhost/modhost_internal.hshared statics for modhost compilation units. Included ONLY by modhost.c and modhost_hotswap.c; not exported.
src/modhost/modhost_inventory.cРеализация modhost / inventory
src/modhost/modhost_metrics.cРеализация modhost / metrics
src/modhost/modhost_policy.cLease enforcement, call tick, and artifact burn list. modhost_policy_check_lease — sanity-check lease parameters vs policy ceilings modhost_policy_check_binding — enforce TTL / call_budget / error_rate_ppm modhost_call_tick — account one call against a binding
src/modhost/modhost_reload.cРеализация modhost / reload
src/modhost/modhost_sandbox.cРеализация modhost / sandbox
src/modhost/modhost_sense.cSENSE governor implementation (TZ_RA2C_DYNMOD_V2).
src/modhost/modhost_sense.hSENSE governor: periodic health monitoring for Module Host. The governor runs a background thread that polls every running instance in the Module Host context. For each instance it aggregates error-rate metrics across all active bindings and classifies the instance health as
src/modhost/modhost_store.cIn-memory Artifact Store (keyed by artifact_id). Stores verified PLUG archive blobs in process memory, keyed by their SHA-256 content address (artifact_id_t). No disk write is performed — received archive to any persistent medium in the transport path. modhost_store_init — reset the store (call once at startup / test)
src/modhost/modhost_verify.cPLUG archive VERIFY phase (10 checks). 1. magic == PLUG_MAGIC, version == PLUG_VERSION 2. sz sha256 4. nfiles > 0 and index region fits within archive 5. each entry offset+size within data region (no header/index overlap)
src/modhost/modhostctl_main.cавтономный бинарь modhostctl. gcc -std=c11 -Wall -I include \ src/modhost/modhost_cli.c \ src/modhost/modhostctl_main.c
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/modhost_host.h
/* platx/modhost_host.h — CLI host header для modhost.
 *
 * Включает реальный platx/platx_modhost.h и добавляет:
 *   — plat_mod_get(idx) — итерация слотов inventory по индексу
 *     (реальный API даёт только find(name) и count(); итерация — слабая заглушка)
 *   — plat_mod_cli() — точка входа CLI
 *   — plat_mod_register_console() — регистрация глагола
 */
/* Получить слот по индексу (0 .. modhost_inventory_count()-1).
 * Возвращает указатель на слот или NULL если idx вне диапазона.
 * Слабая заглушка работает со своей статической таблицей.
 * Реальная реализация возвращает указатель на внутренний массив slots[]. */
modhost_slot_t *plat_mod_get(int idx);

/* CLI entry point.
 * argc/argv начинаются с подкоманды (list|verify|rehearse). */
int  plat_mod_cli(int argc, char **argv, FILE *out, FILE *err);

/* Регистрация глагола "modhost" в консоли платформы. */
void plat_mod_register_console(void);
31

msx

Язык сценариев и автоматизация операций платформы
src/msx/Исполнение и расширения14 файлов3 API headers

MSX — ограниченный automation runtime PLATX. Он парсит/выполняет scripts, подписывает/verifies assets и планирует resident after/every jobs через Core Task API. Он использует platform operations, но не владеет lifecycle/resources напрямую.

Граница ответственности

  • Grammar/namespaces/builtins allowlisted; no arbitrary native pointer/plugin import.
  • Resident job owner — script module/instance generation; reload cancels/joins old jobs.

Устройство подсистемы

  • Lexer/parser создают source-located AST; validator resolves names/types/permissions до execution.
  • Interpreter имеет explicit value/error model, budgets steps/memory/depth/time.
  • Builtin registry связывает name/version/risk с typed platform operation.
  • Scheduler adapter after/every создаёт managed tasks/timers и correlation context.

Поток работы

  • Source/signature → lex/parse/validate.
  • Execution context + leases → statements/builtins.
  • Operation results/events → script values.
  • Completion/cancel/reload → join/wipe/report.

Отказ и восстановление

  • Parse/validation no side effects.
  • Budget exhausted returns typed error and cancels pending operations.
  • Reload generation makes old timer callbacks stale.
  • Builtin unavailable profile → explicit error, not hidden shell fallback.

Основные возможности

  • Lexer, parser, AST and builtin execution pipeline.
  • Signed/obfuscated/binary token tooling.
  • Resident after/every jobs now route through Core task API for ownership/recovery.
Архитектурные детали и инварианты

Invariant

**INV-MSX-01:** Any module whose signing key fingerprint is on the revocation

list is immediately unloaded (via modhost_inventory_remove).

Управление и диагностика

Корневые команды: script, ms, msx. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / msx →
Состав подсистемы / 14 файлов
Файл / компонентНазначение и граница
src/msx/cmd_msx.cnamespace "script" (алиасы "ms", "msx"): интерпретатор .ms. script [-- арг...] выполнить сценарий из файла script --eval "" выполнить строку script --check проверить синтаксис без выполнения script --repl интерактивный режим
src/msx/msx.hпредметно-ориентированный язык (DSL) платформы: публичный API. Императивный интерпретируемый язык сценариев (файлы .ms): динамическая типизация, функции и замыкания, массивы/объекты, if/while/for/foreach, try/catch, интерполяция строк, песочница с лимитами. Расширяется нативными
src/msx/msx_builtins.cвстроенные функции DSL и биндинги к платформе. Регистрирует глобальные функции (log/print/len/type/json/base64/hex/sha256/ string/sleep/exit/time) и namespace-объекты (log, time, net) с привязкой к реальным подсистемам платформы (сетевой стек через реестр netstack).
src/msx/msx_engine.cядро DSL: лексер, парсер (рекурсивный спуск), AST, значения с подсчётом ссылок, окружения/замыкания, tree-walking интерпретатор, песочница, JSON. Без внешних зависимостей. См. script.h.
src/msx/msx_launch.cраспознавание и запуск .ms-сценариев самим бинарём платформы. Раньше единственным способом выполнить сценарий было ./memfd --batch --command "script file.ms" — сценарий не мог быть исполняемым файлом и не мог получить аргументы. Здесь добавлены две короткие формы (см. msx.h), а разбор и песочница
src/msx/msx_launch.hплатформенный бинарь в роли интерпретатора языка .ms. Отдельного интерпретатора нет: сценарии исполняет сам memfd. Invariant: MSX observes. It does not recovery decide and does not pick ./memfd ./msxscript/test.ms арг1 арг2 запуск по имени файла ./msx ./msxscript/test.ms арг1 арг2 запуск через симлинк
src/msx/msx_mil.cРеализация msx / mil
src/msx/msx_mod.cthin platx adapt for the existing script engine. Invariant: this file is not a language. Lexer/parser/VM stay in msx_engine.c. Restart is plat_recovery_choose/apply → plat_lifecycle_request only. After STOP the cap is gone. After START it is provided again — empty,
src/msx/msx_mod.hexisting .ms engine as a platx module. Not a new language.
src/msx/msx_modhost.cMSX "modhost" namespace bindings (TZ_RA2C_DYNMOD_V2).
src/msx/msx_modhost.hMSX scripting bindings for the Module Host. Exposes the Module Host lifecycle and SENSE probe as a "modhost" namespace callable from .ms scripts: modhost.load(artifact_hex) → "ok" | error string modhost.unload(artifact_hex) → "ok" | error string
src/msx/msx_revoke.cРеализация msx / revoke
src/msx/msx_rotate.cРеализация msx / rotate
src/msx/msx_timer.cbounded resident event.after/every scheduler. One scheduler task owns all timers. It is born only through abi->task. Callbacks drain serially, so ticks missed while an every callback is still running are counted and dropped.
Контракты API / 3 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/artifact_buffer.h
/* platx/artifact_buffer.h — cross-platform anonymous buffer contract.
 *
 * The provider is selected by capability artifact.buffer:v1:
 * Linux uses a sealed memfd, Windows uses a paging-file-backed section.
 * No backend handle (fd/HANDLE) crosses this ABI.
 */

#define PLAT_CAP_ARTIFACT_BUFFER          "artifact.buffer"
#define PLAT_ARTIFACT_BUFFER_V1           0x00010000u
#define PLAT_ARTIFACT_BUFFER_ABI          1u
#define PLAT_ARTIFACT_BUFFER_NAME_MAX     96u
#define PLAT_ARTIFACT_BUFFER_REASON_MAX  160u
#define PLAT_ARTIFACT_BUFFER_DEFAULT_MAX (16u * 1024u * 1024u)
#define PLAT_ARTIFACT_BUFFER_HARD_MAX    (64u * 1024u * 1024u)

#define PLAT_ARTIFACT_BUFFER_SEAL_SHRINK       (1u << 0)
#define PLAT_ARTIFACT_BUFFER_SEAL_GROW         (1u << 1)
#define PLAT_ARTIFACT_BUFFER_SEAL_WRITE        (1u << 2)
#define PLAT_ARTIFACT_BUFFER_SEAL_FUTURE_WRITE (1u << 3)
#define PLAT_ARTIFACT_BUFFER_SEAL_FINAL        (1u << 4)
#define PLAT_ARTIFACT_BUFFER_SEAL_IMMUTABLE    \
    (PLAT_ARTIFACT_BUFFER_SEAL_SHRINK |         \
     PLAT_ARTIFACT_BUFFER_SEAL_GROW |           \
     PLAT_ARTIFACT_BUFFER_SEAL_WRITE |          \
     PLAT_ARTIFACT_BUFFER_SEAL_FUTURE_WRITE |   \
     PLAT_ARTIFACT_BUFFER_SEAL_FINAL)

typedef enum plat_artifact_buffer_backend {
    PLAT_ARTIFACT_BUFFER_BACKEND_NONE = 0,
    PLAT_ARTIFACT_BUFFER_BACKEND_MEMFD = 1,
    PLAT_ARTIFACT_BUFFER_BACKEND_WIN_SECTION = 2
} plat_artifact_buffer_backend_t;

typedef enum plat_artifact_buffer_reason {
    PLAT_ARTIFACT_BUFFER_OK = 0,
    PLAT_ARTIFACT_BUFFER_INVALID_ARGUMENT,
    PLAT_ARTIFACT_BUFFER_POLICY_DENIED,
    PLAT_ARTIFACT_BUFFER_OWNER_UNAVAILABLE,
    PLAT_ARTIFACT_BUFFER_OWNER_STALE,
    PLAT_ARTIFACT_BUFFER_LEASE_REQUIRED,
    PLAT_ARTIFACT_BUFFER_LEASE_STALE,
    PLAT_ARTIFACT_BUFFER_NOT_FOUND,
    PLAT_ARTIFACT_BUFFER_TABLE_FULL,
    PLAT_ARTIFACT_BUFFER_SIZE_LIMIT,
    PLAT_ARTIFACT_BUFFER_UNSUPPORTED,
    PLAT_ARTIFACT_BUFFER_CREATE_FAILED,
    PLAT_ARTIFACT_BUFFER_IO_FAILED,
    PLAT_ARTIFACT_BUFFER_SEALED,
    PLAT_ARTIFACT_BUFFER_SEAL_FAILED,
    PLAT_ARTIFACT_BUFFER_BUSY,
    PLAT_ARTIFACT_BUFFER_RESOURCE_FAILED
} plat_artifact_buffer_reason_t;

typedef struct plat_artifact_buffer_owner {
    uint64_t module_id;
    uint64_t instance_id;
    uint64_t generation;
} plat_artifact_buffer_owner_t;

typedef struct plat_artifact_buffer_ref {
    uint64_t artifact_id;
    uint64_t lease_id;
    plat_artifact_buffer_owner_t owner;
} plat_artifact_buffer_ref_t;

typedef struct plat_artifact_buffer_create_req {
    uint32_t abi_version;
    uint32_t struct_size;
    const char *name;
    size_t initial_size;
    size_t max_size;
    plat_artifact_buffer_owner_t owner;
} plat_artifact_buffer_create_req_t;

typedef struct plat_artifact_buffer_result {
    uint32_t abi_version;
    uint32_t struct_size;
    uint32_t reason_code; /* plat_artifact_buffer_reason_t */
    uint32_t applied;
    uint32_t backend;     /* plat_artifact_buffer_backend_t */
    uint32_t seals;
    uint32_t closed;
    uint32_t reserved;
    plat_artifact_buffer_ref_t ref;
    uint64_t size;
    char reason[PLAT_ARTIFACT_BUFFER_REASON_MAX];
} plat_artifact_buffer_result_t;

typedef struct plat_artifact_buffer_v1 {
    uint32_t struct_size;
    uint32_t abi_version;
    int (*create)(const plat_artifact_buffer_create_req_t *req,
                  plat_artifact_buffer_result_t *out);
    int (*write)(plat_artifact_buffer_ref_t ref,
                 const void *data, size_t bytes, uint64_t offset,
                 size_t max_size, plat_artifact_buffer_result_t *out);
    int (*seal)(plat_artifact_buffer_ref_t ref, uint32_t seals,
                plat_artifact_buffer_result_t *out);
    int (*close)(plat_artifact_buffer_ref_t ref,
                 plat_artifact_buffer_result_t *out);
    int (*release_owner)(plat_artifact_buffer_owner_t owner);
    const char *(*reason_name)(uint32_t reason);
    const char *(*backend_name)(void);
} plat_artifact_buffer_v1_t;

#define PLAT_ARTIFACT_BUFFER_V1_BASE_SIZE \
    (offsetof(plat_artifact_buffer_v1_t, backend_name) + \
     sizeof(((plat_artifact_buffer_v1_t *)0)->backend_name))

/* Platform-selected provider accessor.  Capability registries publish the
 * returned immutable vtable under artifact.buffer:v1. */
const plat_artifact_buffer_v1_t *plat_artifact_buffer_provider_v1(void);
include/platx/msx_lease_shim.h
/* platx/msx_lease_shim.h — INT-138: MSX/DSL requests through Lease gate only.
 * Never calls msx_engine/msx_mod directly; all hooks are weak so the unit
 * test can link without the MSX engine. */

#define PLAT_MSX_SHIM_CAP  "msx.exec:v1"

typedef enum {
    PLAT_MSX_SHIM_OK     = 0,
    PLAT_MSX_SHIM_DENIED = 1,  /* lease check failed */
    PLAT_MSX_SHIM_ERROR  = 2,  /* execution error    */
    PLAT_MSX_SHIM_NOLINK = 3,  /* hooks not linked   */
} plat_msx_shim_status_t;

typedef struct plat_msx_shim_result {
    plat_msx_shim_status_t status;
    int                    exit_code;
    char                   output[512];
    char                   reason[128];
} plat_msx_shim_result_t;

/* Route an MSX/DSL script through the lease gate.
 * Returns 0 on success, -1 otherwise (result populated). */
int plat_msx_shim_exec(uint64_t context_id, uint32_t context_gen,
                       uint64_t lease_id, const char *script,
                       plat_msx_shim_result_t *out);
include/platx/msx_net_metrics.h
/* platx/msx_net_metrics.h — MSX read network/flow metrics (INT-108). */
#define PLAT_MSX_METRICS_REASON 128

typedef struct {
    uint64_t bytes_in;
    uint64_t bytes_out;
    uint64_t pkts_in;
    uint64_t pkts_out;
    uint64_t active_flows;
    uint64_t dropped_pkts;
    uint64_t sampled_at_ms;
} plat_msx_net_metrics_t;

/* Populate metrics snapshot from flow subsystem.  0/-1. */
int plat_msx_net_metrics_read(plat_msx_net_metrics_t *out);

/* Push metrics into MSX investigation queue as a structured event. 0/-1. */
int plat_msx_net_metrics_push(const plat_msx_net_metrics_t *m);
32

ndr

Анализ сетевого наблюдения, признаков и контекста потоков
src/ndr/Наблюдение и исследование20 файлов19 API headers

NDR обрабатывает доступную сетевую телеметрию и связывает признаки с источником, временем и контекстом. Контейнер, который не удалось прочитать, не считается чистым. Находки, качество захвата и потери отображаются раздельно, а решение о действии остаётся у политики.

Граница ответственности

  • NDR обрабатывает доступную сетевую телеметрию и связывает признаки с источником, временем и контекстом. Контейнер, который не удалось прочитать, не считается чистым. Находки, качество захвата и потери отображаются раздельно, а решение о действии остаётся у политики.

Устройство подсистемы

  • адаптер CLI домена NDR к консоли платформы. Логики нет: она в ndr_cli.c и печатает в FILE*. Вывод собирается в поток в памяти и отдаётся консоли одним куском — иначе пришлось бы держать второй набор форматирования, и две реализации одной команды однажды разошлись бы
  • Домен офлайновый и без I/O: конвейер работает над буфером, время берёт из записи захвата, память — от вызывающего. Поэтому весь ввод-вывод собран здесь, а `t_ndr_no_effects` продолжает держать ядро чистым. ЧТО ЭТОТ CLI ПОКАЗЫВАЕТ. Находка сетевого детектора без контекста
  • представления CLI домена NDR: потоки, протоколы, база DPI, снимок IOC из файла, retrospective hunt, покрытие и JSON, история. Host-ТУ: здесь есть I/O (fopen, qsort, printf), и в ядро этот файл не линкуется (граница NDR_HOST_TUS). Всё, что печатается, берётся из
  • контейнер захвата и разбор L2–L4 (контракт ndr.flow:v1). Инварианты, которые здесь держатся руками, потому что компилятор их не 1. Каждое чтение из кадра предваряется проверкой остатка. Ни одна арифметика над смещением не выполняется до проверки, что смещение

Управление и диагностика

Корневые команды: ndr. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / ndr →
Состав подсистемы / 20 файлов
Файл / компонентНазначение и граница
src/ndr/cmd_ndr.cадаптер CLI домена NDR к консоли платформы. Логики нет: она в ndr_cli.c и печатает в FILE*. Вывод собирается в поток в памяти и отдаётся консоли одним куском — иначе пришлось бы держать второй набор форматирования, и две реализации одной команды однажды разошлись бы
src/ndr/ndr_cli.cДомен офлайновый и без I/O: конвейер работает над буфером, время берёт из записи захвата, память — от вызывающего. Поэтому весь ввод-вывод собран здесь, а `t_ndr_no_effects` продолжает держать ядро чистым. ЧТО ЭТОТ CLI ПОКАЗЫВАЕТ. Находка сетевого детектора без контекста
src/ndr/ndr_cli_view.cпредставления CLI домена NDR: потоки, протоколы, база DPI, снимок IOC из файла, retrospective hunt, покрытие и JSON, история. Host-ТУ: здесь есть I/O (fopen, qsort, printf), и в ядро этот файл не линкуется (граница NDR_HOST_TUS). Всё, что печатается, берётся из
src/ndr/ndr_decode.cконтейнер захвата и разбор L2–L4 (контракт ndr.flow:v1). Инварианты, которые здесь держатся руками, потому что компилятор их не 1. Каждое чтение из кадра предваряется проверкой остатка. Ни одна арифметика над смещением не выполняется до проверки, что смещение
src/ndr/ndr_detect.cдетекторы ndr.detect:v1. Правило, которому подчинён весь файл: строка claim описывает ровно то, что проверено кодом выше неё, и ни слова больше. Если признак наблюдаем сетью — так и написано; если для подтверждения нужен хост, поднят requires_host_evidence, и claim говорит «гипотеза», а не «атака».
src/ndr/ndr_dpi.cинтерпретатор правил и база сигнатур протоколов. Правило чтения базы: каждая строка — утверждение о ФОРМАТЕ протокола по его спецификации (RFC / документация вендора), а не «так выглядело в одном захвате». Порядок — от частного к общему. Образец на каждую
src/ndr/ndr_flow.cтаблица потоков ndr.flow:v1. Три решения, из-за которых файл выглядит именно так. 1. Память только снаружи. Внутри нет ни одного malloc: таблица живёт в буфере вызывающего, её размер задан на входе и не растёт. «Bounded flow-table» из ТЗ — это не пожелание, а отсутствие кода, который мог
src/ndr/ndr_hpack.cдекодер HPACK (RFC 7541). Ноль I/O, ноль malloc. Таблицы ниже сгенерированы tests/ndr/gen_hpack_tables.py из пакета `hpack`; правки — только через генератор.
src/ndr/ndr_intel.cпроекция IOC на неизменяемый снимок (intel.match:v1). Единственное место, где здесь возможен тихий отказ, — бинарный поиск по массиву, который на самом деле не отсортирован: он вернул бы «не найдено» ровно так же уверенно, как настоящее отсутствие. Поэтому снимок обязан
src/ndr/ndr_ipfrag.cсборка IP-фрагментов с бюджетом. Ключ датаграммы — (домен наблюдения, сенсор, версия IP, адреса, протокол, идентификатор). Без домена и сенсора два разных сенсора, видящие один и тот же id, склеивали бы чужие фрагменты — та же ошибка, что и слияние потоков по 5-tuple.
src/ndr/ndr_lite.cлёгкие разборщики протоколов, опознанных DPI. См. заголовок.
src/ndr/ndr_md5.cMD5 по RFC 1321, без таблиц из внешних библиотек. Проверяется тестовыми векторами RFC (t_ndr_ja3). Единственное назначение в домене — отпечаток JA3; см. ndr_md5.h о том, чем это НЕ является.
src/ndr/ndr_module.cдескриптор модуля NDR по MODULE_CONTRACT. Что здесь намеренно НЕТ и почему: - malloc/calloc: экземпляр один (PLAT_MOD_FLAG_SINGLETON) и статический; конвейер весит сотни килобайт и живёт столько же, сколько процесс. Гейт «ноль динамической памяти» домена распространяется и на модуль.
src/ndr/ndr_pipeline.cпуть «захват → поток → пересборка → разбор → находка». Про детерминизм. Отпечаток считается не по памяти структуры (там есть указатели и порядок слотов, зависящий от хеша), а по явно перечисленным полям в порядке flow_id. Иначе тест на воспроизводимость проверял бы
src/ndr/ndr_proto.cразбор L7 (контракт ndr.protocol:v1). Правила, общие для всех разборщиков этого файла: - вход недоверенный целиком, включая длины внутри самого протокола; ни одна длина из входа не используется как размер копирования без сверки с остатком буфера И с ёмкостью приёмника;
src/ndr/ndr_reasm.cокно пересборки TCP с политикой FIRST-WINS. Почему битовая карта, а не список диапазонов: с картой вопрос «этот байт уже приходил и был ли он другим» решается точно и за O(len), без слияния интервалов и без случая «списка не хватило». Цена — cap/8 байт на
src/ndr/ndr_render.cJSON для Console: находка с объяснением и покрытие. Строки из недоверенного трафика (features, claim содержат имена и SNI) экранируются: кавычка, обратная косая, управляющие. Всё непечатаемое уходит как \\u00XX. Усечение буфера — отказ целиком.
src/ndr/ndr_response.cиз находки в намерение, и ни шагом дальше. Дескриптор строится по контракту action_provider_v1, но с тремя режим LIVE. Это не забывчивость, а граница: координатор отказывает на каждой из них по отдельности и с названной причиной, так что «NDR сам заблокировал» — недостижимое состояние, а не неслучившееся.
src/ndr/ndr_store.cкольцо сетевой истории и retrospective hunt. Две вещи, которые здесь нельзя перепутать: 1. Время события и время знания. Попадание несёт оба и признак as_known_then: BEFORE — индикатор уже был известен, когда событие происходило (и, значит, его тогда пропустили); AFTER — знание
src/ndr/ndrctl_main.cавтономный CLI домена NDR. Домен в поставляемый профиль не входит, поэтому глагол `ndr` внутри platx появится по решению владельца. До тех пор CLI обязан быть запускаемым: «CLI есть» без исполнения — утверждение без проверки.
Контракты API / 19 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/ndr_decode.h
/* platx/ndr_decode.h — offline-декодер захвата: pcap-контейнер и L2–L4.
 *
 * ТЗ: раздел 7, NDR-01 (воспроизводимый replay) и NDR-02 (IPv4/IPv6,
 * retransmit, reorder, fragmentation, overlap, VLAN/туннели).
 *
 * Декодер работает НАД БУФЕРОМ. Он не открывает файлов и не знает, откуда
 * байты взялись: чтение файла — дело вызывающего (в дереве это тестовый
 * harness). Так граница M3/M4 «I/O только через ABI» не нарушается тем,
 * что разбор пакета потянул за собой fopen.
 *
 * Все входные данные недоверенные (C27). Ни одна функция здесь не пишет за
 * пределы выходной структуры и не разыменовывает вход дальше объявленной
 * длины; выход при отказе не заполнен, а не «заполнен частично».
 */

/* Link types, которые декодер обязуется разбирать. Остальные — отказ. */
#define NDR_DLT_EN10MB   1u
#define NDR_DLT_RAW      101u
#define NDR_DLT_LINUX_SLL 113u

typedef struct {
    const uint8_t *buf;      /* весь файл                                  */
    size_t         len;
    size_t         off;      /* курсор чтения                              */
    uint32_t       dlt;
    uint32_t       snaplen;
    uint8_t        swapped;  /* порядок байт файла отличается от хоста     */
    uint8_t        nanos;    /* магия 0xa1b23c4d — ts_frac в наносекундах  */
    uint32_t       recs_read;
} ndr_pcap_t;

typedef struct {
    uint64_t       ts_us;
    uint32_t       cap_len;
    uint32_t       orig_len;
    const uint8_t *data;
} ndr_pcap_rec_t;

/* Открыть буфер как classic pcap. 0 — успех, -1 — не pcap/короткий файл,
 * -2 — неподдержанный DLT. */
int ndr_pcap_open(ndr_pcap_t *p, const uint8_t *buf, size_t len);
/* Следующая запись. 0 — есть запись, 1 — конец, -1 — усечённый файл. */
int ndr_pcap_next(ndr_pcap_t *p, ndr_pcap_rec_t *rec);

/* Разбор одного кадра в нормализованный пакет.
 * dlt — из ndr_pcap_t. ts_us/cap_len/orig_len проставляет вызывающий через
 * запись pcap; функция заполняет остальное.
 * 0 — успех (в т.ч. с поднятыми битами out->decode_reason),
 * -1 — аргументы, -2 — кадр не разобран (не IP, обрезан до неразбираемости). */
int ndr_decode_frame(uint32_t dlt, const uint8_t *frame, uint32_t cap_len,
                     uint32_t orig_len, uint64_t ts_us, ndr_pkt_t *out);

/* Разбор L4 над уже собранной полезной нагрузкой IP (после сборки
 * фрагментов). out обязан нести адреса/версию/протокол; функция заполняет
 * порты, флаги, l7. 0 — успех, -1 — аргументы. */
int ndr_decode_l4(const uint8_t *payload, uint32_t len, ndr_pkt_t *out);
include/platx/ndr_detect.h
/* platx/ndr_detect.h — контракт ndr.detect:v1: находки, а не приговоры.
 *
 * ТЗ, раздел 2 «NDR Detect»: «Он публикует находки, не меняет firewall»,
 * и раздел 5: «получение свежей сигнатуры не даёт права изменять firewall».
 * В этом заголовке поэтому НЕТ ни одной функции, способной что-либо
 * применить: ни deny, ни block, ни undo. Отсутствие такой функции — и есть
 * исполнение требования; комментарий «мы не блокируем» им не является.
 *
 * Второе требование, которое видно прямо в структуре находки:
 * requires_host_evidence. ТЗ (раздел 3, NDR-07) запрещает превращать
 * всплеск SSH/RDP-сессий в «подтверждённые неудачные входы»: сеть этого
 * не видит. Поле стоит рядом с confidence, чтобы утверждение нельзя было
 * повысить, не сняв признак — то есть не солгав явно.
 *
 * Третье: у каждой находки есть поколения — движка, снимка IOC и baseline.
 * Находка без них не воспроизводима: через неделю никто не скажет, на чём
 * она была построена.
 */

#define NDR_DETECT_CONTRACT   "ndr.detect:v1"
#define NDR_ENGINE_VERSION    1u

/* Пределы состояния. Всё, что не поместилось, отражается счётчиком
 * *_truncated, а не молча теряется: «мы посчитали 64 из неизвестно скольких»
 * — это другая находка, чем «их было ровно 64». */
#define NDR_DET_MAX_HOSTS      64u
#define NDR_DET_MAX_PEERS      64u   /* различных назначений на хост       */
#define NDR_DET_MAX_PAIRS     128u
#define NDR_DET_MAX_INTERVALS  16u
#define NDR_DET_MAX_FINDINGS   64u
#define NDR_DET_ALLOW_MAX      16u
#define NDR_DET_PAIR_COLLAPSE   8u   /* парных находок одного вида от узла */

typedef enum {
    NDR_F_NONE = 0,
    NDR_F_IOC_IP,
    NDR_F_IOC_DOMAIN,
    NDR_F_FANOUT,
    NDR_F_ADMIN_OUT_OF_WINDOW,
    NDR_F_BEACON_HYPOTHESIS,
    NDR_F_DNS_TUNNEL_HYPOTHESIS,
    NDR_F_AUTH_BURST_HYPOTHESIS,
    NDR_F_TCP_OVERLAP_CONFLICT,
    NDR_F_IOC_JA3,
    NDR_F_IOC_RESOLVED_IP,       /* адрес из ответа DNS совпал с IOC      */
    NDR_F_SMB_ADMIN_SHARE,       /* TREE_CONNECT к ADMIN$/C$/IPC$          */
    NDR_F_SMB_LOGON_FAILURES,    /* наблюдённые STATUS_LOGON_FAILURE       */
    NDR_F_SMB_FAIL_THEN_SUCCESS, /* серия отказов, затем успешная сессия   */
    NDR_F_CLEARTEXT_PROTOCOL,    /* протокол несёт учётные данные открыто  */
    NDR_F_TUNNEL_PROTOCOL,       /* туннель/VPN/прокси: содержимое скрыто  */
    NDR_F_CLEARTEXT_PASSWORD,    /* НАБЛЮДЁН секрет открытым текстом       */
    NDR_F_AUTH_FAILURES,         /* наблюдённые отказы входа (FTP/SMTP/LDAP/Kerberos/MySQL) */
    NDR_F_AUTH_FAIL_THEN_SUCCESS,/* серия отказов, затем успех             */
    NDR_F_KRB_UNKNOWN_PRINCIPALS,/* KDC: C_PRINCIPAL_UNKNOWN ≥ порога      */
    NDR_F_WEAK_AUTH,             /* наблюдён выбор без/со слабой аутентификацией (VNC None, RDP standard security) */
    NDR_F_LEGACY_PROTOCOL,       /* наблюдён устаревший протокол (SMB1, SSH-1) */
    NDR_F_DNS_NXDOMAIN_BURST,    /* NXDOMAIN ≥ порога от одного узла за окно (DGA — одно из объяснений) */
    NDR_F_DHCP_MULTIPLE_SERVERS, /* OFFER/ACK от второго сервера в домене наблюдения */
    NDR_F_SMB_EXE_WRITE,         /* CREATE с записью исполняемого файла на шаре (psexec-подобно) */
    NDR_F_EXECUTABLE_OBJECT      /* извлечённый объект — исполняемый по magic (MZ/ELF) */
} ndr_finding_kind_t;

/* Область находки. Fan-out относится к УЗЛУ, beaconing — к ПАРЕ узлов, а
 * совпадение IOC — к потоку. Складывать их в одно поле «ключ» и делать вид,
 * что это всегда поток, значит либо плодить дубликаты на каждый пакет, либо
 * склеивать разные факты. Поле scope говорит, что именно в key. */
typedef enum {
    NDR_SCOPE_FLOW = 0,
    NDR_SCOPE_HOST = 1,   /* key.addr_a — узел, остальное нули            */
    NDR_SCOPE_PAIR = 2    /* key.addr_a/addr_b/port_b — пара и порт        */
} ndr_scope_t;

typedef enum {
    NDR_SEV_INFO = 0,
    NDR_SEV_LOW  = 1,
    NDR_SEV_MED  = 2,
    NDR_SEV_HIGH = 3
} ndr_severity_t;

#define NDR_FEATURES_MAX 224u
#define NDR_CLAIM_MAX    160u

typedef struct {
    uint64_t finding_id;
    uint8_t  kind;
    uint8_t  severity;
    uint8_t  confidence;          /* 0..100                                */
    uint8_t  requires_host_evidence;
    uint8_t  obs;                 /* наблюдаемость источника               */
    uint8_t  suppressed_by_allow; /* сработало бы, но источник в allowlist  */
    uint8_t  scope;               /* ndr_scope_t: что лежит в key          */
    /* Агрегат: находки одного вида от одного узла ко многим целям свёрнуты
     * в одну HOST-находку, когда их стало больше NDR_DET_PAIR_COLLAPSE.
     * observations = число целей. Иначе один сканирующий узел заполнял бы
     * кольцо парными находками и вытеснял бы всё остальное. */
    uint8_t  collapsed;

    ndr_flow_key_t key;
    uint64_t flow_id;
    uint64_t first_ts_us;
    uint64_t last_ts_us;

    /* Сколько раз условие находки наблюдалось повторно. Это НЕ «сколько
     * было атак»: одно и то же условие, увиденное дважды, — два
     * наблюдения одного факта, и слово выбрано именно поэтому. */
    uint32_t observations;
    uint32_t coverage_reason;     /* NDR_R_* источника находки             */
    uint32_t engine_version;
    uint32_t baseline_generation;
    uint64_t snapshot_generation; /* 0 — снимок IOC не участвовал          */
    uint64_t bundle_id;
    uint64_t ioc_id;

    char features[NDR_FEATURES_MAX]; /* наблюдённое, стабильный формат     */
    char claim[NDR_CLAIM_MAX];       /* что именно утверждается            */
} ndr_finding_t;

typedef struct {
    uint32_t fanout_distinct_threshold;
    uint64_t fanout_window_us;

    uint16_t admin_ports[8];
    uint8_t  n_admin_ports;
    uint8_t  admin_hour_start;    /* UTC-час начала разрешённого окна      */
    uint8_t  admin_hour_end;      /* конец, невключительно; start<end      */

    uint32_t beacon_min_samples;
    uint32_t beacon_max_jitter_pct;
    uint64_t beacon_max_bytes;    /* «малый» обмен: верхняя граница        */

    uint16_t dns_label_max;       /* метка длиннее — признак туннеля       */
    uint16_t dns_qname_len;

    uint32_t auth_burst_min_conns;
    uint64_t auth_burst_window_us;
    uint32_t smb_logon_fail_min;  /* отказов входа, чтобы поднять находку   */
    uint32_t nxdomain_min;        /* NXDOMAIN за окно от узла → находка      */
    uint64_t nxdomain_window_us;

    uint8_t  allow_src[NDR_DET_ALLOW_MAX][16];
    uint8_t  allow_src_ipver[NDR_DET_ALLOW_MAX];
    uint32_t n_allow_src;         /* админы, сканеры, backup               */

    uint32_t baseline_generation;
} ndr_detect_cfg_t;

/* Состояние. Целиком по значению: размер известен на этапе компиляции и
 * печатается тестом — «bounded» проверяется числом, а не обещанием. */
typedef struct {
    uint8_t  addr[16];
    uint8_t  ip_ver;
    uint8_t  in_use;
    uint16_t n_peers;
    uint64_t win_start_us;
    uint64_t last_us;
    uint8_t  peers[NDR_DET_MAX_PEERS][16];
    uint16_t peer_ports[NDR_DET_MAX_PEERS];
    uint8_t  peers_truncated;
    uint32_t conns;
    uint32_t nxdomain;           /* NXDOMAIN-ответов этому узлу за окно   */
    uint64_t nx_win_start_us;
    char     nx_last[64];
} ndr_det_host_t;

typedef struct {
    uint8_t  src[16], dst[16];
    uint16_t dport;
    uint8_t  ip_ver;
    uint8_t  in_use;
    uint64_t last_us;
    uint64_t bytes_total;
    uint32_t conns;
    uint32_t n_iv;
    uint32_t iv_us[NDR_DET_MAX_INTERVALS];
    uint32_t admin_conns;
    uint64_t admin_win_start;
    uint32_t smb_logon_fail;     /* SMB2 SESSION_SETUP c LOGON_FAILURE     */
    uint32_t smb_logon_ok;
    uint32_t smb_setup_req;
    uint32_t auth_fail;          /* отказы входа по лёгким разборщикам     */
    uint32_t auth_ok;
    uint32_t auth_attempts;
    uint32_t krb_unknown;        /* KRB-ERROR C_PRINCIPAL_UNKNOWN          */
    char     last_user[48];      /* последнее имя в попытке входа          */
} ndr_det_pair_t;

#define NDR_DET_MAX_DHCP_SERVERS 4u
typedef struct {
    ndr_detect_cfg_t cfg;
    uint8_t  dhcp_servers[NDR_DET_MAX_DHCP_SERVERS][16]; /* видевшиеся серверы DHCP */
    uint8_t  n_dhcp_servers;
    ndr_det_host_t   hosts[NDR_DET_MAX_HOSTS];
    ndr_det_pair_t   pairs[NDR_DET_MAX_PAIRS];
    ndr_finding_t    findings[NDR_DET_MAX_FINDINGS];
    uint32_t         n_findings;
    uint32_t         findings_dropped;   /* переполнение кольца находок    */
    uint32_t         hosts_dropped;
    uint32_t         pairs_dropped;
    uint32_t         suppressed;         /* подавлено allowlist'ом         */
    uint64_t         next_finding_id;
} ndr_detect_t;

/* cfg == NULL — набор по умолчанию (значения перечислены в .c и в ТЗ). */
int  ndr_detect_init(ndr_detect_t *d, const ndr_detect_cfg_t *cfg);
void ndr_detect_defaults(ndr_detect_cfg_t *cfg);

/* Учёт одного наблюдённого пакета в контексте его потока. */
int  ndr_detect_on_pkt(ndr_detect_t *d, const ndr_pkt_t *p, const ndr_flow_t *f);

/* Транзакции L7 и совпадения IOC. snap может быть NULL (снимка нет —
 * тогда IOC-находки не появляются, а не «появляются с generation 0»). */
int  ndr_detect_on_dns(ndr_detect_t *d, const ndr_flow_t *f,
                       const ndr_dns_txn_t *q, const ndr_intel_snapshot_t *snap);
/* DHCP OFFER/ACK от сервера с адресом server_ip (option 54 или src). */
int  ndr_detect_on_dhcp_server(ndr_detect_t *d, const ndr_flow_t *f, const uint8_t *server_ip, uint8_t ip_ver);
/* Извлечённый объект: исполняемый по magic-байтам. */
int  ndr_detect_on_object(ndr_detect_t *d, const ndr_flow_t *f, const uint8_t *data, uint32_t len,
                          const char *host, const char *uri);
/* RDP: cookie-пользователь и выбранный уровень безопасности. */
int  ndr_detect_on_rdp(ndr_detect_t *d, const ndr_flow_t *f, const ndr_rdp_txn_t *t);
/* SSH: баннер (версия протокола 1.x — устаревший). */
int  ndr_detect_on_ssh_banner(ndr_detect_t *d, const ndr_flow_t *f, const char *banner);
/* Лёгкие разборщики: имена → IOC (доменная часть адреса), наблюдённые
 * секреты и отказы входа. Всё — наблюдение, не гипотеза. */
int  ndr_detect_on_lite(ndr_detect_t *d, const ndr_flow_t *f,
                        const ndr_lite_txn_t *t, const ndr_intel_snapshot_t *snap);
/* Классификация DPI: находки по СВОЙСТВУ протокола (открытые учётные
 * данные, туннель), не по содержимому — содержимого у DPI нет. */
int  ndr_detect_on_dpi(ndr_detect_t *d, const ndr_flow_t *f,
                       const ndr_dpi_result_t *r);
/* HTTP/2: :authority из декодированного HPACK (поле в находке — h2.authority). */
int  ndr_detect_on_h2(ndr_detect_t *d, const ndr_flow_t *f,
                      const char *authority, const ndr_intel_snapshot_t *snap);
int  ndr_detect_on_http(ndr_detect_t *d, const ndr_flow_t *f,
                        const ndr_http_txn_t *h, const ndr_intel_snapshot_t *snap);
int  ndr_detect_on_tls(ndr_detect_t *d, const ndr_flow_t *f,
                       const ndr_tls_hello_t *t, const ndr_intel_snapshot_t *snap);
/* Совпадение по адресу назначения потока. */
int  ndr_detect_on_flow_ip(ndr_detect_t *d, const ndr_flow_t *f,
                           const ndr_intel_snapshot_t *snap);
/* SMB-транзакция. dir_client=1 — сообщение от инициатора потока. */
int  ndr_detect_on_smb(ndr_detect_t *d, const ndr_flow_t *f,
                       const ndr_smb_txn_t *t);
/* Наблюдение конфликта перекрывающихся сегментов (см. ndr_reasm.h). */
int  ndr_detect_on_overlap_conflict(ndr_detect_t *d, const ndr_flow_t *f,
                                    uint32_t conflicts);

/* Выпуск гипотез, которые видны только по накопленному состоянию. */
uint32_t ndr_detect_finalize(ndr_detect_t *d, uint64_t now_us);

const ndr_finding_t *ndr_detect_findings(const ndr_detect_t *d, uint32_t *n);
const char *ndr_finding_kind_name(uint8_t kind);
include/platx/ndr_dpi.h
/* platx/ndr_dpi.h — классификация протоколов по содержимому (DPI) на базе
 * ДАННЫХ: таблица сигнатур + маленький интерпретатор правил. Ноль I/O, ноль
 * malloc, ноль состояния между вызовами.
 *
 * Место в контуре: разборщики домена (`ndr_proto`) остаются главными —
 * они выдают поля и транзакции. DPI отвечает на другой вопрос: «что это за
 * протокол», когда разборщика нет. Ответ DPI — КЛАССИФИКАЦИЯ, не разбор:
 * ни одного поля из нагрузки он не выдаёт, и это видно в покрытии.
 *
 * Правила базы:
 *  - совпадение = все правила сигнатуры сработали (И); порядок таблицы —
 *    от частного к общему, первое совпадение побеждает, число совпавших
 *    сигнатур с РАЗНЫМИ протоколами считается (ambiguous);
 *  - порт только повышает уверенность и никогда не отменяет содержимое;
 *    сигнатуры, у которых без порта содержимое неотличимо (NTP, RADIUS,
 *    Modbus…), помечены NDR_DPI_F_PORT_ASSISTED и БЕЗ порта не срабатывают;
 *  - у каждой сигнатуры есть категория и флаги свойств (открытые учётные
 *    данные, туннель, ICS/OT) — их читает детектор, а не сама DPI;
 *  - у каждой сигнатуры в тесте есть образец, и он должен совпадать ТОЛЬКО с
 *    ней (матрица взаимной исключительности). Образцы синтетические: база
 *    проверена на непротиворечивость и по спецификациям, НЕ на реальном
 *    трафике — так и написано в реестре.
 */

/* Категории. */
enum {
    NDR_DPI_CAT_WEB = 1, NDR_DPI_CAT_MAIL, NDR_DPI_CAT_REMOTE, NDR_DPI_CAT_FILE,
    NDR_DPI_CAT_DB, NDR_DPI_CAT_DIRECTORY, NDR_DPI_CAT_NETINFRA, NDR_DPI_CAT_VOIP,
    NDR_DPI_CAT_TUNNEL, NDR_DPI_CAT_ICS, NDR_DPI_CAT_IOT, NDR_DPI_CAT_MESSAGING,
    NDR_DPI_CAT_P2P, NDR_DPI_CAT_GAMING, NDR_DPI_CAT_CRYPTO, NDR_DPI_CAT_MGMT,
    NDR_DPI_CAT_STORAGE, NDR_DPI_CAT_STREAMING, NDR_DPI_CAT_DEV, NDR_DPI_CAT_PROXY,
    NDR_DPI_CAT_MAX
};

/* Флаги сигнатуры. */
#define NDR_DPI_F_TCP            0x01u
#define NDR_DPI_F_UDP            0x02u
#define NDR_DPI_F_CLEARTEXT_AUTH 0x04u  /* учётные данные открытым текстом по устройству протокола */
#define NDR_DPI_F_TUNNEL         0x08u  /* переносит другой трафик             */
#define NDR_DPI_F_ICS            0x10u  /* промышленный протокол               */
#define NDR_DPI_F_PORT_ASSISTED  0x20u  /* без совпадения порта не срабатывает */
#define NDR_DPI_F_HEURISTIC      0x40u  /* содержимое слабое; уверенность низкая */
#define NDR_DPI_F_ENCRYPTED      0x80u  /* нагрузка шифрована по устройству    */
#define NDR_DPI_F_GENERIC        0x100u /* общая сигнатура семейства: более частная
                                         * (websocket → http) не считается неоднозначностью */

/* Операции правил. Смещения — от начала нагрузки L4. */
enum {
    NDR_DPI_OP_EQ = 1,     /* байты pat[len] на off                          */
    NDR_DPI_OP_MASK,       /* (d[off+i] & pat[len+i]) == pat[i]              */
    NDR_DPI_OP_ANY,        /* на off — одна из альтернатив: (len,bytes)*, 0   */
    NDR_DPI_OP_RANGE,      /* d[off] в [arg>>8, arg&0xff]                    */
    NDR_DPI_OP_FIND,       /* pat[len] встречается в первых arg байтах        */
    NDR_DPI_OP_TEXT,       /* d[off..off+arg) — печатаемый ASCII/CR/LF/TAB     */
    NDR_DPI_OP_LENMIN,     /* длина нагрузки ≥ arg                            */
    NDR_DPI_OP_LENEQ,      /* длина нагрузки == arg                           */
    NDR_DPI_OP_LEN8,       /* u8(off) + arg == длина                          */
    NDR_DPI_OP_LEN16BE,    /* u16be(off) + arg == длина                       */
    NDR_DPI_OP_LEN16LE,
    NDR_DPI_OP_LEN24BE,
    NDR_DPI_OP_LEN24LE,
    NDR_DPI_OP_LEN32BE,
    NDR_DPI_OP_LEN32LE,
    NDR_DPI_OP_PORT,       /* один из портов потока == arg                    */
    NDR_DPI_OP_U16BE_MAX   /* u16be(off) <= arg                               */
};

typedef struct {
    uint8_t  op;
    uint16_t off;
    uint8_t  len;
    uint16_t arg;
    const uint8_t *pat;
} ndr_dpi_rule_t;

#define NDR_DPI_MAX_RULES 5u

typedef struct {
    uint16_t id;                 /* стабильный идентификатор протокола       */
    const char *name;            /* короткое имя (для Console/CLI)           */
    uint8_t  category;
    uint16_t flags;              /* NDR_DPI_F_*                             */
    uint8_t  confidence;         /* 0..100 при совпадении без порта          */
    uint8_t  n_rules;
    uint16_t ports[3];           /* подсказки; 0 — нет                       */
    ndr_dpi_rule_t rules[NDR_DPI_MAX_RULES + 1u];   /* + терминатор op=0     */
} ndr_dpi_sig_t;

typedef struct {
    uint16_t id;                 /* 0 — не опознано                          */
    uint8_t  confidence;
    uint8_t  category;
    uint16_t flags;
    uint8_t  ambiguous;          /* совпало ещё столько сигнатур ДРУГИХ протоколов */
    uint16_t sig_index;          /* индекс сигнатуры в таблице              */
    uint32_t evaluated;          /* сигнатур проверено                       */
} ndr_dpi_result_t;

/* Классифицировать нагрузку. is_tcp: 1 — TCP, 0 — UDP. port_a/port_b — порты
 * потока (0 — неизвестны). Возвращает 0; результат в out (id 0 — не опознано). */
int ndr_dpi_classify(const uint8_t *d, uint32_t len, uint8_t is_tcp,
                     uint16_t port_a, uint16_t port_b, ndr_dpi_result_t *out);

/* База. */
const ndr_dpi_sig_t *ndr_dpi_table(uint32_t *n);
const char *ndr_dpi_name(uint16_t id);          /* "" — неизвестный id      */
const char *ndr_dpi_category_name(uint8_t cat);
uint32_t    ndr_dpi_protocol_count(void);       /* различных id в базе      */
/* Самопроверка базы: каждое правило структурно допустимо (смещение и
 * длина в пределах, ANY завершён нулём, MASK несёт маску, TEXT/FIND с arg).
 * Возвращает 0 или индекс первой битой сигнатуры + 1. */
int         ndr_dpi_table_check(void);

/* Диапазон идентификаторов: ядро кладёт DPI-классификацию в l7_proto как
 * NDR_DPI_L7_BASE + id, чтобы не смешивать с разбираемыми протоколами. */
#define NDR_DPI_L7_BASE 0x1000u
include/platx/ndr_dpi_ids.h
/* platx/ndr_dpi_ids.h — стабильные идентификаторы протоколов базы DPI.
 * Номера не переиспользуются: удалённый протокол оставляет дыру. */
enum {
    NDR_DPI_UNKNOWN = 0,
    /* web / text */
    NDR_DPI_HTTP = 1, NDR_DPI_WEBSOCKET, NDR_DPI_RTSP, NDR_DPI_SIP, NDR_DPI_SSDP,
    NDR_DPI_IPP, NDR_DPI_HTTP_PROXY,
    /* mail */
    NDR_DPI_SMTP = 20, NDR_DPI_POP3, NDR_DPI_IMAP, NDR_DPI_NNTP, NDR_DPI_LMTP,
    /* remote access / terminal */
    NDR_DPI_TELNET = 40, NDR_DPI_VNC, NDR_DPI_X11, NDR_DPI_TEAMVIEWER, NDR_DPI_SPICE,
    NDR_DPI_CITRIX_ICA, NDR_DPI_ADB, NDR_DPI_JDWP, NDR_DPI_JAVA_RMI, NDR_DPI_RLOGIN,
    NDR_DPI_H225, NDR_DPI_ISO_COTP,
    /* file / storage */
    NDR_DPI_FTP = 70, NDR_DPI_TFTP, NDR_DPI_SUNRPC, NDR_DPI_NFS, NDR_DPI_PORTMAP,
    NDR_DPI_MOUNT, NDR_DPI_NETBIOS_SSN, NDR_DPI_NETBIOS_NS, NDR_DPI_NETBIOS_DGM,
    NDR_DPI_AFP, NDR_DPI_ISCSI, NDR_DPI_NBD, NDR_DPI_GIT, NDR_DPI_SVN, NDR_DPI_CVS,
    NDR_DPI_LPD, NDR_DPI_RSYNC, NDR_DPI_CEPH, NDR_DPI_CLEARTEXT_LOGIN,
    /* databases */
    NDR_DPI_MYSQL = 100, NDR_DPI_POSTGRES, NDR_DPI_MONGODB, NDR_DPI_REDIS,
    NDR_DPI_MEMCACHED, NDR_DPI_MSSQL_TDS, NDR_DPI_ORACLE_TNS, NDR_DPI_CASSANDRA,
    NDR_DPI_DB2_DRDA, NDR_DPI_ELASTIC_TRANSPORT, NDR_DPI_KAFKA, NDR_DPI_ZOOKEEPER,
    NDR_DPI_THRIFT,
    /* directory / auth */
    NDR_DPI_LDAP = 130, NDR_DPI_KERBEROS, NDR_DPI_NTLMSSP, NDR_DPI_RADIUS,
    NDR_DPI_TACACS, NDR_DPI_DIAMETER, NDR_DPI_DCERPC,
    /* network infrastructure */
    NDR_DPI_SNMP = 150, NDR_DPI_NTP, NDR_DPI_DHCP, NDR_DPI_DHCPV6, NDR_DPI_SYSLOG,
    NDR_DPI_NETFLOW, NDR_DPI_IPFIX, NDR_DPI_SFLOW, NDR_DPI_SLP, NDR_DPI_WHOIS,
    NDR_DPI_FINGER, NDR_DPI_GOPHER, NDR_DPI_IDENT, NDR_DPI_XDMCP,
    /* voip / realtime */
    NDR_DPI_STUN = 180, NDR_DPI_TURN, NDR_DPI_DTLS, NDR_DPI_RTP, NDR_DPI_RTCP,
    NDR_DPI_SKINNY, NDR_DPI_IAX2, NDR_DPI_MGCP, NDR_DPI_RTMP, NDR_DPI_TEAMSPEAK,
    /* tunnels / vpn / proxy */
    NDR_DPI_OPENVPN = 200, NDR_DPI_WIREGUARD, NDR_DPI_IKE, NDR_DPI_L2TP, NDR_DPI_PPTP,
    NDR_DPI_VXLAN, NDR_DPI_GENEVE, NDR_DPI_GTP, NDR_DPI_SOCKS4, NDR_DPI_SOCKS5,
    /* ics / ot */
    NDR_DPI_S7COMM = 220, NDR_DPI_MODBUS, NDR_DPI_DNP3, NDR_DPI_ENIP, NDR_DPI_BACNET,
    NDR_DPI_IEC104, NDR_DPI_OPCUA,
    /* iot / messaging */
    NDR_DPI_MQTT = 240, NDR_DPI_AMQP, NDR_DPI_COAP, NDR_DPI_NATS, NDR_DPI_ZMTP,
    NDR_DPI_XMPP, NDR_DPI_IRC, NDR_DPI_SMPP, NDR_DPI_ZABBIX,
    /* p2p / crypto / finance */
    NDR_DPI_BITTORRENT = 260, NDR_DPI_BT_DHT, NDR_DPI_BITCOIN, NDR_DPI_STRATUM,
    NDR_DPI_MONERO_LEVIN, NDR_DPI_FIX,
    /* gaming */
    NDR_DPI_SOURCE_ENGINE = 280, NDR_DPI_IDTECH_OOB, NDR_DPI_MINECRAFT_JAVA, NDR_DPI_RAKNET,
    /* вторая волна базы (06.09.2026): ICS/OT */
    NDR_DPI_S7COMM_PLUS = 300, NDR_DPI_OMRON_FINS, NDR_DPI_MELSEC_MC, NDR_DPI_KNXNET_IP,
    NDR_DPI_HART_IP, NDR_DPI_ISO_MMS, NDR_DPI_DICOM, NDR_DPI_HL7_MLLP, NDR_DPI_NIAGARA_FOX,
    /* данные / очереди */
    NDR_DPI_NEO4J_BOLT = 320, NDR_DPI_CLICKHOUSE, NDR_DPI_ARANGODB_VST, NDR_DPI_HADOOP_RPC,
    NDR_DPI_HBASE_RPC, NDR_DPI_FIREBIRD, NDR_DPI_IBM_MQ, NDR_DPI_SAP_NI, NDR_DPI_STOMP,
    NDR_DPI_GLUSTER, NDR_DPI_NVME_TCP, NDR_DPI_DRBD, NDR_DPI_RIAK_PB,
    /* сеть / телеметрия */
    NDR_DPI_BGP = 340, NDR_DPI_RIP, NDR_DPI_LDP, NDR_DPI_BFD, NDR_DPI_HSRP, NDR_DPI_PTP,
    NDR_DPI_NAT_PMP, NDR_DPI_PCP, NDR_DPI_WS_DISCOVERY, NDR_DPI_COLLECTD, NDR_DPI_GELF,
    NDR_DPI_LUMBERJACK, NDR_DPI_SPLUNK_S2S, NDR_DPI_NRPE, NDR_DPI_MUNIN, NDR_DPI_STATSD,
    /* web-подтипы, почта, voip */
    NDR_DPI_WEBDAV = 360, NDR_DPI_OCSP, NDR_DPI_MANAGESIEVE, NDR_DPI_MEGACO,
    /* p2p / iot / crypto */
    NDR_DPI_GNUTELLA = 370, NDR_DPI_DCPP, NDR_DPI_ED2K, NDR_DPI_IPFS, NDR_DPI_SYNCTHING,
    NDR_DPI_DROPBOX_LANSYNC, NDR_DPI_RTPS_DDS, NDR_DPI_TUYA, NDR_DPI_XIAOMI_MIIO, NDR_DPI_ESPHOME,
    NDR_DPI_LITECOIN, NDR_DPI_DOGECOIN, NDR_DPI_ZCASH,
    NDR_DPI_ID_MAX = 400
};
include/platx/ndr_extract.h
/* platx/ndr_extract.h — извлечённый объект и приёмник для него.
 *
 * ТЗ, раздел 6: «NDR/Proxy/X могут отправить разрешённо извлечённый объект
 * в PXSANDBOX: immutable digest, flow/session refs, capture gaps, caller
 * scope и budgets сохраняются. Неполный или недоступный из-за шифрования
 * объект не считается готовым executable sample. Submission асинхронный и
 * bounded: очередь sandbox не блокирует packet path.»
 *
 * Здесь — только извлечение и границы. Digest, файл на диске и подача в
 * песочницу — дело АДАПТЕРА снаружи домена (у него есть I/O и sha256);
 * домен отдаёт байты, ссылки и честные флаги полноты через callback и
 * НИКОГДА не ждёт его ответа: отказ приёмника — счётчик, а не блокировка.
 */

#define NDR_OBJ_FIELD_MAX 128u

typedef enum {
    NDR_OBJ_SRC_HTTP_RESPONSE = 1,
    NDR_OBJ_SRC_H2_RESPONSE   = 2   /* тело из DATA-кадров h2c одного потока */
} ndr_object_source_t;

typedef struct {
    uint8_t  source;             /* ndr_object_source_t                    */
    uint8_t  complete;           /* все Content-Length байт получены (h2:
                                  * END_STREAM и ничего не обрезано)      */
    uint8_t  oversize;           /* объявленный размер больше бюджета: не извлекался */
    uint8_t  obs;                /* наблюдаемость потока-источника         */
    uint32_t coverage_reason;    /* NDR_R_* потока — capture gaps           */
    uint64_t flow_id;
    ndr_flow_key_t key;
    uint64_t first_ts_us, last_ts_us;
    int64_t  declared_len;       /* Content-Length                          */
    uint32_t len;                /* байт в data                             */
    const uint8_t *data;         /* буфер приёмника; валиден внутри callback */
    char     host[NDR_OBJ_FIELD_MAX];
    char     uri[NDR_OBJ_FIELD_MAX];
    uint16_t http_status;
} ndr_object_t;

/* Возврат 0 — принято; иное — приёмник перегружен/отказал. Конвейер
 * считает отказ и идёт дальше: packet path не ждёт песочницу. */
typedef int (*ndr_object_sink_fn)(const ndr_object_t *obj, void *ud);

typedef struct {
    ndr_object_sink_fn fn;
    void    *ud;
    uint8_t *buf;                /* память приёмника под ОДИН объект        */
    uint32_t cap;                /* бюджет извлечения                       */
} ndr_object_sink_t;
include/platx/ndr_flow.h
/* platx/ndr_flow.h — контракт ndr.flow:v1: нормализация наблюдения сети.
 *
 * ТЗ: docs/TZ_PLATX_NDR_THREATMOD_V1.md, раздел 2 «NDR Flow».
 *
 * Что этот контракт делает и чего он НЕ делает
 * --------------------------------------------
 * Делает: превращает недоверенные байты пакета в типизированный факт о
 * потоке с явно объявленным уровнем наблюдаемости и причинами его снижения.
 * Не делает: не открывает сокеты, не читает файлы, не выделяет память, не
 * заводит потоков и не принимает решений о реагировании. Вся память —
 * снаружи (ndr_flow_table_init), всё время — из пакета (ts_us), поэтому
 * один и тот же вход даёт один и тот же выход (NDR-01).
 *
 * Ключ потока НЕ равен 5-tuple: в него входят observation domain, sensor и
 * его boot epoch. Два сенсора, видящие «тот же» 5-tuple, не сливаются в один
 * поток — иначе разные соединения стали бы одним свидетельством (ТЗ, раздел 2).
 *
 * Пустая таблица не означает «атак нет». Любое место, где наблюдение
 * неполно, обязано поднять бит в reason_mask и понизить obs — см. NDR-03.
 */

#define NDR_FLOW_CONTRACT   "ndr.flow:v1"
#define NDR_FLOW_VERSION    1u

/* ── Наблюдаемость (ТЗ, раздел 3) ─────────────────────────────────────── */
typedef enum {
    NDR_OBS_FULL          = 0,  /* виден и заголовок, и полезная нагрузка   */
    NDR_OBS_METADATA_ONLY = 1,  /* только метаданные (шифрование/NetFlow)   */
    NDR_OBS_PARTIAL       = 2,  /* часть наблюдения потеряна                */
    NDR_OBS_UNOBSERVABLE  = 3   /* содержимое недоступно принципиально      */
} ndr_obs_t;

/* Причины снижения наблюдаемости. Битовая маска: причин бывает несколько. */
#define NDR_R_NONE            0x00000000u
#define NDR_R_ENCRYPTED       0x00000001u /* транспорт шифрован по природе  */
#define NDR_R_ASYMMETRIC      0x00000002u /* видно только одно направление  */
#define NDR_R_SNAPLEN         0x00000004u /* пакет обрезан захватом         */
#define NDR_R_PKT_LOSS        0x00000008u /* пропуск в последовательности   */
#define NDR_R_TABLE_FULL      0x00000010u /* поток вытеснен из таблицы      */
#define NDR_R_REASM_LIMIT     0x00000020u /* достигнут лимит пересборки     */
#define NDR_R_TUNNEL_DEPTH    0x00000040u /* превышена вложенность туннелей */
#define NDR_R_PARSER_UNSUP    0x00000080u /* нет разбора для этого случая   */
#define NDR_R_MIDSTREAM       0x00000100u /* поток подхвачен без рукопожатия*/
#define NDR_R_FRAGMENT        0x00000200u /* IP-фрагмент без L4-заголовка   */
#define NDR_R_MALFORMED       0x00000400u /* заголовок не разобран          */
#define NDR_R_DECODE_LIMIT    0x00000800u /* превышен лимит разбора L2/L3   */

/* ── Пределы. Это контракт, а не «пока так». ──────────────────────────── */
#define NDR_MAX_TUNNEL_DEPTH   3u      /* глубже — NDR_R_TUNNEL_DEPTH      */
#define NDR_MAX_IP_EXT_HDRS    8u      /* IPv6 extension header chain      */
#define NDR_MAX_PKT_LEN        65535u  /* верхняя граница одного пакета    */
#define NDR_FLOW_IDLE_US       (120ull * 1000000ull) /* idle-таймаут потока */

/* ── Направления ──────────────────────────────────────────────────────── */
#define NDR_DIR_AB 0u   /* адрес A → адрес B в каноническом порядке ключа  */
#define NDR_DIR_BA 1u

/* ── Состояние TCP, наблюдаемое пассивно ──────────────────────────────── */
typedef enum {
    NDR_TCP_NONE       = 0,  /* не TCP                                     */
    NDR_TCP_SYN        = 1,
    NDR_TCP_SYN_ACK    = 2,
    NDR_TCP_ESTABLISHED= 3,
    NDR_TCP_FIN        = 4,  /* хотя бы одна сторона закрыла               */
    NDR_TCP_CLOSED     = 5,
    NDR_TCP_RESET      = 6,
    NDR_TCP_MIDSTREAM  = 7   /* первый пакет не SYN — рукопожатие не видели*/
} ndr_tcp_state_t;

/* ── Нормализованный пакет ────────────────────────────────────────────── */
typedef struct {
    uint64_t ts_us;            /* время из захвата, не из часов процесса   */
    uint32_t observation_domain;
    uint32_t sensor_id;
    uint64_t sensor_boot_epoch;

    uint8_t  ip_ver;           /* 4 | 6                                    */
    uint8_t  l4_proto;         /* 6 TCP, 17 UDP, 1 ICMP, 58 ICMPv6         */
    uint16_t vlan_id;          /* 0xFFFF — VLAN не наблюдался              */
    uint8_t  tunnel_depth;     /* 0 — трафик не инкапсулирован             */
    uint8_t  is_fragment;      /* IP-фрагмент со смещением != 0            */
    uint8_t  more_frags;
    uint8_t  truncated;        /* caplen < origlen: часть пакета не видна  */
    uint8_t  reassembled;      /* датаграмма собрана из фрагментов         */
    uint32_t frag_off;         /* смещение фрагмента, байт                 */
    uint32_t ip_id;            /* идентификатор датаграммы (IPv4 16 бит,   */
                               /* IPv6 32 бита)                            */
    const uint8_t *ip_payload; /* полезная нагрузка IP (для сборки)        */
    uint32_t       ip_payload_len;

    uint8_t  src[16];
    uint8_t  dst[16];
    uint16_t sport;            /* 0, если L4-заголовок не наблюдался       */
    uint16_t dport;

    uint32_t seq;              /* TCP                                      */
    uint32_t ack;
    uint8_t  tcp_flags;        /* NDR_TCPF_*                               */
    uint16_t tcp_win;

    const uint8_t *l7;         /* указывает ВНУТРЬ входного буфера         */
    uint32_t       l7_len;
    uint32_t       orig_len;   /* длина пакета на проводе                  */
    uint32_t       cap_len;    /* сколько байт реально записано            */
    uint32_t       decode_reason; /* маска NDR_R_* от декодера             */
} ndr_pkt_t;

#define NDR_TCPF_FIN  0x01u
#define NDR_TCPF_SYN  0x02u
#define NDR_TCPF_RST  0x04u
#define NDR_TCPF_PSH  0x08u
#define NDR_TCPF_ACK  0x10u
#define NDR_TCPF_URG  0x20u

/* ── Ключ потока ──────────────────────────────────────────────────────── */
typedef struct {
    uint32_t observation_domain;
    uint32_t sensor_id;
    uint64_t sensor_boot_epoch;
    uint8_t  addr_a[16];
    uint8_t  addr_b[16];
    uint16_t port_a;
    uint16_t port_b;
    uint16_t vlan_id;
    uint8_t  ip_ver;
    uint8_t  l4_proto;
    uint8_t  tunnel_depth;
    uint8_t  pad[3];
} ndr_flow_key_t;

/* ── Запись потока ────────────────────────────────────────────────────── */
typedef struct {
    ndr_flow_key_t key;
    uint64_t first_ts_us;
    uint64_t last_ts_us;
    uint64_t pkts[2];
    uint64_t bytes[2];      /* байты на проводе (orig_len), не caplen      */
    uint64_t l7_bytes[2];

    /* На уровне потока дубликат и переупорядочивание НЕРАЗЛИЧИМЫ: для
     * различения нужна история принятых диапазонов, а она есть только у
     * пересборки (ndr_reasm.h). Поэтому здесь одно честное поле, а не два
     * красивых. Точные retrans/reorder/overlap берутся из ndr_reasm_stats_t. */
    uint32_t dup_low_seq[2];/* сегмент с seq ниже максимума увиденного      */
    uint32_t gaps[2];       /* seq прыгнул вперёд: дыра (потеря или обрез)  */

    uint32_t seq_max[2];    /* наибольший увиденный конец сегмента         */
    uint8_t  seq_valid[2];
    uint8_t  dir_seen[2];   /* видели ли хоть один пакет в направлении     */

    uint8_t  initiator_is_a;/* сторона, приславшая SYN/первый пакет        */
    uint8_t  tcp_state;     /* ndr_tcp_state_t                            */
    uint8_t  obs;           /* ndr_obs_t                                   */
    uint8_t  fin_seen[2];
    uint32_t reason_mask;   /* NDR_R_*                                     */

    uint32_t l7_proto;      /* ndr_l7_proto_t, 0 — не классифицирован      */
    uint32_t l7_txns[2];    /* разобранных транзакций по направлениям      */
    uint8_t  l7_confidence; /* 0..100                                      */
    uint8_t  in_use;
    /* Надгробие: слот освобождён истечением idle, но цепочка открытой
     * адресации через него ещё должна просматриваться. Без этого поиск
     * останавливался бы на дырке и терял поток, лежащий дальше по цепи. */
    uint8_t  tomb;
    uint16_t gen;           /* поколение слота: растёт при переиспользовании*/
    uint16_t dpi_id;        /* протокол по базе DPI (l7_proto == NDR_L7_DPI) */
    uint64_t flow_id;       /* стабильный идентификатор для evidence refs  */
} ndr_flow_t;

/* ── Счётчики таблицы: пределы обязаны быть видимыми ──────────────────── */
typedef struct {
    uint64_t pkts_in;
    uint64_t pkts_dropped_decode;
    uint64_t flows_created;
    uint64_t flows_evicted_idle;
    uint64_t flows_evicted_pressure; /* вытеснение из-за нехватки места    */
    uint64_t lookups;
    uint64_t probe_len_max;
    uint64_t fragments;
    uint64_t truncated_pkts;
    uint64_t ts_regressions;   /* пакет с меткой раньше последней в потоке */
} ndr_flow_stats_t;

typedef struct {
    ndr_flow_t      *slots;
    uint32_t         capacity;      /* степень двойки                      */
    uint32_t         mask;
    uint32_t         live;
    uint64_t         next_flow_id;
    uint64_t         now_us;        /* последний увиденный ts              */
    ndr_flow_stats_t st;
} ndr_flow_table_t;

/* Инициализация поверх памяти вызывающего. bytes должен вмещать
 * capacity * sizeof(ndr_flow_t); capacity округляется ВНИЗ до степени
 * двойки. Возврат: 0 — успех, -1 — аргументы, -2 — памяти не хватает
 * даже на минимальную таблицу (NDR_FLOW_MIN_SLOTS). */
#define NDR_FLOW_MIN_SLOTS 16u
int  ndr_flow_table_init(ndr_flow_table_t *t, void *mem, size_t bytes);
void ndr_flow_table_reset(ndr_flow_table_t *t);
size_t ndr_flow_table_bytes_for(uint32_t slots);

/* Построение канонического ключа из пакета. Направление возвращается в
 * *dir (NDR_DIR_AB/NDR_DIR_BA): ключ не зависит от того, чей пакет пришёл
 * первым, а направление сохраняется отдельно. */
void ndr_flow_key_from_pkt(const ndr_pkt_t *p, ndr_flow_key_t *k, uint32_t *dir);

/* Основной вход. Возвращает поток (никогда не NULL при rc==0).
 * rc: 0 — учтено; -1 — аргументы; -2 — пакет не пригоден для учёта. */
int ndr_flow_observe(ndr_flow_table_t *t, const ndr_pkt_t *p, ndr_flow_t **out);

/* Поиск без создания. NULL — нет такого потока. */
ndr_flow_t *ndr_flow_find(ndr_flow_table_t *t, const ndr_flow_key_t *k);

/* Закрытие потоков, молчащих дольше idle_us, относительно t->now_us.
 * Возвращает число закрытых. Вызывающий обязан звать это сам: таймеров
 * внутри контракта нет (M10 — своего recovery/таймера у модуля нет). */
uint32_t ndr_flow_expire_idle(ndr_flow_table_t *t, uint64_t idle_us);

/* Итерация по живым потокам. idx — курсор вызывающего, начинать с 0. */
ndr_flow_t *ndr_flow_iter(ndr_flow_table_t *t, uint32_t *idx);

/* Наблюдаемость потока пересчитывается из reason_mask и увиденных
 * направлений. Функция чистая: используется и тестом, и учётом. */
ndr_obs_t ndr_flow_obs_of(const ndr_flow_t *f);

const char *ndr_obs_name(ndr_obs_t o);
/* Печать причин в буфер, стабильный порядок. Возвращает длину. */
size_t ndr_reason_str(uint32_t mask, char *buf, size_t n);
include/platx/ndr_host.h
/* platx/ndr_host.h — CLI домена NDR.
 *
 * Та же граница, что у PXSIG и PXSANDBOX: домен NDR не делает I/O (гейт
 * `t_ndr_no_effects` держит это свойство), поэтому чтение файла захвата и
 * печать живут здесь и в `src/ndr/{ndr_cli,cmd_ndr}.c`.
 *
 * Логика печатает в FILE*, чтобы её можно было прогнать без консоли;
 * `cmd_ndr.c` — тонкий адаптер к console_printf.
 */
int  ndr_cli(int argc, char **argv, FILE *out, FILE *err);
int  ndr_cli_script(const char *path, FILE *out, FILE *err);
void ndr_register_console(void);

/* Представления (src/ndr/ndr_cli_view.c). */
int  ndr_cli_intel_load(const char *path, ndr_intel_snapshot_t **out, FILE *err);
const char *ndr_cli_intel_source(void);
int  ndr_cli_flows(ndr_pipeline_t *p, uint32_t limit, FILE *out);
int  ndr_cli_protocols(ndr_pipeline_t *p, FILE *out);
int  ndr_cli_dpi(int list, FILE *out);
int  ndr_cli_names(ndr_pipeline_t *p, uint32_t limit, FILE *out);
int  ndr_cli_hunt(ndr_pipeline_t *p, const char *what, FILE *out, FILE *err);
int  ndr_cli_coverage(ndr_pipeline_t *p, int json, FILE *out);
int  ndr_cli_finding_json(ndr_pipeline_t *p, uint32_t i, FILE *out, FILE *err);
int  ndr_cli_history(ndr_pipeline_t *p, const char *op, const char *path, FILE *out, FILE *err);
include/platx/ndr_hpack.h
/* platx/ndr_hpack.h — декодер HPACK (RFC 7541) для h2c без I/O и malloc.
 *
 * Зачем: без HPACK у HTTP/2 нет ни :authority, ни :path — то есть нет
 * сверки имён с IOC и нет записи в историю. До этого модуля h2c
 * кадрировался, а имена честно помечались PARSER_UNSUP.
 *
 * Границы, которые здесь не обходятся:
 *  - Динамическая таблица — фиксированная память NDR_HPACK_DYN_BYTES на
 *    направление. Если пир объявил таблицу больше, чем мы можем зеркалить,
 *    или мы пропустили блок заголовков (CONTINUATION, потеря), индексы
 *    расходятся, и таблица объявляется ПОТЕРЯННОЙ (`lost`): ссылки в неё
 *    больше не разрешаются и считаются, а не подставляются наугад.
 *    Потерянная таблица не восстанавливается до конца соединения.
 *  - Коды Хаффмана и статическая таблица — сгенерированы из пакета
 *    `hpack` (tests/ndr/gen_hpack_tables.py), а не набраны из RFC руками.
 *  - Строки — данные атакующего: обрезаются по границе поля с пометкой,
 *    непечатаемое заменяется.
 */

#define NDR_HPACK_DYN_BYTES   4096u             /* = SETTINGS default   */
#define NDR_HPACK_DYN_ENTRIES 128u              /* 4096 / 32 (мин. запись) */
#define NDR_HPACK_FIELD_MAX   256u

typedef struct {
    uint16_t name_off, name_len, val_off, val_len;
} ndr_hpack_ent_t;

typedef struct {
    uint8_t  buf[NDR_HPACK_DYN_BYTES];
    ndr_hpack_ent_t ent[NDR_HPACK_DYN_ENTRIES]; /* ent[0] — самая старая */
    uint32_t count;
    uint32_t used;          /* байт buf занято (сплошным префиксом)        */
    uint32_t size;          /* размер по RFC: Σ(name+value+32)             */
    uint32_t max_size;      /* действующий предел (пир / size update)      */
    uint8_t  lost;          /* таблица потеряна — ссылки не разрешаются    */
    uint8_t  lost_reason;   /* NDR_HPACK_LOST_*                            */
    uint32_t inserts, evictions, dyn_refs, dyn_unresolved, huffman_errors;
} ndr_hpack_dyn_t;

enum {
    NDR_HPACK_LOST_NONE      = 0,
    NDR_HPACK_LOST_TOO_LARGE = 1,   /* пир объявил таблицу больше нашей   */
    NDR_HPACK_LOST_SKIPPED   = 2,   /* блок заголовков не декодирован     */
    NDR_HPACK_LOST_MALFORMED = 3,   /* ошибка в середине блока: вставки
                                     * после неё неизвестны                */
};

/* Аномалии блока. */
#define NDR_HPACK_A_TRUNCATED   0x0001u
#define NDR_HPACK_A_BAD_INDEX   0x0002u   /* индекс 0 или вне таблиц       */
#define NDR_HPACK_A_HUFFMAN     0x0004u   /* EOS внутри / плохая набивка   */
#define NDR_HPACK_A_OVERLONG    0x0008u   /* строка обрезана по полю       */
#define NDR_HPACK_A_DYN_LOST    0x0010u   /* ссылка в потерянную таблицу   */
#define NDR_HPACK_A_SIZE_UPDATE 0x0020u   /* был dynamic table size update */
#define NDR_HPACK_A_INT_OVERFLOW 0x0040u

typedef struct {
    char     authority[NDR_HPACK_FIELD_MAX];
    char     path[NDR_HPACK_FIELD_MAX];
    char     method[16];
    char     scheme[8];
    char     status[4];
    char     user_agent[NDR_HPACK_FIELD_MAX];
    int64_t  content_length;     /* -1: не объявлен                        */
    uint16_t headers;            /* полей декодировано                    */
    uint16_t huffman_strings;
    uint16_t dyn_refs;           /* ссылок в динамическую таблицу         */
    uint16_t dyn_unresolved;     /* из них не разрешено (таблица потеряна)*/
    uint16_t anomalies;
    uint32_t consumed;
} ndr_hpack_hdrs_t;

void ndr_hpack_dyn_init(ndr_hpack_dyn_t *t);
/* Пир объявил SETTINGS_HEADER_TABLE_SIZE: применить к декодеру, который
 * читает заголовки, закодированные ЭТИМ пиром. Больше ёмкости — lost. */
void ndr_hpack_dyn_set_max(ndr_hpack_dyn_t *t, uint32_t max_size);
/* Блок заголовков пропущен (не декодирован): таблица считается потерянной,
 * потому что вставки из блока неизвестны. */
void ndr_hpack_dyn_mark_skipped(ndr_hpack_dyn_t *t);

/* Декодировать один блок заголовков. 0 — блок разобран (возможно с
 * аномалиями), -1 — аргументы, -2 — блок не разобран (структурная ошибка;
 * таблица помечена потерянной). t может быть NULL — тогда динамические
 * ссылки не разрешаются и считаются. */
int ndr_hpack_decode(ndr_hpack_dyn_t *t, const uint8_t *blk, uint32_t len,
                     ndr_hpack_hdrs_t *out);

/* Декодер Хаффмана отдельно (для тестов и других строк): возвращает длину
 * или -1 при ошибке (EOS, набивка не из единиц, длиннее 7 бит). */
int ndr_hpack_huffman_decode(const uint8_t *in, uint32_t len,
                             char *out, uint32_t cap);
include/platx/ndr_intel.h
/* platx/ndr_intel.h — контракт intel.match:v1: проекция IOC на снимок PXSIG.
 *
 * ТЗ, раздел 2: «NDR не создаёт свой updater, trust store или альтернативный
 * формат пакетов… Контракт intel.match:v1 — переиспользуемая проекция IOC на
 * snapshot PXSIG».
 *
 * Поэтому здесь НЕТ и не должно появиться: чтения файла, проверки подписи,
 * загрузки, ротации, сети. Снимок приходит готовым и неизменяемым: массивы
 * const, поколение и digest — снаружи. Всё, что делает этот контракт, —
 * отвечает на вопрос «есть ли такой индикатор в ЭТОМ снимке» и подписывает
 * ответ поколением снимка, чтобы находка не пережила своё основание.
 *
 * Отдельно: совпадение IOC не является вердиктом. Оно лишь один признак,
 * и разрешение реагировать из него не следует (ТЗ, раздел 5 «Реагирование»).
 */

#define NDR_INTEL_CONTRACT "intel.match:v1"
#define NDR_IOC_DOMAIN_MAX 128u

typedef enum {
    NDR_IOC_IPV4   = 1,
    NDR_IOC_IPV6   = 2,
    NDR_IOC_DOMAIN = 3,
    NDR_IOC_JA3    = 4
} ndr_ioc_type_t;

typedef struct {
    uint64_t id;               /* идентификатор записи в снимке PXSIG      */
    uint8_t  addr[16];         /* IPv4 — первые 4 байта, остальные нули    */
    uint8_t  ip_ver;
    uint8_t  prefix_len;       /* для IP: длина префикса (32/128 — точный) */
    uint8_t  confidence;       /* 0..100, приходит из источника            */
    uint8_t  source_id;        /* идентификатор источника внутри снимка    */
    /* Когда индикатор стал известен (мкс с эпохи), 0 — неизвестно.
     * Нужен retrospective hunt'у, чтобы отличать «событие было до того,
     * как мы узнали» от «мы знали и пропустили» — это разные выводы. */
    uint64_t known_since_us;
} ndr_ioc_ip_t;

typedef struct {
    uint64_t id;
    char     name[NDR_IOC_DOMAIN_MAX];  /* нормализовано: нижний регистр   */
    uint8_t  suffix_match;     /* 1 — совпадает и любой поддомен           */
    uint8_t  confidence;
    uint8_t  source_id;
    uint64_t known_since_us;   /* см. ndr_ioc_ip_t                          */
} ndr_ioc_domain_t;

typedef struct {
    uint64_t id;
    char     md5[33];          /* 32 hex в нижнем регистре + NUL           */
    uint8_t  confidence;
    uint8_t  source_id;
    uint64_t known_since_us;
} ndr_ioc_ja3_t;

/* Неизменяемый снимок. Все указатели — на память вызывающего, живущую не
 * меньше снимка. Поля generation/bundle_id/digest попадают в КАЖДУЮ находку. */
typedef struct {
    const ndr_ioc_ip_t     *ips;      /* отсортирован по (ip_ver, addr)    */
    uint32_t                n_ips;
    const ndr_ioc_domain_t *domains;  /* отсортирован по name (memcmp)     */
    uint32_t                n_domains;
    const ndr_ioc_ja3_t    *ja3s;     /* отсортирован по md5 (strcmp)      */
    uint32_t                n_ja3s;
    uint64_t                generation;
    uint64_t                bundle_id;
    uint8_t                 digest[32];
    uint8_t                 sealed;   /* прошёл ndr_intel_seal()           */
} ndr_intel_snapshot_t;

typedef struct {
    uint8_t  matched;
    uint8_t  kind;             /* ndr_ioc_type_t                           */
    uint8_t  suffix;           /* совпадение по суффиксу домена            */
    uint8_t  confidence;
    uint64_t ioc_id;
    uint8_t  source_id;
    uint64_t snapshot_generation;  /* находка не переживает своё основание */
    uint64_t bundle_id;
    uint64_t known_since_us;
} ndr_intel_match_t;

/* Проверка снимка перед использованием: отсортированность и границы строк.
 * Fail-closed: неотсортированный снимок НЕ используется, потому что
 * бинарный поиск по нему молча не находил бы часть индикаторов —
 * то есть выдавал бы «чисто» там, где совпадение есть.
 * 0 — снимок опечатан и пригоден, -1 — аргументы, -2 — порядок нарушен,
 * -3 — строка не завершена нулём / вне алфавита. */
int ndr_intel_seal(ndr_intel_snapshot_t *s);

/* Поиск. Возвращает 1 при совпадении, 0 если нет, -1 при неопечатанном
 * снимке (использовать непроверенный снимок нельзя). */
int ndr_intel_lookup_ip(const ndr_intel_snapshot_t *s, const uint8_t *addr,
                        uint8_t ip_ver, ndr_intel_match_t *out);
int ndr_intel_lookup_domain(const ndr_intel_snapshot_t *s, const char *name,
                            ndr_intel_match_t *out);
int ndr_intel_lookup_ja3(const ndr_intel_snapshot_t *s, const char *md5hex,
                         ndr_intel_match_t *out);
include/platx/ndr_ipfrag.h
/* platx/ndr_ipfrag.h — сборка IP-фрагментов с бюджетом (ndr.flow:v1).
 *
 * ТЗ, раздел 2: нормализация обязана покрывать фрагментацию, а лимиты
 * пересборки — быть объявлены и видны. Здесь оба требования в одном
 * файле: датаграмма собирается в буфер фиксированного размера из памяти
 * структуры, всё, что не влезло или не дождалось хвоста, считается, а не
 * теряется.
 *
 * Политика перекрытия та же, что у TCP-пересборки, — FIRST-WINS, и по той
 * же причине: перекрывающиеся фрагменты с разным содержимым (teardrop и
 * его потомки) — это приём рассинхронизации сенсора и хоста. Конфликт не
 * применяется и считается отдельно.
 */

#define NDR_IPFRAG_SLOTS       32u
#define NDR_IPFRAG_MAX_DGRAM 8192u   /* больше — REASM_LIMIT, не рост      */
#define NDR_IPFRAG_TIMEOUT_US (30ull * 1000000ull)

typedef struct {
    uint8_t  in_use;
    uint8_t  ip_ver;
    uint8_t  proto;
    uint8_t  have_last;         /* видели фрагмент с MF=0                 */
    uint8_t  src[16], dst[16];
    uint32_t ip_id;
    uint32_t observation_domain, sensor_id;
    uint64_t sensor_boot_epoch;
    uint16_t vlan_id;
    uint8_t  tunnel_depth;
    uint8_t  truncated;         /* хоть один фрагмент был обрезан          */
    uint64_t first_ts_us, last_ts_us;
    uint32_t total_len;         /* известна после последнего фрагмента     */
    uint32_t top;               /* верхняя граница принятых байт           */
    uint32_t frags, overlap, conflict;
    uint64_t wire_bytes;
    uint8_t  buf[NDR_IPFRAG_MAX_DGRAM];
    uint8_t  bits[NDR_IPFRAG_MAX_DGRAM / 8u];
} ndr_ipfrag_dgram_t;

typedef struct {
    uint64_t started;
    uint64_t completed;
    uint64_t timed_out;
    uint64_t evicted;           /* слот отобран у незавершённой датаграммы  */
    uint64_t oversize;          /* датаграмма больше NDR_IPFRAG_MAX_DGRAM    */
    uint64_t conflicts;
    uint64_t bad_fragment;      /* смещение/длина вне допустимого           */
} ndr_ipfrag_stats_t;

typedef struct {
    ndr_ipfrag_dgram_t d[NDR_IPFRAG_SLOTS];
    ndr_ipfrag_stats_t st;
} ndr_ipfrag_t;

void ndr_ipfrag_init(ndr_ipfrag_t *t);

/* Принять фрагмент (p->is_fragment || p->more_frags, ip_payload заполнен).
 * Возврат: 1 — датаграмма собрана целиком, *done указывает на неё (валиден
 * до следующего вызова push/expire); 0 — принято, ждём; -1 — аргументы;
 * -2 — отвергнуто (oversize / нет слота / битый фрагмент), причина в st. */
int ndr_ipfrag_push(ndr_ipfrag_t *t, const ndr_pkt_t *p,
                    const ndr_ipfrag_dgram_t **done);

/* Освободить слот собранной датаграммы (после того, как вызывающий
 * построил из неё пакет). */
void ndr_ipfrag_release(ndr_ipfrag_t *t, const ndr_ipfrag_dgram_t *d);

/* Истечение незавершённых датаграмм. Возвращает число закрытых. */
uint32_t ndr_ipfrag_expire(ndr_ipfrag_t *t, uint64_t now_us);

/* Построить нормализованный пакет из собранной датаграммы. Порты и L7
 * берутся из собранного буфера через ndr_decode_l4 (вызывающий зовёт сам,
 * чтобы не тянуть декодер сюда). */
void ndr_ipfrag_to_pkt(const ndr_ipfrag_dgram_t *d, ndr_pkt_t *out);
include/platx/ndr_lite.h
/* platx/ndr_lite.h — «лёгкие» разборщики для протоколов, опознанных базой
 * DPI: не полный разбор, а РОВНО те поля, которые нужны детектору и истории —
 * имена (кто входил, кому писали), факт передачи секрета открытым текстом,
 * результат попытки входа, код ошибки.
 *
 * Границы: разбирается одно сообщение/строка за вызов; поля, которых нет в
 * сообщении, пусты; ничего не выводится из отсутствия. Как и всё в домене —
 * ноль I/O, ноль malloc, ноль состояния между вызовами.
 *
 * Поддержано: SMTP/LMTP (MAIL FROM/RCPT TO/AUTH, коды ответов), FTP (USER/
 * PASS, 230/530), POP3 (USER/PASS), IMAP (LOGIN), LDAP (bindRequest simple/
 * sasl, bindResponse resultCode), Kerberos (AS-REQ/TGS-REQ cname@realm,
 * AS-REP/TGS-REP, KRB-ERROR error-code + cname), MySQL (HandshakeResponse41
 * username, OK/ERR), PostgreSQL (StartupMessage user/database, PasswordMessage
 * cleartext, AuthenticationOk, ErrorResponse 28xxx), Redis (AUTH,
 * -WRONGPASS/-NOAUTH), VNC/RFB (типы безопасности, SecurityResult), Telnet
 * (подсказки сервера: login:/Password:/Login incorrect), SNMP v1/v2c
 * (community = секрет открытым текстом; public/private — слабый), MSSQL TDS
 * (Login7 user, LOGINACK / ERROR 18456), MongoDB (saslStart/authenticate:
 * механизм, имя из SCRAM-payload, code 18). Остальные протоколы базы —
 * только классификация.
 */

#define NDR_LITE_MAX_NAMES 4u
#define NDR_LITE_NAME_MAX  96u

typedef struct {
    uint8_t  field;                 /* ndr_name_field_t                     */
    char     value[NDR_LITE_NAME_MAX];
} ndr_lite_name_t;

typedef struct {
    uint16_t dpi_id;
    uint8_t  n_names;
    ndr_lite_name_t names[NDR_LITE_MAX_NAMES];
    uint8_t  password_observed;     /* секрет ушёл открытым текстом         */
    uint8_t  auth_attempt;          /* сообщение — попытка входа            */
    uint8_t  auth_result;           /* 0 — не известно, 1 — успех, 2 — отказ */
    uint8_t  generic_result;        /* ответ «ок/ошибка» БЕЗ привязки к попытке
                                     * (POP3 +OK/-ERR, IMAP tag OK/NO, Redis
                                     * +OK/-ERR): результатом входа становится
                                     * только если вызывающий знает, что перед
                                     * ним была попытка (состояние соединения) */
    uint32_t error_code;            /* LDAP resultCode / KRB error-code / SMTP|FTP код */
    uint8_t  krb_msg_type;          /* 10 AS-REQ 11 AS-REP 12 TGS-REQ 13 TGS-REP 30 KRB-ERROR */
    uint8_t  krb_preauth;           /* AS-REQ нёс padata                     */
    uint8_t  dhcp_msg_type;         /* DHCP option 53: 1 DISCOVER 2 OFFER 3 REQUEST 5 ACK 6 NAK */
    uint8_t  dhcp_server[4];        /* option 54 (server identifier), 0 — нет  */
    uint8_t  weak_auth;             /* аутентификация отсутствует/устарела по
                                     * наблюдённому выбору (VNC None, RDP
                                     * standard security) — НЕ гипотеза      */
    uint8_t  is_request;
    uint32_t consumed;
    uint16_t anomalies;             /* NDR_A_*                               */
} ndr_lite_txn_t;

/* 1 — для этого протокола есть лёгкий разборщик. */
int ndr_lite_supported(uint16_t dpi_id);

/* Разобрать одно сообщение. from_initiator: 1 — от инициатора соединения.
 * 0 — разобрано (consumed > 0); -3 — нужно больше данных; -2 — не для этого
 * протокола или сообщение не опознано (вызывающий решает, ждать или
 * закрыть); -1 — аргументы. */
int ndr_lite_parse(uint16_t dpi_id, const uint8_t *d, uint32_t len,
                   uint8_t from_initiator, ndr_lite_txn_t *out);
include/platx/ndr_md5.h
/* platx/ndr_md5.h — MD5 (RFC 1321) для отпечатков вида JA3.
 *
 * Это НЕ криптография доверия: MD5 здесь — соглашение о формате
 * отпечатка, принятое индустрией, чтобы значения можно было сравнивать с
 * общедоступными списками. Для подписей, целостности или ключей эта
 * функция непригодна и в домене NDR для этого не используется.
 */

void ndr_md5(const uint8_t *data, size_t len, uint8_t out[16]);
/* 32 hex-символа + NUL. buf не короче 33. */
void ndr_md5_hex(const uint8_t *data, size_t len, char *buf);
include/platx/ndr_module.h
/* platx/ndr_module.h — NDR как модуль платформы: identity, ingest и факт.
 *
 * Модуль не открывает ни сокета, ни файла: пакеты ему ПОДАЮТ через
 * capability `ndr.ingest`, а адаптер захвата (src/ndrcap) — отдельный
 * провайдер, который в платформенном варианте получает свой fd через
 * abi->xio (M3/M5). Так граница «домен без I/O» сохраняется и в модуле.
 *
 * Находки публикуются через plat_event коротким типизированным ФАКТОМ
 * (ТЗ, раздел 1: «plat_event передаёт короткие типизированные факты и
 * ссылки»). Полная находка (528 байт, строки features/claim) в событие не
 * помещается и не должна: она остаётся у домена и доступна по finding_id
 * через `ndr.findings`. Факт — это ссылка плюс то, что нужно подписчику,
 * чтобы решить, идти ли за полной записью.
 *
 * Имена capability ПРОЕКТНЫЕ (ТЗ, раздел 7): до решения о продвижении они
 * живут здесь, а не в names.h. Грамматика та же — <domain>.<token>.
 */

#define PLAT_NAME_NDR           "sensors.ndr"
#define PLAT_NAME_NDR_INGEST    "sensors.ndr.ingest"
#define PLAT_NAME_NDR_FINDINGS  "sensors.ndr.findings"
#define NDR_INGEST_V1   1u
#define NDR_FINDINGS_V1 1u

/* Блок 0x1600: 0x1200 hook, 0x1300 xim, 0x1400 attach, 0x1500 hades. */
#define PLAT_EV_NDR_FINDING      0x1600u
#define PLAT_EV_NDR_COVERAGE     0x1601u   /* потеря захвата / давление     */

/* Факт о находке. Умещается в PLAT_EV_PAY_MAX (256): проверяется
 * _Static_assert ниже, а не обещанием. Нет ни указателей, ни строк. */
typedef struct {
    uint32_t version;              /* 1                                   */
    uint32_t engine_version;
    uint64_t finding_id;           /* ссылка на полную запись             */
    uint8_t  kind, severity, confidence, requires_host_evidence;
    uint8_t  scope, obs, pad[2];
    uint32_t coverage_reason;
    uint32_t observations;
    uint32_t baseline_generation;
    uint64_t snapshot_generation;
    uint64_t bundle_id;
    uint64_t ioc_id;
    uint64_t flow_id;
    uint64_t first_ts_us, last_ts_us;
    ndr_flow_key_t key;            /* 64 байта                            */
} ndr_finding_fact_t;

typedef struct {
    uint32_t version;
    uint32_t reason;               /* NDR_R_* — что именно ухудшилось     */
    uint64_t ts_us;
    uint64_t count;                /* dropped / evicted                   */
} ndr_coverage_fact_t;

/* Вход пакетов: vtable capability `sensors.ndr.ingest`.
 * feed: 0 — учтён, -1 — модуль не принимает (quiesce/stop), -2 — пакет
 * непригоден. Пакет копируется в пределах вызова: l7-указатель после
 * возврата не хранится. */
typedef struct {
    int  (*feed)(void *inst, ndr_pkt_t *pkt);
    void (*capture_loss)(void *inst, uint64_t dropped, uint64_t ts_us);
    uint32_t (*expire)(void *inst, uint64_t now_us);
} ndr_ingest_v1_t;

/* Чтение находок: vtable capability `sensors.ndr.findings`. */
typedef struct {
    const ndr_finding_t *(*get)(void *inst, uint64_t finding_id);
    const ndr_finding_t *(*list)(void *inst, uint32_t *n);
} ndr_findings_v1_t;

/* Заполнить факт из находки. Чистая функция — используется и модулем,
 * и тестом, чтобы сверить, что ушло в событие. */
void ndr_finding_to_fact(const ndr_finding_t *f, ndr_finding_fact_t *out);
include/platx/ndr_pipeline.h
/* platx/ndr_pipeline.h — сборка контрактов в один проверяемый путь.
 *
 * Это тот самый «небольшой собственный проверяемый контур» из раздела 4 ТЗ:
 * захват → нормализация → пересборка → разбор → находки. Внешний DPI/IDS
 * (nDPI, Suricata) сюда не линкуется и линковаться не должен: его место —
 * заменяемый провайдер за границей CHILD, и его отсутствие не мешает
 * контуру работать.
 *
 * Ни одной функции ввода-вывода. На вход подаётся БУФЕР с содержимым
 * захвата; кто и как его прочитал — забота вызывающего. Ни одного malloc:
 * весь размер состояния — это sizeof(ndr_pipeline_t), и он печатается
 * тестом. Ни одного обращения к часам: время берётся из записей захвата,
 * поэтому один и тот же вход даёт бит-в-бит один и тот же результат
 * (NDR-01), в том числе digest.
 */

/* Размеры — параметры профиля развёртывания, задаются при сборке
 * (-DNDR_PIPE_FLOW_SLOTS=65536). Значения по умолчанию — для тестов и
 * стенда; для узла их выбирает профиль по замеру new flows/s × idle. */
#define NDR_PIPE_FLOW_SLOTS   512u
#define NDR_PIPE_REASM_SLOTS    8u
#define NDR_PIPE_REASM_WIN   8192u

/* Верхняя граница транзакций, разбираемых за один пакет: защита от
 * входа, где каждый байт — «транзакция». Остальное дождётся следующего
 * пакета, а не съест процессор здесь. */
#define NDR_PIPE_TXN_PER_PKT    8u
/* Заголовки длиннее этого без пустой строки — не «ждём ещё», а отказ:
 * иначе поток из бесконечного заголовка держал бы окно вечно. */
#define NDR_PIPE_HDR_MAX     4096u
/* Столько байт направления достаточно, чтобы классифицировать протокол;
 * если и после них он неизвестен — он неизвестен, а не «ещё не пришёл». */
#define NDR_PIPE_CLASSIFY_MAX 512u
/* Слот пересборки отбирается у другого потока, только если тот молчал
 * дольше этого. Иначе при потоках > слотов окна перекидывались бы на
 * каждый пакет (замер 06.09.2026: 12 мкс/пакет вместо 0.6), а разбор
 * не получил бы ни один поток. Без слота поток разбирается по первому
 * сегменту с REASM_LIMIT — видимая деградация вместо thrash. */
#define NDR_PIPE_REASM_REUSE_US (2000000ull)

typedef struct {
    uint64_t flow_id;
    uint8_t  in_use;
    uint8_t  stop[2];        /* направление больше не разбирается (и почему
                              * — записано в reason_mask потока)           */
    uint8_t  proto;
    uint8_t  confidence;
    uint64_t skip[2];        /* байт тела, которые ещё предстоит пропустить */
    /* chunked-тело (RFC 9112 §7.1): 0 — нет; 1 — ждём строку размера;
     * 2 — внутри данных (chunk_left); 3 — ждём CRLF после данных;
     * 4 — трейлеры до пустой строки. Граница тела здесь ВЫЧИСЛЯЕТСЯ из
     * кадрирования, а не угадывается. */
    uint8_t  chunk_mode[2];
    uint64_t chunk_left[2];
    uint64_t last_use_us;    /* для отбора слота у самого давнего потока   */
    /* извлечение тела ответа (один объект на конвейер за раз) */
    uint8_t  x_active;       /* извлечение идёт в этом слоте               */
    uint8_t  x_dir;
    uint8_t  x_src;          /* ndr_object_source_t                         */
    uint8_t  x_over;         /* h2: тело не поместилось в бюджет — неполно */
    uint32_t x_stream;       /* h2: поток, чьё тело извлекается             */
    uint32_t x_len;
    int64_t  x_declared;
    uint16_t x_status;
    char     last_host[NDR_OBJ_FIELD_MAX];  /* из последнего запроса       */
    char     last_uri[NDR_OBJ_FIELD_MAX];
    /* HPACK: динамическая таблица на направление. hp[dir] зеркалит
     * кодировщик отправителя направления dir; SETTINGS из dir задают
     * предел для hp[1-dir]. Потерянная таблица не восстанавливается. */
    ndr_hpack_dyn_t hp[2];
    uint8_t  hp_init;
    /* Состояние «попытка входа → ответ» по направлениям: ответ сразу после
     * попытки в том же соединении становится её результатом. */
    uint8_t  auth_pending[2];
    ndr_reasm_t r;
    uint8_t  mem[2u * (NDR_PIPE_REASM_WIN + (NDR_PIPE_REASM_WIN + 7u) / 8u)];
} ndr_pipe_reasm_slot_t;

typedef struct {
    uint64_t pkts_total;
    uint64_t pkts_decoded;
    uint64_t pkts_undecodable;   /* не IP / кадр не разобран              */
    uint64_t recs_bad;           /* битые записи контейнера               */
    uint64_t l7_parsed;
    uint64_t l7_unsupported;
    uint64_t reasm_slots_exhausted; /* уже не растёт при переиспользовании */
    uint64_t reasm_slots_reused;    /* слот отобран у самого давнего потока */
    uint64_t reasm_slots_released;  /* освобождён при закрытии/истечении    */
    uint64_t ipfrag_completed;   /* датаграмм собрано из фрагментов       */
    uint64_t ipfrag_rejected;    /* фрагментов отвергнуто (см. frag.st)   */
    uint64_t ipfrag_pending_fed; /* нет: фрагмент ждёт — не учтён потоком */
    uint64_t capture_drops;      /* пакетов потеряно ЯДРОМ/сенсором        */
    uint64_t capture_loss_events;/* сообщений о потере                     */
    uint64_t flows_marked_loss;  /* потоков, помеченных PKT_LOSS по этому  */
    uint64_t chunked_bodies;     /* chunked-тел пропущено по кадрированию   */
    uint64_t intel_switches;     /* смен снимка IOC на живом конвейере      */
    uint64_t objects_extracted;  /* объектов отдано приёмнику целиком       */
    uint64_t objects_oversize;   /* объявленный размер больше бюджета       */
    uint64_t objects_busy;       /* приёмник занят другим объектом          */
    uint64_t objects_incomplete; /* поток закрылся/истёк до конца тела      */
    uint64_t objects_rejected;   /* приёмник вернул отказ                   */
    uint64_t h2_blocks_decoded;  /* блоков заголовков HPACK декодировано     */
    uint64_t h2_blocks_undecoded;/* не декодировано: нет слота / CONTINUATION
                                  * вне буфера / структурная ошибка         */
    uint64_t h2_dyn_lost;        /* таблиц HPACK объявлено потерянными       */
    uint64_t h2_dyn_unresolved;  /* ссылок в потерянную таблицу             */
    uint64_t h2_data_frames;     /* DATA-кадров учтено                       */
    uint64_t l7_classified_dpi;  /* направлений опознано базой DPI (без разбора) */
    uint64_t lite_parsed;        /* сообщений разобрано лёгкими разборщиками  */
    uint64_t dpi_ambiguous;      /* из них с совпадением ещё одной сигнатуры  */
} ndr_pipe_stats_t;

typedef struct {
    ndr_flow_table_t      ft;
    ndr_flow_t            flows[NDR_PIPE_FLOW_SLOTS];
    ndr_pipe_reasm_slot_t reasm[NDR_PIPE_REASM_SLOTS];
    ndr_detect_t          det;
    ndr_ipfrag_t          frag;
    ndr_store_t           store;        /* сохранённая история для hunt   */
    ndr_pipe_stats_t      st;
    uint32_t              obs_domain;
    uint32_t              sensor_id;
    uint64_t              sensor_boot_epoch;
    const ndr_intel_snapshot_t *snap;   /* может быть NULL                */
    uint64_t loss_until_us;  /* до этого момента новые потоки несут PKT_LOSS */
    ndr_object_sink_t sink;  /* приёмник извлечённых объектов; fn==NULL — нет */
    uint8_t  sink_busy;      /* буфер приёмника занят текущим извлечением     */
} ndr_pipeline_t;

/* Окно после сообщения о потере, в течение которого новые потоки
 * считаются затронутыми: пакет мог быть их первым. */
#define NDR_PIPE_LOSS_GRACE_US (1000000ull)

int  ndr_pipeline_init(ndr_pipeline_t *p, const ndr_detect_cfg_t *cfg,
                       uint32_t obs_domain, uint32_t sensor_id,
                       uint64_t boot_epoch);
void ndr_pipeline_bind_intel(ndr_pipeline_t *p, const ndr_intel_snapshot_t *s);

/* Прогнать буфер захвата целиком. Возвращает число учтённых пакетов,
 * либо отрицательное — контейнер не открылся. Битая ЗАПИСЬ внутри файла
 * не роняет прогон: она считается в recs_bad, и обработка останавливается
 * на ней (дальше файла всё равно не разобрать). */
int  ndr_pipeline_run_pcap(ndr_pipeline_t *p, const uint8_t *buf, size_t len);

/* Один уже разобранный пакет — для тестов и для живых адаптеров. */
int  ndr_pipeline_feed(ndr_pipeline_t *p, ndr_pkt_t *pkt);

/* Истечение простаивающих потоков и датаграмм относительно now_us:
 * истёкшие потоки уходят в историю. Живой адаптер зовёт это сам по своим
 * часам — таймеров внутри нет (M10). Возвращает число закрытых потоков. */
uint32_t ndr_pipeline_expire(ndr_pipeline_t *p, uint64_t now_us);

/* Сенсор/ядро сообщили, что dropped пакетов не дошли до нас. Это НЕ
 * пакет и не поток — это дыра в наблюдении. Все живые потоки получают
 * PKT_LOSS (любой из них мог потерять сегмент), а потоки, начатые в
 * ближайшую секунду, — тоже: их первый пакет мог быть среди потерянных.
 * Потеря, о которой не сообщили конвейеру, выглядит как «трафика не
 * было», и именно поэтому у этой функции нет пути «проигнорировать». */
void ndr_pipeline_note_capture_loss(ndr_pipeline_t *p, uint64_t dropped,
                                    uint64_t ts_us);

/* Завершение прогона: все живые потоки уходят в историю, детект выпускает
 * накопленные гипотезы. run_pcap зовёт это сам. */
void ndr_pipeline_finish(ndr_pipeline_t *p);

/* Приёмник извлечённых объектов (тел HTTP-ответов). Память под объект — у
 * приёмника; один объект за раз; тела больше cap не извлекаются и
 * считаются oversize. sink==NULL снимает приёмник. */
void ndr_pipeline_set_object_sink(ndr_pipeline_t *p, const ndr_object_sink_t *sink);

/* Retrospective hunt по сохранённой истории против снимка (может
 * отличаться от привязанного к конвейеру — в этом и смысл). */
int  ndr_pipeline_hunt(const ndr_pipeline_t *p, const ndr_intel_snapshot_t *snap,
                       uint64_t from_us, uint64_t to_us, uint64_t now_us,
                       ndr_hunt_result_t *out);

/* Детерминированный отпечаток нормализованных фактов: ключи потоков,
 * счётчики, состояние TCP, наблюдаемость и причины. НЕ включает адреса
 * указателей, порядок слотов и время процесса. */
uint64_t ndr_pipeline_digest(const ndr_pipeline_t *p);

/* Отпечаток находок: вид, ключ, уверенность, поколения. */
uint64_t ndr_pipeline_findings_digest(const ndr_pipeline_t *p);
include/platx/ndr_proto.h
/* platx/ndr_proto.h — контракт ndr.protocol:v1: классификация и разбор L7.
 *
 * ТЗ, раздел 2 требует по каждому протоколу объявить пять вещей:
 * классификацию и её уверенность; поддержанные версии/команды/поля; анализ
 * состояния и аномалий; поддержанные сигнатурные семейства и условия
 * видимости; неподдержанные/зашифрованные режимы и допустимую деградацию.
 * Здесь объявлено ровно то, что реализовано, — таблица покрытия внизу файла
 * и есть тот самый список, и она обязана совпадать с кодом, а не с планом.
 *
 * Классификация ведётся ПО СОДЕРЖИМОМУ, порт — только подсказка. ТЗ прямо
 * запрещает исключать разборщик из-за нестандартного порта: детектор,
 * который смотрит только на 53/tcp, не видит DNS на 5353 и молчит именно
 * там, где интереснее всего.
 */

#define NDR_PROTO_CONTRACT "ndr.protocol:v1"

typedef enum {
    NDR_L7_UNKNOWN = 0,
    NDR_L7_DNS     = 1,
    NDR_L7_HTTP    = 2,
    NDR_L7_TLS     = 3,
    NDR_L7_SSH     = 4,
    NDR_L7_SMB     = 5,
    NDR_L7_RDP     = 6,
    NDR_L7_ENCRYPTED_OTHER = 7,
    NDR_L7_HTTP2   = 8,        /* h2c: кадрирование + HPACK (ndr_hpack)     */
    NDR_L7_QUIC    = 9,        /* long header: версия/CID; содержимое шифровано */
    NDR_L7_DPI     = 10        /* опознан базой сигнатур (ndr_dpi): имя — по
                                * flow->dpi_id; разбора нет, только класс     */
} ndr_l7_proto_t;

/* Аномалии разбора. Это НАБЛЮДЕНИЯ, а не вердикты. */
#define NDR_A_TRUNCATED       0x0001u /* транзакция обрывается на границе   */
#define NDR_A_MALFORMED       0x0002u /* поле не соответствует формату      */
#define NDR_A_LABEL_LOOP      0x0004u /* DNS: цикл указателей сжатия        */
#define NDR_A_OVERLONG        0x0008u /* поле длиннее разумного предела     */
#define NDR_A_UNSUPPORTED_VER 0x0010u /* версия вне объявленного покрытия   */
#define NDR_A_NONPRINTABLE    0x0020u /* непечатаемые байты там, где текст  */

#define NDR_DNS_NAME_MAX  256u
#define NDR_HTTP_FIELD_MAX 256u
#define NDR_TLS_SNI_MAX   256u

#define NDR_DNS_MAX_ANSWERS 8u

/* Запись ответа. Разбираются только A, AAAA и CNAME — ровно те типы,
 * которые связывают имя с адресом и нужны сопоставлению с IOC; остальные
 * пропускаются с сохранением типа. */
typedef struct {
    uint16_t type;
    uint32_t ttl;
    uint8_t  ip_ver;           /* 4/6 для A/AAAA, 0 — адреса нет         */
    uint8_t  addr[16];
    char     cname[NDR_DNS_NAME_MAX];
} ndr_dns_rr_t;

typedef struct {
    uint16_t id;
    uint8_t  is_response;
    uint8_t  opcode;
    uint8_t  rcode;
    uint8_t  truncated_flag;   /* бит TC из заголовка                     */
    uint16_t qdcount, ancount, nscount, arcount;
    uint16_t qtype, qclass;
    char     qname[NDR_DNS_NAME_MAX];
    uint16_t qname_len;
    uint16_t label_max;        /* самая длинная метка: признак туннеля     */
    uint16_t anomalies;
    uint32_t consumed;         /* байт сообщения, занятых разобранным      */
    ndr_dns_rr_t answers[NDR_DNS_MAX_ANSWERS];
    uint16_t n_answers;        /* разобрано записей ответа                 */
    uint16_t answers_skipped;  /* не поместилось в NDR_DNS_MAX_ANSWERS     */
} ndr_dns_txn_t;

typedef struct {
    char     method[16];
    char     uri[NDR_HTTP_FIELD_MAX];
    char     version[16];
    char     host[NDR_HTTP_FIELD_MAX];
    char     user_agent[NDR_HTTP_FIELD_MAX];
    /* Authorization: 0 — нет; 1 Basic (открытый текст, имя в auth_user);
     * 2 NTLM; 3 Negotiate; 4 Bearer; 5 Digest; 6 другое. Секрет НЕ хранится. */
    uint8_t  auth_scheme;
    char     auth_user[64];
    uint16_t hdr_count;
    uint16_t anomalies;
    uint8_t  is_request;
    uint16_t status;           /* для ответа                               */
    /* Граница транзакции в потоке. header_len == 0 означает, что конец
     * заголовков в буфер не попал (и поднят NDR_A_TRUNCATED); тогда
     * потреблять нечего — надо ждать данных, а не выдумывать границу. */
    uint32_t header_len;       /* байт до конца пустой строки включительно */
    int64_t  content_length;   /* -1: не объявлен; -2: chunked             */
} ndr_http_txn_t;

typedef struct {
    uint16_t record_version;   /* версия записи TLS                        */
    uint16_t hello_version;    /* legacy_version из ClientHello            */
    char     sni[NDR_TLS_SNI_MAX];
    uint16_t sni_len;
    uint16_t cipher_count;
    uint16_t ext_count;
    uint8_t  has_alpn;
    char     alpn[32];
    uint16_t anomalies;
    uint32_t record_len;       /* полная длина записи (5 + тело), 0 — обрыв */

    /* Материал для JA3 (без GREASE-значений, RFC 8701). Отпечаток —
     * признак клиента, а не вердикт: одинаковый JA3 у вредоноса и у
     * популярной библиотеки — обычное дело, и claim обязан это говорить. */
    uint16_t ciphers[64];
    uint16_t n_ciphers;
    uint16_t exts[64];
    uint16_t n_exts;
    uint16_t groups[32];
    uint16_t n_groups;
    uint8_t  pformats[8];
    uint16_t n_pformats;
    uint8_t  ja3_truncated;    /* списки длиннее приёмников: JA3 неточен  */
    char     ja3[512];         /* строка вида ver,c-c-c,e-e,g-g,p          */
    char     ja3_md5[33];
} ndr_tls_hello_t;

/* SMB (MS-SMB2 / MS-CIFS): заголовок, команда, сессия/дерево, путь
 * TREE_CONNECT. Тела команд, DCERPC поверх IPC$ и шифрованный SMB3
 * НЕ разбираются — покрытие говорит об этом явно. */
#define NDR_SMB_PATH_MAX 128u
typedef struct {
    uint8_t  dialect;          /* 1 — SMB1, 2 — SMB2/3                    */
    uint8_t  is_response;
    uint16_t command;          /* SMB2: MS-SMB2 §2.2.1; SMB1: байт команды */
    uint32_t status;           /* SMB2 NT_STATUS в ответе                  */
    uint64_t session_id;
    uint32_t tree_id;
    uint64_t message_id;
    uint8_t  signed_flag;
    uint8_t  encrypted;        /* SMB2 TRANSFORM_HEADER 0xFD 'S' 'M' 'B'   */
    char     tree_path[NDR_SMB_PATH_MAX]; /* TREE_CONNECT: \\host\share  */
    uint8_t  admin_share;      /* путь оканчивается на ADMIN$ / C$..Z$ / IPC$ */
    /* SMB2 CREATE (MS-SMB2 2.2.13): имя файла относительно шары, доступ и
     * disposition — чтобы отличать запись/создание от открытия на чтение. */
    char     create_path[NDR_SMB_PATH_MAX];
    uint32_t desired_access;   /* FILE_WRITE_DATA 0x2, GENERIC_WRITE 0x40000000 … */
    uint32_t create_disposition; /* 1 OPEN, 2 CREATE, 3 OPEN_IF, 4 OVERWRITE, 5 OVERWRITE_IF */
    uint8_t  create_write;     /* доступ на запись ИЛИ создание/перезапись   */
    uint8_t  create_exe;       /* имя оканчивается на .exe/.dll/.sys/.ps1/.bat/.cmd/.scr */
    uint32_t consumed;
    uint16_t anomalies;
} ndr_smb_txn_t;
int ndr_smb_parse(const uint8_t *d, uint32_t len, ndr_smb_txn_t *out);

/* SMB2 commands */
#define NDR_SMB2_NEGOTIATE     0x0000u
#define NDR_SMB2_SESSION_SETUP 0x0001u
#define NDR_SMB2_TREE_CONNECT  0x0003u
#define NDR_SMB2_CREATE        0x0005u
#define NDR_SMB2_IOCTL         0x000Bu

/* QUIC (RFC 9000): только long header — версия и длины CID. Initial-пакеты
 * шифрованы ключами, выводимыми из DCID; ClientHello/SNI здесь НЕ
 * извлекаются — для этого нужен HKDF+AES-GCM, и покрытие это называет. */
typedef struct {
    uint8_t  long_header;
    uint8_t  packet_type;      /* 0 Initial, 1 0-RTT, 2 Handshake, 3 Retry */
    uint32_t version;
    uint8_t  dcid_len, scid_len;
    uint8_t  dcid[20];
    uint16_t anomalies;
} ndr_quic_hdr_t;
int ndr_quic_parse(const uint8_t *d, uint32_t len, ndr_quic_hdr_t *out);

/* HTTP/2 (RFC 9113): кадрирование. Заголовки закодированы HPACK; здесь
 * они НЕ декодируются, а выдаются как дескрипторы блоков (смещение,
 * длина, поток, END_HEADERS) — декодер `ndr_hpack` живёт отдельно, потому
 * что ему нужна динамическая таблица на соединение, а кадрированию — нет.
 * SETTINGS_HEADER_TABLE_SIZE отдаётся вызывающему: он задаёт предел
 * таблицы декодера ВСТРЕЧНОГО направления. */
#define NDR_H2_PREFACE_LEN 24u
#define NDR_H2_MAX_BLOCKS  8u
typedef struct {
    uint32_t off, len;         /* полезная нагрузка блока без padding/priority */
    uint32_t stream_id;
    uint8_t  type;             /* 1 HEADERS, 9 CONTINUATION, 5 PUSH_PROMISE  */
    uint8_t  end_headers;
    uint8_t  end_stream;
} ndr_h2_block_t;
typedef struct {
    uint32_t off, len;         /* полезная нагрузка DATA без padding          */
    uint32_t stream_id;
    uint8_t  end_stream;
} ndr_h2_data_t;
typedef struct {
    uint8_t  preface_seen;
    uint32_t frames;
    uint32_t frames_headers;   /* type 1                                  */
    uint32_t frames_data;      /* type 0                                  */
    uint32_t frames_settings;  /* type 4                                  */
    uint32_t frames_other;
    uint32_t stream_id_max;
    uint32_t consumed;         /* байт полных кадров (+ преамбула)        */
    uint16_t anomalies;
    ndr_h2_block_t blocks[NDR_H2_MAX_BLOCKS];
    uint8_t  n_blocks;
    uint8_t  blocks_dropped;   /* блоков больше NDR_H2_MAX_BLOCKS — НЕ выданы */
    uint8_t  settings_table_seen; /* SETTINGS_HEADER_TABLE_SIZE в этом буфере */
    uint32_t settings_table_size;
    ndr_h2_data_t datas[NDR_H2_MAX_BLOCKS];
    uint8_t  n_datas;
    uint8_t  datas_dropped;    /* DATA-кадров больше NDR_H2_MAX_BLOCKS — НЕ выданы */
} ndr_h2_txn_t;
int ndr_h2_parse(const uint8_t *d, uint32_t len, ndr_h2_txn_t *out);

/* RDP: X.224 Connection Request (MS-RDPBCGR 2.2.1.1) поверх TPKT: cookie
 * mstshash=<user> и rdpNegReq.requestedProtocols. 0 — разобран; -2 — не CR;
 * -3 — нужно больше. Всё после согласования не разбирается. */
typedef struct {
    char     cookie_user[64];    /* из "Cookie: mstshash=" — пусто, если нет */
    uint8_t  neg_present;        /* rdpNegReq есть                            */
    uint32_t requested_protocols;/* 0 — standard RDP security, 1 TLS, 2 CredSSP, 8 RDSTLS */
    uint8_t  is_response;        /* X.224 CC: rdpNegRsp/rdpNegFailure         */
    uint32_t selected_protocol;  /* из rdpNegRsp                              */
    uint8_t  neg_failure;        /* rdpNegFailure (код в failure_code)        */
    uint32_t failure_code;
    uint32_t consumed;
    uint16_t anomalies;
} ndr_rdp_txn_t;
int ndr_rdp_parse(const uint8_t *d, uint32_t len, ndr_rdp_txn_t *out);

/* Классификация. Возвращает протокол, *confidence — 0..100.
 * port_hint: любой из портов потока (0, если неизвестен). Порт НЕ может
 * ни подтвердить, ни отменить содержимое: он только повышает confidence. */
ndr_l7_proto_t ndr_l7_classify(const uint8_t *d, uint32_t len,
                               uint16_t port_hint, uint8_t is_tcp,
                               uint8_t *confidence);

/* Разборщики. 0 — транзакция заполнена (возможно с anomalies),
 * -1 — аргументы, -2 — это не тот протокол / разобрать нечего. */
int ndr_dns_parse(const uint8_t *d, uint32_t len, ndr_dns_txn_t *out);
/* DNS поверх TCP: двухбайтный префикс длины (RFC 1035 §4.2.2).
 * -3 — сообщение ещё не пришло целиком (нужно больше данных). */
int ndr_dns_parse_tcp(const uint8_t *d, uint32_t len, ndr_dns_txn_t *out);
int ndr_http_parse(const uint8_t *d, uint32_t len, ndr_http_txn_t *out);
int ndr_tls_parse_client_hello(const uint8_t *d, uint32_t len,
                               ndr_tls_hello_t *out);

/* Явное объявление границ покрытия: строка для улик и Console «Coverage». */
const char *ndr_proto_coverage(ndr_l7_proto_t p);
const char *ndr_l7_name(ndr_l7_proto_t p);
/* Имя с учётом DPI: для NDR_L7_DPI — имя протокола из базы по dpi_id. */
const char *ndr_l7_name_ex(uint32_t l7, uint16_t dpi_id);

/* Классификация с результатом DPI (dpi может быть NULL). Разборщики домена
 * главнее: DPI спрашивается только если ни один из них не узнал нагрузку,
 * и ДО слабых эвристик (TPKT → «rdp»), чтобы S7/H.225 не тонули в них. */
ndr_l7_proto_t ndr_l7_classify_ex(const uint8_t *d, uint32_t len,
                                  uint16_t port_hint, uint8_t is_tcp,
                                  uint8_t *confidence, ndr_dpi_result_t *dpi);
include/platx/ndr_reasm.h
/* platx/ndr_reasm.h — ограниченная пересборка TCP-потока (ndr.protocol:v1).
 *
 * ТЗ, раздел 2: «Обязательны bounded flow-table, лимиты reassembly на поток
 * и в сумме… Политика обработки overlap и неоднозначной сборки фиксируется
 * и проверяется тестами.»
 *
 * Политика здесь одна и названа явно: FIRST-WINS. Байт, уже принятый в
 * окно, не переписывается пришедшим позже сегментом. Это не «как удобнее»:
 * перекрывающиеся сегменты с РАЗНЫМ содержимым — классический приём обхода
 * инспекции, при котором сенсор и хост собирают разные потоки. Поэтому
 * конфликт не только не применяется, но и считается отдельно
 * (overlap_conflict) — он сам по себе наблюдение, а не деталь реализации.
 *
 * Окно фиксировано и живёт в памяти вызывающего. Выход за окно не
 * «расширяет буфер», а поднимает NDR_R_REASM_LIMIT: предел, о котором
 * никто не узнал, — это не предел, а утечка.
 */

#define NDR_REASM_CONTRACT "ndr.reasm:v1"

typedef struct {
    uint32_t segs;             /* сегментов с данными принято            */
    uint32_t retrans;          /* сегмент целиком уже был в окне         */
    uint32_t reorder;          /* сегмент лёг за дырой (пришёл раньше)   */
    uint32_t overlap;          /* частичное пересечение с принятым       */
    uint32_t overlap_conflict; /* пересечение с ДРУГИМ содержимым        */
    uint32_t out_of_window;    /* не поместился в окно                   */
    uint32_t holes;            /* дыр в окне на текущий момент           */
    uint64_t bytes_accepted;
    uint64_t bytes_duplicate;
} ndr_reasm_dir_stats_t;

typedef struct {
    uint8_t *buf;              /* окно данных                            */
    uint8_t *bits;             /* битовая карта принятых байт            */
    uint32_t cap;              /* размер окна в байтах                   */
    uint32_t base;             /* seq, соответствующий buf[0]            */
    uint32_t contig;           /* длина непрерывного префикса от base    */
    uint32_t top;              /* верхняя граница принятых байт в окне   */
    uint8_t  started;          /* base установлен                        */
    ndr_reasm_dir_stats_t st;
} ndr_reasm_dir_t;

typedef struct {
    ndr_reasm_dir_t dir[2];
    uint32_t reason_mask;      /* NDR_R_REASM_LIMIT и т.п.               */
} ndr_reasm_t;

/* Сколько памяти нужно на оба направления при окне win байт. */
size_t ndr_reasm_bytes_for(uint32_t win);

/* Разложить память вызывающего на два направления. 0 — успех. */
int ndr_reasm_init(ndr_reasm_t *r, void *mem, size_t bytes, uint32_t win);

/* Принять сегмент. syn=1 означает, что seq — это ISN (данные начинаются
 * с seq+1). Возврат: 0 — учтено, -1 — аргументы. Отказов «потерять
 * данные молча» здесь нет: всё, что не поместилось, попадает в счётчики. */
int ndr_reasm_push(ndr_reasm_t *r, uint32_t dir, uint32_t seq, uint8_t syn,
                   const uint8_t *data, uint32_t len);

/* Непрерывный префикс окна: то, что уже можно отдать разборщику L7. */
const uint8_t *ndr_reasm_data(const ndr_reasm_t *r, uint32_t dir, uint32_t *len);

/* Сдвинуть окно на n принятых байт (после разбора). */
void ndr_reasm_consume(ndr_reasm_t *r, uint32_t dir, uint32_t n);
include/platx/ndr_render.h
/* platx/ndr_render.h — канонические представления для Console/Fleet.
 *
 * ТЗ, раздел 6 «Console»: Coverage и Detection explanation. Данные для них
 * уже есть в структурах; здесь — их стабильная сериализация (JSON, порядок
 * полей фиксирован, строки экранированы). Ядро по-прежнему ничего не
 * пишет — оно заполняет буфер вызывающего.
 *
 * Стабильность порядка — не косметика: две одинаковые находки обязаны
 * давать одинаковые байты, иначе по представлению нельзя сравнивать.
 */

/* Возвращают длину (без NUL) или -1, если буфер мал: усечённый JSON — не
 * JSON, и он не отдаётся частично. */
int ndr_render_finding_json(const ndr_finding_t *f, char *buf, size_t cap);
int ndr_render_coverage_json(const ndr_pipeline_t *p, char *buf, size_t cap);
include/platx/ndr_response.h
/* platx/ndr_response.h — намерение реагирования: то, что NDR МОЖЕТ, и то,
 * чего он НЕ МОЖЕТ по построению.
 *
 * ТЗ, раздел 5: «NDR публикует finding; получение свежей сигнатуры не даёт
 * права изменять firewall… Разрешённый response использует существующий
 * Action/Firewall provider: typed scope, TTL, idempotency, preconditions,
 * verify и undo. Неопределённая asset/process attribution не превращается
 * в широкую IP-блокировку.»
 *
 * Здесь NDR строит из находки ТИПИЗИРОВАННОЕ НАМЕРЕНИЕ в форме дескриптора
 * действующего Action-провайдера (platx/action_provider.h) — и это всё.
 * Три поля дескриптора NDR не заполняет и заполнить не может: lease_id,
 * authority_id и mode=LIVE. Без них Action Coordinator отказывает (D096),
 * а PLAN-режим он не исполняет никогда. Значит, намерение NDR — это план,
 * который Policy обязана явно превратить в действие, подписав его
 * lease/authority. Проверяется это не комментарием, а прогоном через
 * НАСТОЯЩИЙ координатор (tests/ndr/integ).
 *
 * Помимо этого намерение само отказывает себе в случаях, где сеть не
 * даёт основания: гипотеза (requires_host_evidence), низкая уверенность,
 * неопределённая attribution (доменное совпадение — это не адрес), путь
 * управления/ремонта, несовпадение поколения снимка, изоляция целого узла
 * по fan-out без явного разрешения.
 */

#define NDR_RESP_MGMT_MAX 8u

typedef struct {
    uint8_t  min_confidence;          /* ниже — отказ                       */
    uint8_t  allow_host_isolate;      /* 0: fan-out не изолирует узел       */
    uint8_t  allow_partial_obs;       /* 0: PARTIAL-наблюдение не основание */
    uint32_t ttl_s;                   /* срок действия намерения            */
    uint64_t active_snapshot_generation; /* precondition: то же поколение   */
    uint8_t  mgmt[NDR_RESP_MGMT_MAX][16];   /* management / repair path      */
    uint8_t  mgmt_ipver[NDR_RESP_MGMT_MAX];
    uint32_t n_mgmt;
} ndr_response_policy_t;

typedef enum {
    NDR_RESP_OK = 0,
    NDR_RESP_REFUSED_HYPOTHESIS   = 1,  /* нужны свидетельства хоста         */
    NDR_RESP_REFUSED_CONFIDENCE   = 2,
    NDR_RESP_REFUSED_ATTRIBUTION  = 3,  /* цель нельзя назвать однозначно    */
    NDR_RESP_REFUSED_SCOPE        = 4,  /* изоляция узла не разрешена        */
    NDR_RESP_REFUSED_MGMT_PATH    = 5,  /* путь управления неприкосновенен   */
    NDR_RESP_REFUSED_STALE_INTEL  = 6,  /* находка на другом поколении       */
    NDR_RESP_REFUSED_OBSERVABILITY= 7,  /* основание наблюдалось неполно     */
    NDR_RESP_REFUSED_KIND         = 8   /* для этого вида действия нет       */
} ndr_resp_rc_t;

typedef struct {
    action_descriptor_t d;            /* verb=ISOLATE, mode=PLAN, lease=0,
                                       * authority=0, evidence_ref=finding   */
    uint32_t ttl_s;
    uint64_t expires_at_us;
    uint64_t finding_id;
    uint8_t  target_ipver;
    uint8_t  target[16];
    char     reason[128];             /* почему намерение построено/нет     */
} ndr_response_intent_t;

void ndr_response_policy_defaults(ndr_response_policy_t *p);

/* Построить намерение. Возврат — ndr_resp_rc_t; при отказе out->reason
 * называет причину, дескриптор пуст. */
int  ndr_response_intent(const ndr_finding_t *f, const ndr_response_policy_t *pol,
                         uint64_t now_us, ndr_response_intent_t *out);

const char *ndr_resp_rc_name(int rc);
include/platx/ndr_store.h
/* platx/ndr_store.h — сохранённая сетевая история и retrospective hunt.
 *
 * ТЗ, раздел 6: «Retrospective Hunt запускается при новом IOC на доступной
 * сохранённой истории… различает время события и время получения знания
 * (AS_KNOWN_THEN). Новый IOC не переписывает старый verdict… Давность
 * поиска ограничена retention и реально сохранёнными полями.»
 *
 * Хранилище — кольцо фиксированного размера в памяти вызывающего. Когда
 * оно переполняется, старые записи вытесняются, и ЭТО ВИДНО: результат
 * поиска несёт признак полноты истории и границу retention. «Не нашли»
 * на неполной истории — это «не знаем», а не «не было».
 *
 * Поиск ничего не пишет в находки: он возвращает попадания вызывающему.
 * Старые вердикты не трогаются по построению — у функции нет доступа к
 * детекту.
 */

#define NDR_STORE_RECORDS   512u
#define NDR_STORE_NAMES     256u
#define NDR_STORE_NAME_MAX   96u
#define NDR_HUNT_MAX_HITS    64u

typedef enum {
    NDR_NAME_DNS_QNAME = 1,
    NDR_NAME_HTTP_HOST = 2,
    NDR_NAME_TLS_SNI   = 3,
    NDR_NAME_TLS_JA3   = 4,     /* name = md5 hex отпечатка                */
    /* поля лёгких разборщиков (ndr_lite) */
    NDR_NAME_SMTP_FROM = 5,     /* адрес отправителя                       */
    NDR_NAME_SMTP_RCPT = 6,     /* адрес получателя                        */
    NDR_NAME_SMTP_DOMAIN = 7,   /* доменная часть адреса — сверяется с IOC */
    NDR_NAME_LOGIN_USER = 8,    /* USER/LOGIN (FTP, POP3, IMAP)            */
    NDR_NAME_LDAP_BIND_DN = 9,  /* DN из bindRequest                       */
    NDR_NAME_KRB_CNAME = 10,    /* principal@REALM из AS-REQ/REP/ERROR     */
    NDR_NAME_DB_USER   = 11,    /* имя пользователя БД (MySQL/PostgreSQL)  */
    NDR_NAME_SSH_BANNER = 12,   /* строка версии SSH (клиент и сервер)     */
    NDR_NAME_DB_NAME   = 13,    /* имя базы данных (PostgreSQL)            */
    NDR_NAME_SMB_PATH  = 14     /* SMB2 CREATE: имя файла на шаре           */
} ndr_name_field_t;

typedef struct {
    uint8_t  in_use;
    uint8_t  tcp_state, obs;
    uint32_t reason_mask;
    uint32_t l7_proto;
    uint64_t flow_id;
    ndr_flow_key_t key;
    uint64_t first_ts_us, last_ts_us;
    uint64_t pkts[2], bytes[2];
    uint64_t recorded_at_us;    /* когда запись легла в историю          */
} ndr_flow_record_t;

typedef struct {
    uint8_t  in_use;
    uint8_t  field;             /* ndr_name_field_t                       */
    uint64_t flow_id;
    uint64_t ts_us;
    ndr_flow_key_t key;
    char     name[NDR_STORE_NAME_MAX];
} ndr_name_record_t;

typedef struct {
    ndr_flow_record_t recs[NDR_STORE_RECORDS];
    ndr_name_record_t names[NDR_STORE_NAMES];
    uint32_t rhead, nhead;
    uint64_t rwritten, revicted;
    uint64_t nwritten, nevicted;
    uint64_t oldest_kept_us;    /* самое старое событие, ещё в истории    */
} ndr_store_t;

typedef enum {
    NDR_KNOWN_UNKNOWN = 0,      /* у индикатора нет known_since            */
    NDR_KNOWN_BEFORE  = 1,      /* индикатор был известен на момент события */
    NDR_KNOWN_AFTER   = 2       /* знание пришло позже события             */
} ndr_known_then_t;

typedef struct {
    uint64_t event_ts_us;       /* когда это наблюдалось                  */
    uint64_t knowledge_ts_us;   /* когда об этом спросили (now)           */
    uint64_t ioc_known_since_us;
    uint8_t  as_known_then;     /* ndr_known_then_t                        */
    uint8_t  field;             /* 0 — совпадение по адресу потока         */
    uint8_t  obs;
    uint32_t reason_mask;
    uint64_t flow_id;
    uint64_t ioc_id;
    ndr_flow_key_t key;
    char     name[NDR_STORE_NAME_MAX];
} ndr_hunt_hit_t;

typedef struct {
    ndr_hunt_hit_t hits[NDR_HUNT_MAX_HITS];
    uint32_t n;
    uint8_t  truncated;         /* попаданий больше, чем поместилось       */
    uint8_t  history_complete;  /* ничего не вытеснялось за всё время      */
    uint8_t  window_before_retention; /* from_us раньше oldest_kept_us И
                                       * вытеснения были: часть окна утеряна */
    uint64_t records_scanned;
    uint64_t names_scanned;
    uint64_t oldest_kept_us;
    uint64_t snapshot_generation;
} ndr_hunt_result_t;

void ndr_store_init(ndr_store_t *s);
int  ndr_store_put_flow(ndr_store_t *s, const ndr_flow_t *f, uint64_t now_us);
int  ndr_store_put_name(ndr_store_t *s, const ndr_flow_t *f, uint8_t field,
                        const char *name, uint64_t ts_us);

/* Поиск по всей истории в окне [from_us, to_us] против снимка. now_us —
 * время запроса (knowledge time). Возврат: число попаданий (>=0), -1 —
 * аргументы или неопечатанный снимок. */
int  ndr_store_hunt(const ndr_store_t *s, const ndr_intel_snapshot_t *snap,
                    uint64_t from_us, uint64_t to_us, uint64_t now_us,
                    ndr_hunt_result_t *out);

/* ── перенос истории между запусками ─────────────────────────────────────
 * Ядро не пишет файлов; оно отдаёт байты и принимает байты. Формат:
 * заголовок (магия, версия, размеры, число записей, FNV-64 содержимого)
 * + сами записи. Импорт fail-closed: другая версия, другие размеры структур
 * или битый контрольный код — отказ целиком, а не «сколько прочиталось». */
#define NDR_STORE_WIRE_MAGIC   0x4e445253544f5231ull /* "NDRSTOR1" */
#define NDR_STORE_WIRE_VERSION 1u

typedef struct {
    uint64_t magic;
    uint32_t version;
    uint32_t rec_size, name_size;
    uint32_t n_recs, n_names;
    uint64_t rwritten, revicted, nwritten, nevicted, oldest_kept_us;
    uint64_t fnv;               /* FNV-64 над всеми записями после заголовка */
} ndr_store_wire_hdr_t;

/* Сколько байт нужно под экспорт текущего содержимого. */
size_t ndr_store_export_size(const ndr_store_t *s);
/* 0 — успех, *len — записано; -1 — аргументы; -2 — буфер мал. */
int  ndr_store_export(const ndr_store_t *s, uint8_t *buf, size_t cap, size_t *len);
/* 0 — история восстановлена; -1 — аргументы; -2 — формат/версия/размеры;
 * -3 — контрольный код не сошёлся. Существующее содержимое s заменяется
 * ТОЛЬКО при успехе. */
int  ndr_store_import(ndr_store_t *s, const uint8_t *buf, size_t len);
33

ndrcap

Поставщик пакетного наблюдения для сетевого анализа
src/ndrcap/Наблюдение и исследование3 файлов2 API headers

NDR Capture предоставляет NDR контролируемый источник пакетов с учётом состояния захвата и потерь. Жизненный цикл capture отделён от аналитического вывода. Данные о пропусках ядра, очередях и текущем состоянии помогают правильно оценить полноту исследуемого интервала.

Граница ответственности

  • NDR Capture предоставляет NDR контролируемый источник пакетов с учётом состояния захвата и потерь. Жизненный цикл capture отделён от аналитического вывода. Данные о пропусках ядра, очередях и текущем состоянии помогают правильно оценить полноту исследуемого интервала.

Устройство подсистемы

  • регистрация `ndr capture` в консоли платформы. ndrcap НЕ регистрирует отдельный глагол верхнего уровня. Вместо этого он расширяет существующий глагол `ndr`, добавляя подкоманду `capture`. Вызывается из register_cmds дескриптора модуля ndrcap. В ndr_cli.c диспетчер проверяет argv[1] == "capture" и вызывает
  • обход блоков TPACKET_V3 и слой системных вызовов. Разделение на две половины — не архитектурная прихоть. Обход блока (ndrcap_walk_block) можно проверить без CAP_NET_RAW, на буфере, собранном тестом, включая злонамеренные смещения: кольцо разделено с ядром, но границы всё равно проверяются — доверять tp_next_offset

Управление и диагностика

Корневые команды: capture. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / ndrcap →
Состав подсистемы / 3 файлов
Файл / компонентНазначение и граница
src/ndrcap/cmd_ndrcap.cрегистрация `ndr capture` в консоли платформы. ndrcap НЕ регистрирует отдельный глагол верхнего уровня. Вместо этого он расширяет существующий глагол `ndr`, добавляя подкоманду `capture`. Вызывается из register_cmds дескриптора модуля ndrcap. В ndr_cli.c диспетчер проверяет argv[1] == "capture" и вызывает
src/ndrcap/ndrcap_cli.cCLI провайдера захвата TPACKET_V3 (NDR capture). ЧТО ПОКАЗЫВАЕТ, ПОЧЕМУ. start — запустить захват: интерфейс, фильтр, путь вывода. stop — остановить, напечатать итоговую статистику с потерями. stats — текущая статистика В РЕАЛЬНОМ ВРЕМЕНИ. КЛЮЧЕВОЕ: tp_drops из tpacket_stats_v3 — потери на стороне
src/ndrcap/ndrcap_tpv3.cобход блоков TPACKET_V3 и слой системных вызовов. Разделение на две половины — не архитектурная прихоть. Обход блока (ndrcap_walk_block) можно проверить без CAP_NET_RAW, на буфере, собранном тестом, включая злонамеренные смещения: кольцо разделено с ядром, но границы всё равно проверяются — доверять tp_next_offset
Контракты API / 2 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/ndrcap.h
/* platx/ndrcap.h — живой источник пакетов: AF_PACKET / TPACKET_V3.
 *
 * Это АДАПТЕР сенсора (ТЗ, раздел 2: «адаптеры существующих сенсоров
 * предоставляют пакет… с явно указанным уровнем наблюдаемости»), а не
 * часть контура разбора. Граница проходит здесь: всё, что делает
 * системные вызовы, живёт в src/ndrcap и НЕ попадает под гейт «ноль I/O»
 * домена src/ndr. Внутри платформенного модуля эти вызовы уходят через
 * abi->xio (M3); в утилите — напрямую в libc. Разделение достигнуто
 * таблицей операций ndrcap_ops_t: обход кольца от системных вызовов не
 * зависит и проверяется синтетическими блоками без прав.
 *
 * Главная обязанность адаптера — не «отдавать пакеты», а ЧЕСТНО СЧИТАТЬ,
 * чего он не отдал: tp_drops из PACKET_STATISTICS, TP_STATUS_LOSING на
 * кадре, замороженные блоки. Потеря, о которой конвейер не узнал, — это
 * «трафика не было».
 */

#define NDRCAP_CONTRACT "ndr.capture.afpacket:v1"

/* Кадр, отданный обходом блока. data указывает внутрь блока кольца и
 * действителен, пока блок не возвращён ядру. */
typedef struct {
    uint64_t       ts_us;
    uint32_t       cap_len;    /* tp_snaplen                              */
    uint32_t       orig_len;   /* tp_len                                  */
    const uint8_t *data;
    uint8_t        losing;     /* TP_STATUS_LOSING: перед кадром были потери */
    uint16_t       vlan_tci;   /* если ядро сняло тег: TP_STATUS_VLAN_VALID */
    uint8_t        vlan_valid;
} ndrcap_frame_t;

typedef struct {
    uint64_t blocks;
    uint64_t frames;
    uint64_t frames_losing;    /* кадров с флагом LOSING                  */
    uint64_t blocks_malformed; /* блок отвергнут обходом (границы)        */
    uint64_t frames_malformed;
    uint64_t kernel_packets;   /* tp_packets (сумма)                      */
    uint64_t kernel_drops;     /* tp_drops   (сумма) — ПОТЕРЯНО ЯДРОМ     */
    uint64_t freeze_q;         /* tp_freeze_q_cnt                          */
} ndrcap_stats_t;

/* Обход одного блока TPACKET_V3. Чистая функция над буфером: каждый
 * заголовок проверяется на границы блока, кадр — на границы своего
 * слота. Возвращает число кадров, выданных через cb; -1 — блок не
 * разобран (считается в st->blocks_malformed). Обход прекращается на
 * первом некорректном кадре: доверять смещениям после него нельзя. */
typedef int (*ndrcap_frame_fn)(const ndrcap_frame_t *fr, void *ud);
int ndrcap_walk_block(const uint8_t *block, uint32_t block_len,
                      ndrcap_stats_t *st, ndrcap_frame_fn cb, void *ud);

/* Слой системных вызовов. Подменяется в тестах. */
typedef struct {
    int   (*socket)(int dom, int type, int proto);
    int   (*setsockopt)(int fd, int lvl, int opt, const void *v, uint32_t l);
    int   (*getsockopt)(int fd, int lvl, int opt, void *v, uint32_t *l);
    int   (*bind_ifindex)(int fd, int ifindex);
    void *(*mmap_ring)(int fd, size_t len);
    int   (*munmap_ring)(void *p, size_t len);
    int   (*poll_fd)(int fd, int timeout_ms);
    int   (*close)(int fd);
    unsigned (*if_nametoindex)(const char *name);
} ndrcap_ops_t;

typedef struct {
    uint32_t block_size;       /* степень двойки, кратна PAGE              */
    uint32_t block_nr;
    uint32_t frame_size;
    uint32_t timeout_ms;       /* tp_retire_blk_tov                        */
    uint8_t  promisc;          /* не реализовано в v1: DEFER              */
} ndrcap_cfg_t;

typedef struct {
    const ndrcap_ops_t *ops;
    ndrcap_cfg_t cfg;
    int      fd;
    uint8_t *ring;
    size_t   ring_len;
    uint32_t next_block;
    ndrcap_stats_t st;
    int      last_errno;
    char     err[96];          /* человекочитаемая причина отказа open     */
} ndrcap_t;

void ndrcap_default_cfg(ndrcap_cfg_t *cfg);
const ndrcap_ops_t *ndrcap_libc_ops(void);

/* Открыть захват на интерфейсе. Fail-closed: любая ошибка настройки
 * закрывает сокет и возвращает -1 с текстом в c->err; полусостояния
 * «сокет есть, кольца нет» не бывает. -2 — нет прав (EPERM/EACCES). */
int  ndrcap_open(ndrcap_t *c, const ndrcap_ops_t *ops, const ndrcap_cfg_t *cfg,
                 const char *ifname);

/* Обработать готовые блоки (poll с таймаутом). Возвращает число кадров,
 * -1 — ошибка. После каждого блока опрашивается PACKET_STATISTICS; число
 * потерь ядра передаётся вызывающему через *drops (0 — не было). */
int  ndrcap_poll(ndrcap_t *c, int timeout_ms, ndrcap_frame_fn cb, void *ud,
                 uint64_t *drops);

void ndrcap_close(ndrcap_t *c);
include/platx/ndrcap_host.h
/* platx/ndrcap_host.h — CLI провайдера захвата NDR (ndrcap).
 *
 * ndrcap реализует захват сетевого трафика через TPACKET_V3.
 * Интегрируется в существующий глагол `ndr` как подкоманда `capture`:
 *   ndr capture start  — начать захват
 *   ndr capture stop   — остановить
 *   ndr capture stats  — статистика с ЧЕСТНЫМ показом потерь ядра
 *
 * ПОЧЕМУ ЧЕСТНЫЙ ПОКАЗ ПОТЕРЬ:
 *   TPACKET_V3 имеет кольцевой буфер. Если userspace не успевает — пакеты
 *   теряются МОЛЧА на стороне ядра. `tp_drops` из tpacket_stats — факт,
 *   а не повод для беспокойства. CLI_CONTRACT §3: «Отсутствие наблюдения
 *   ≠ чистота». Захват с 5% потерями не может называться «полным».
 */
/* Состояние сессии захвата. */
typedef enum {
    CAP_STATE_IDLE    = 0,
    CAP_STATE_RUNNING = 1,
    CAP_STATE_STOPPED = 2,
    CAP_STATE_ERROR   = 3,
} cap_state_t;

const char *cap_state_name(cap_state_t s);

/* Статистика от ядра (из tpacket_stats_v3). */
typedef struct {
    uint32_t    tp_packets;    /* принято из кольца                           */
    uint32_t    tp_drops;      /* потеряно ядром (переполнение кольца)       */
    uint32_t    tp_freeze_q;   /* пакеты в заморожённых блоках               */
    double      drop_rate;     /* tp_drops / (tp_packets + tp_drops)         */
} cap_kernel_stats_t;

/* Статус текущей сессии захвата. */
typedef struct {
    cap_state_t        state;
    const char        *iface;
    const char        *filter;       /* BPF-выражение или NULL               */
    uint32_t           ring_size_kb;
    uint64_t           bytes_written;
    uint64_t           packets_written;
    uint64_t           session_ns;   /* продолжительность                    */
    cap_kernel_stats_t kstats;
    const char        *output_path;  /* куда пишем                           */
} plat_cap_status_t;

/* ── API ядра (реализует ndrcap_core.c) ────────────────────────────────── */

/* Запустить захват. iface — интерфейс, filter — BPF или NULL.
 * output_path — pcapng-файл. Возвращает 0 или код ошибки. */
int plat_cap_start(const char *iface, const char *filter,
                   const char *output_path, uint32_t ring_size_kb);

/* Остановить захват. */
int plat_cap_stop(void);

/* Текущий статус (включая статистику ядра). */
int plat_cap_get_status(plat_cap_status_t *out);

/* ── CLI ─────────────────────────────────────────────────────────────────── */

/* Точка входа для подкоманды `ndr capture`.
 * argc/argv — аргументы НАЧИНАЯ с подглагола (start|stop|stats). */
int plat_cap_cli(int argc, char **argv, FILE *out, FILE *err);

/* Регистрирует расширение для `ndr capture` в консоли платформы.
 * Вызывается из register_cmds модуля ndrcap. */
void plat_cap_register_console(void);
34

net

Вспомогательная политика повторных обращений к endpoint
src/net/Связь и ввод-вывод1 файлов1 API headers

Сетевой вспомогательный слой предоставляет ограниченную политику повторов вызывающему компоненту. Он не становится самостоятельным менеджером сессии. Бюджет, окончательный отказ и решение о восстановлении принадлежат владельцу операции.

Граница ответственности

  • Сетевой вспомогательный слой предоставляет ограниченную политику повторов вызывающему компоненту. Он не становится самостоятельным менеджером сессии. Бюджет, окончательный отказ и решение о восстановлении принадлежат владельцу операции.

Управление и диагностика

Домен не регистрирует собственную корневую команду. Его контракт используется вызывающими подсистемами через API и интерфейсы ra2c, transports.

Справочник CLI / net →
Состав подсистемы / 1 файлов
Файл / компонентНазначение и граница
src/net/endpoint_retry.cРеализация endpoint / retry
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/endpoint_retry.h
typedef enum plat_endpoint_rc {
    PLAT_ENDPOINT_OK = 0,
    PLAT_ENDPOINT_RESOLVE = -1,
    PLAT_ENDPOINT_EXHAUSTED = -2,
    PLAT_ENDPOINT_STALE = -3
} plat_endpoint_rc_t;

typedef int (*plat_endpoint_resolve_fn)(void *arg, const char *name,
                                        char *endpoint, size_t cap);

typedef struct plat_endpoint_retry {
    const char *name;
    unsigned max_attempts;
    uint64_t base_delay_ms;
    unsigned attempts;
    uint64_t next_attempt_ms;
    uint64_t generation;
    char endpoint[256];
    char refusal[96];
} plat_endpoint_retry_t;

void plat_endpoint_retry_init(plat_endpoint_retry_t *r, const char *name,
                              unsigned max_attempts, uint64_t base_delay_ms);
plat_endpoint_rc_t plat_endpoint_retry_step(plat_endpoint_retry_t *r,
                                             uint64_t now_ms,
                                             plat_endpoint_resolve_fn resolve,
                                             void *arg);
36

observe

Реестр источников и нормализация наблюдений
src/observe/Наблюдение и исследование21 файлов3 API headers

Observe предоставляет registry источников наблюдения и нормализованный claim/fact contract. Он объединяет synthetic, FIM/inotify и будущие sensors, не превращаясь в capability registry или policy engine.

Граница ответственности

  • Source registration owner-scoped и internal; внешние consumers получают event/fact schema.
  • Fact immutable; source не решает response.
  • Synthetic source test-only/profile marked.

Устройство подсистемы

  • Source descriptor: name/version/kind, owner, schemas, start/stop/health и coverage.
  • Claim normalizer validates subject/context/time/confidence/payload bounds.
  • Subscription/filter uses Core event or MBus based payload size.

Поток работы

  • Source start → coverage active.
  • Raw observation → normalize claim.
  • Publish event/MBus → consumers.
  • Source loss → coverage degrade event.

Отказ и восстановление

  • Malformed claim rejected and source error counter.
  • Source flood bounded/drop policy.
  • Source owner restart revokes registration/subscriptions.
  • inotify overflow becomes coverage gap, not silent continuation.

Основные возможности

  • Registers observation sources and normalizes claims.
  • Synthetic source supports deterministic tests.
  • FIM/inotify sources feed filesystem observations.

Управление и диагностика

Корневые команды: observe. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / observe →
Состав подсистемы / 21 файлов
Файл / компонентНазначение и граница
src/observe/_to_delete/sense_mod.cPLATX SENSE v1 module lifecycle (SENSE201-202) Fixed against ABI-v1 headers: - sense_event_header_t fields: schema_version, monotonic_ns, coverage_epoch - sense_ring_publish(owner, prio, class, wire, len) - sense_tlv_write(buf, bufsz, off, type, val, len) → int
src/observe/_to_delete/sense_registry.cSENSE v2 source registry + OBS0 adapter. SENSE051-SENSE060: descriptor module, capability lifecycle, OBS0 compat. SENSE053-SENSE056: thread-safe registry, no callbacks under locks.
src/observe/_to_delete/sense_ring.cSENSE bounded multi-consumer domain ring. SENSE061-SENSE100: ring, consumers, loss policies, coverage epoch, gap, snapshot barriers, heartbeat, CLI status, teardown. - Lock-rank: ring->mu always taken before consumer->mu (SENSE055/SENSE056). - Slow consumer never blocks producer or other consumers (SENSE066).
src/observe/cmd_observe.cрегистрация глагола `observe` в консоли платформы. Адаптер open_memstream: вывод plat_obs_cli() собирается в буфер, затем одним вызовом console_puts отдаётся в платформенную консоль. Вызывается из register_cmds дескриптора модуля observe.
src/observe/fs/fim_baseline.cFIM0: file integrity baseline. Bounded table: path → (size, mtime_ns, sha256). Slot ceiling enforced. Overflow → the (MAX+1)th add returns -1; existing slots are not affected. Не трогать: src/netlink tree, hades tree, plat_event.c
src/observe/fs/fim_baseline.hFIM0: file integrity baseline (internal). Bounded table: path → (size, mtime_ns, sha256). Slot ceiling enforced. Overflow → the (MAX+1)th add returns -1; existing slots are not affected. Не трогать: src/netlink tree, hades tree, plat_event.c
src/observe/fs/obs_inotify.cOBS3: inotify targeted watch source. Bounded allowlist (not recursive /). Observed events: IN_CREATE IN_DELETE IN_MODIFY IN_ATTRIB IN_MOVED_FROM IN_MOVED_TO IN_Q_OVERFLOW: kernel lost events — state → DEGRADED, kernel_lost++, GAP claim emitted (domain ring; not plat_event). Never treat overflow
src/observe/fs/obs_inotify.hinotify targeted watch source (internal). Not a platx/ public API. Include with -iquote src/observe/fs. Не трогать: src/netlink tree, hades tree, plat_event.c
src/observe/obs_abi.cOBS0: observation source registry and dispatch. Manages a static table of up to PLAT_OBS_SOURCES_MAX sources. The registry stores a POINTER to the source struct; the caller must keep the struct alive for the duration of the registration. Thread-safety: single-threaded MVP; no locks.
src/observe/obs_claim.cOBS1: claim normalization. Не трогать: src/netlink tree, hades tree, plat_event.c
src/observe/obs_mock.cOBS0: bounded synthetic observation source. Registered as "obs.mock.v1". Designed for unit tests only. probe → always available (synthetic; no kernel dependency). start → resets counters; emits PLAT_OBS_MOCK_FACTS synthetic facts counted in received; state → RUNNING.
src/observe/observe_cli.cCLI плоскости наблюдения. Использует реальный platx/observe.h напрямую: plat_obs_health(id, out) — текущее состояние источника. plat_obs_stats(id, out) — накопленная статистика. plat_obs_probe(id, out) — доступность источника. Добавляет слабую заглушку plat_obs_list() — перечисление источников,
src/observe/observe_metrics.cРеализация observe / metrics
src/observe/observectl_main.cавтономный бинарь observectl. gcc -std=c11 -Wall -I include \ src/observe/observe_cli.c \ src/observe/observectl_main.c
src/observe/sense_detector.cPLATX SENSE v1 bounded native detector runner SENSE156-174: 10 golden correlations.
src/observe/sense_identity.cSENSE host entity index. SENSE107-SENSE120: process/thread/file/socket/ns entity keys. PID alone is not a key; birth+pidns+boot required (SENSE108). PID reuse detected via birth/generation mismatch (SENSE108, SENSE115). Identity conflict emitted instead of silent overwrite (SENSE118).
src/observe/sense_metrics.cPLATX SENSE v1 typed resource metric snapshots SENSE138-143: CPU/memory/PSI/disk/network/HV collectors.
src/observe/sense_mod.cSENSE v1 module lifecycle (SENSE081, SENSE087, SENSE156)
src/observe/sense_normalize.cSENSE provider decoders / normalizers. SENSE129-SENSE147: Hades/OBS0/SP normalizers, enrichment queue, privacy redaction, XIO enrichment path.
src/observe/sense_registry.cSENSE v1 source registry (SENSE029, SENSE054, SENSE059) Tracks registered observation sources, enforces uniqueness, owner validity, and degrades coverage state to BLIND on sustained silence.
src/observe/sense_ring.cSENSE v1 lock-free ring buffer (SENSE040-060) Single-producer / multi-consumer SPMC ring.
Контракты API / 3 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/obs_claim.h
/* platx/obs_claim.h — OBS1: normalized observation claim.
 *
 * A claim is the canonical, self-contained fact produced by any observation
 * sensor after normalization. It names: who acted (subject), what they did
 * (operation), what they acted on (object), the outcome (result), which sensors
 * contributed (proof_mask), how certain (confidence), and when (ts).
 *
 * Claims are self-contained: no pointers. They ride the domain ring.
 *
 * GAP rule: when a sensor detects a coverage hole — overflow, restart, state
 * transition — it emits a GAP claim, never silence. A rule that requires
 * continuous coverage MUST see a GAP to know the epoch is discontinuous.
 * A GAP claim must never be treated as evidence of an event.
 *
 * Не трогать: src/netlink tree, hades tree, plat_event.c
 */

#define OBS_CLAIM_ID_MAX   64   /* sensor/source id ceiling (incl. NUL) */
#define OBS_CLAIM_OP_MAX   32   /* operation name ceiling */
#define OBS_CLAIM_PATH_MAX 256  /* object path ceiling */

/* ── Subject: who acted ─────────────────────────────────────────────────────
 * available = 0 (UNAVAILABLE): the sensor could not identify the actor.
 * A bare PID without birth + pid_ns_inum is not a valid identity. */
typedef struct obs_subject {
    uint64_t birth;         /* process birth time (monotonic ns) */
    uint32_t pid;
    uint32_t pid_ns_inum;   /* pid namespace inode */
    int      available;     /* 0 = UNAVAILABLE */
} obs_subject_t;

/* ── Object: what was acted on ──────────────────────────────────────────────
 * path is bounded. available = 0 when the sensor cannot name the object. */
typedef struct obs_object {
    char path[OBS_CLAIM_PATH_MAX];
    int  available;
} obs_object_t;

/* ── Operation result ────────────────────────────────────────────────────────
 * UNKNOWN = 0 (zeroed): outcome not observed by the sensor. */
typedef enum obs_result {
    OBS_RESULT_UNKNOWN = 0,
    OBS_RESULT_SUCCESS = 1,
    OBS_RESULT_DENIED  = 2,
    OBS_RESULT_ERROR   = 3
} obs_result_t;

/* ── Proof-source mask ───────────────────────────────────────────────────────
 * Bits indicating which sensors contributed evidence. May be OR-ed.
 * A claim with no proof bits set is at best INFERRED. */
#define OBS_PROOF_AUDIT    (1u << 0)   /* Linux audit subsystem */
#define OBS_PROOF_INOTIFY  (1u << 1)   /* inotify targeted watch */
#define OBS_PROOF_FANOTIFY (1u << 2)   /* fanotify */
#define OBS_PROOF_HOOK     (1u << 3)   /* platx hook engine */
#define OBS_PROOF_XIM      (1u << 4)   /* XIM mediated syscall */
#define OBS_PROOF_EBPF     (1u << 5)   /* eBPF tracepoint or LSM */

typedef uint32_t obs_proof_mask_t;

/* ── Confidence ──────────────────────────────────────────────────────────────
 * INFERRED = 0 (zeroed, weakest). Higher value → stronger evidence.
 * Computed from proof_mask by obs_claim_confidence_from_mask().
 * Confidence never decreases: adding proof only strengthens a claim. */
typedef enum obs_confidence {
    OBS_CONF_INFERRED         = 0,  /* no direct observation */
    OBS_CONF_CORRELATED       = 1,  /* multiple indirect sources assembled */
    OBS_CONF_MEDIATED         = 2,  /* XIM-mediated syscall interposition */
    OBS_CONF_DIRECT_INTERPOSE = 3,  /* hook/kprobe/inotify/fanotify */
    OBS_CONF_DIRECT_KERNEL    = 4   /* strongest: eBPF tracepoint/LSM/audit */
} obs_confidence_t;

/* ── Normalized claim ────────────────────────────────────────────────────────
 * Self-contained; no pointers. Suitable for the domain ring.
 *
 * is_gap == 1: this is a coverage-gap marker, NOT an event observation.
 *   When is_gap is set, only source and ts are meaningful. Consumers must
 *   never treat a GAP claim as evidence that an event occurred. */
typedef struct obs_claim {
    obs_subject_t    subject;
    char             operation[OBS_CLAIM_OP_MAX];  /* e.g. "open", "exec" */
    obs_object_t     object;
    obs_result_t     result;
    char             source[OBS_CLAIM_ID_MAX];     /* originating sensor id */
    uint32_t         owner;                        /* subsystem owner id */
    uint64_t         ts;                           /* monotonic ns */
    obs_proof_mask_t proof_mask;
    obs_confidence_t confidence;
    int              is_gap;  /* 1: epoch discontinuity — not an event claim */
} obs_claim_t;

/* ── Normalization API ───────────────────────────────────────────────────────
 * All functions require a non-NULL claim pointer.
 */

/* Zero-initialize and mark as GAP. source names the sensor that detected the
 * coverage hole; ts is the discontinuity time. Emit a GAP when the sensor
 * detects overflow, restart, or any state transition that breaks the epoch. */
void obs_claim_init_gap(obs_claim_t *c, const char *source, uint64_t ts);

/* Zero-initialize a normal (non-gap) claim with source and ts required.
 * confidence starts at INFERRED; add proof bits to raise it. */
void obs_claim_init(obs_claim_t *c, const char *source, uint64_t ts);

/* Set the subject. Passing all-zero values sets available = 0 (UNAVAILABLE).
 * Do not invent a subject when the sensor cannot identify the actor. */
void obs_claim_set_subject(obs_claim_t *c,
                            uint32_t pid, uint64_t birth, uint32_t pid_ns_inum);

/* Set the object path. Empty or NULL path sets available = 0. */
void obs_claim_set_object(obs_claim_t *c, const char *path);

/* Set the operation name (bounded copy). */
void obs_claim_set_operation(obs_claim_t *c, const char *op);

/* Set the result. */
void obs_claim_set_result(obs_claim_t *c, obs_result_t result);

/* Set the subsystem owner id. */
void obs_claim_set_owner(obs_claim_t *c, uint32_t owner);

/* Add proof bits and raise confidence if the new combined mask warrants it.
 * Confidence only increases: adding weaker proof never lowers it. */
void obs_claim_add_proof(obs_claim_t *c, obs_proof_mask_t mask);

/* Derive confidence purely from a proof mask (pure function, no side effects).
 * Strongest sensor type in the mask wins. */
obs_confidence_t obs_claim_confidence_from_mask(obs_proof_mask_t mask);

/* 1 if the claim is well-formed evidence: not a gap, has a source id, ts != 0.
 * Does not validate completeness of subject/object/operation. */
int obs_claim_ok(const obs_claim_t *c);
include/platx/observe.h
/* platx/observe.h — OBS0: observation source ABI and bounded stats.
 *
 * An observation source is a stateful sensor that emits facts onto the
 * domain ring. Each source moves through the lifecycle:
 *   STOPPED → (start) → RUNNING → (overflow) → DEGRADED → (stop) → STOPPED
 *
 * State invariant: UNAVAILABLE = 0. A zeroed plat_obs_health_t means
 * "no source seen" — it is never mistaken for a running source.
 *
 * Facts ride the domain ring, never plat_event per observation.
 * No SQL, no network, no BPF in this header.
 *
 * Не трогать: src/netlink tree, hades tree, plat_event.c
 */

#define PLAT_OBS_ID_MAX      64   /* source id ceiling (incl. NUL) */
#define PLAT_OBS_SOURCES_MAX 16   /* static table ceiling */

/* ── Source state ──────────────────────────────────────────────────────────
 * UNAVAILABLE = 0: zeroed struct → "no source"; probe failed or absent.
 * Ordered so a healthy running source is unambiguously > 0. */
typedef enum plat_obs_state {
    PLAT_OBS_STATE_UNAVAILABLE = 0,  /* no source; never "ok" */
    PLAT_OBS_STATE_STOPPED     = 1,  /* registered but not emitting */
    PLAT_OBS_STATE_RUNNING     = 2,  /* emitting facts onto domain ring */
    PLAT_OBS_STATE_DEGRADED    = 3   /* partial: ring overflow or kernel loss */
} plat_obs_state_t;

/* ── Bounded health / stats ────────────────────────────────────────────────
 * Returned by both health() and stats(). Never lies:
 *   state == UNAVAILABLE when no source is present or running.
 * received + dropped + kernel_lost are monotonic counters. */
typedef struct plat_obs_health {
    uint64_t         received;     /* facts accepted into the domain ring */
    uint64_t         dropped;      /* dropped: ring-full or filter */
    uint64_t         kernel_lost;  /* events the kernel reported lost */
    uint32_t         queue_depth;  /* current local queue occupancy */
    plat_obs_state_t state;        /* UNAVAILABLE when not running */
} plat_obs_health_t;

/* ── Probe result ──────────────────────────────────────────────────────────
 * probe() fills this; caller checks .available before calling start(). */
typedef struct plat_obs_probe {
    int  available;    /* non-zero: source can be started on this host */
    char reason[128];  /* human-readable when available == 0 */
} plat_obs_probe_t;

/* ── Source operations ─────────────────────────────────────────────────────
 * All ops receive the source's private ctx pointer.
 *
 * probe:  fill *out; return 0 on success (not availability: call error).
 * start:  begin emitting facts onto the domain ring; return 0 on success.
 * stop:   cease emitting; MUST close any fd opened by start; idempotent.
 * health: fill *out with current counters + state; return 0.
 * stats:  fill *out with cumulative counters; return 0.
 *         (stats == health for sources without a separate accumulator.)
 */
typedef struct plat_obs_ops {
    int (*probe) (void *ctx, plat_obs_probe_t  *out);
    int (*start) (void *ctx);
    int (*stop)  (void *ctx);
    int (*health)(void *ctx, plat_obs_health_t *out);
    int (*stats) (void *ctx, plat_obs_health_t *out);
} plat_obs_ops_t;

/* ── Source descriptor ─────────────────────────────────────────────────────
 * The source struct itself must outlive its registration (static storage).
 * The registry stores the pointer, not a copy. */
typedef struct plat_obs_source {
    char                 id[PLAT_OBS_ID_MAX];
    const plat_obs_ops_t *ops;
    void                 *ctx;  /* private context; ops own its lifetime */
} plat_obs_source_t;

/* ── ABI: registration ─────────────────────────────────────────────────────
 * plat_obs_register: add src to the static table.
 *   Returns 0 on success; -1 if table full, id empty, or id collision.
 *   src must have static (or at least persistent) storage.
 * plat_obs_deregister: remove by id; calls stop() if running (best-effort).
 *   Returns 0 on success; -1 if not found.
 */
int plat_obs_register  (plat_obs_source_t *src);
int plat_obs_deregister(const char *id);

/* ── ABI: per-source control (lookup by id, then delegate to ops) ──────────
 * All return -1 when id is not found; out-params left UNAVAILABLE on error.
 */
int plat_obs_probe (const char *id, plat_obs_probe_t  *out);
int plat_obs_start (const char *id);
int plat_obs_stop  (const char *id);
int plat_obs_health(const char *id, plat_obs_health_t *out);
int plat_obs_stats (const char *id, plat_obs_health_t *out);

/* ── Mock source ───────────────────────────────────────────────────────────
 * A bounded synthetic source for unit tests.
 * probe:  always available.
 * start:  resets counters, emits exactly PLAT_OBS_MOCK_FACTS synthetic facts
 *         (counted in received); state → RUNNING.  No file descriptors opened.
 * stop:   state → STOPPED; received unchanged; idempotent.
 * health/stats: reflect current counters.
 *
 * Registered under id "obs.mock.v1".
 */
#define PLAT_OBS_MOCK_FACTS 8u

int plat_obs_mock_register(void);   /* register "obs.mock.v1" */
include/platx/observe_host.h
/* platx/observe_host.h — CLI host header для observe.
 *
 * Включает реальный platx/observe.h (plat_obs_health_t, plat_obs_state_t, ...).
 * Добавляет ТОЛЬКО то, чего нет в реальном API: функцию перечисления источников.
 *
 * plat_obs_list() — слабая заглушка; реальная реализация итерирует
 * внутреннюю статическую таблицу модуля observe.
 */
/* Перечислить зарегистрированные источники наблюдения.
 * ids_out — массив указателей на строки-идентификаторы (static storage).
 * max     — размер массива ids_out.
 * Возвращает число заполненных элементов (≤ PLAT_OBS_SOURCES_MAX).
 * Слабая заглушка возвращает один mock-источник "obs.mock.v1". */
int plat_obs_list(const char **ids_out, int max);

/* CLI entry point.
 * argc/argv начинаются с подкоманды (status|claims|coverage). */
int  plat_obs_cli(int argc, char **argv, FILE *out, FILE *err);

/* Регистрация глагола "observe" в консоли платформы.
 * Вызывается из register_cmds модуля observe. */
void plat_obs_register_console(void);
37

plugin

Упаковка PLUG, проверка доверия, загрузка и жизнь расширения
src/plugin/Исполнение и расширения17 файлов0 API headers

Plugin module управляет package verification, trust, loading, instance lifecycle, command/service publication, reload и unload. PLUG archive — deploy artifact; loader backend является implementation detail под общим descriptor/owner contract.

Граница ответственности

  • Trust verification before active code parsing/loading.
  • Plugin получает ограниченный ABI facade, не global platform pointer/private headers.
  • SMC/patch/trampoline research commands отделены от normal plugin lifecycle.

Устройство подсистемы

  • Manifest v1 содержит identity/version/API range, assets, hashes, signature, requested capabilities/permissions/isolation.
  • Archive parser bounds-checks entries; verifier строит immutable report и trust decision.
  • Loader backend manual ELF или dlopen создаёт plugin instance/owner; adapter строит descriptor/commands/services.
  • Reload side-by-side: start new generation, publish/switch, quiesce/revoke old, unload after leases/tasks.

Поток работы

  • Pack/sign artifact.
  • Parse/hash/signature/policy/ABI verify.
  • Create/load/start → publish commands/services.
  • Reload/unload → revoke/join/resources/unmap.

Отказ и восстановление

  • Current server compile drift plug_arc_t/plug_manifest_t blocks acceptance.
  • Tamper/unknown ABI/forbidden import rejects before execution.
  • Init/start failure rollback loader/resources/registrations.
  • Unload busy due leases/tasks follows deadline policy, no forced dangling pointers.

Основные возможности

  • Signed PLUG archive with Ed25519 trust policy.
  • Internal/manual ELF and explicit dlopen loader paths.
  • Packages MS/DSL/CFG/module/hook assets and example plugins.
  • PIC/trust/builder/embedded integration tests the extension boundary.
Архитектурные детали и инварианты

Overview

The plugin system provides versioned ABI-stable extension points.

Plugins use a major.minor API version; mismatched major versions are refused.

Untrusted plugins run inside MIRAGE sandboxes.

Components

- plugin_api.c — versioned registration (major version check on register)

- plugin_sandbox.c — per-plugin sandbox slot table, 32 slots

- plugin_revoke.c — 64-name revocation list; revoked plugins are blocked at call time

Управление и диагностика

Корневые команды: plugin. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / plugin →
Состав подсистемы / 17 файлов
Файл / компонентНазначение и граница
src/plugin/cmd_plugin.c"plugin" namespace console commands. All sub-commands live under: plugin [args...] plugin list — list all loaded plugins plugin load [type] — load from file (auto/external/internal) plugin unload — unload (call shutdown + free resources)
src/plugin/file_utils.cshared file helpers for the plugin subsystem.
src/plugin/file_utils.hshared file helpers for the plugin subsystem.
src/plugin/plugin.hplugin system for the memfd platform. Loader types (plugin_type_t): PLUGIN_INTERNAL — ELF image mapped by the custom in-memory loader (anonymous mmap, no dlopen, no disk trace). PLUGIN_EXTERNAL — legacy alias for the same in-memory loader. Kept so existing CLI / archive entries keep working.
src/plugin/plugin_alpha_stubs.cweak stubs for plugin_manager symbols not needed in the alpha/agent-ra2c/server profiles (no hot-swap DRM path). Generated for A3-F008 resolution.
src/plugin/plugin_api.cРеализация plugin / api
src/plugin/plugin_archive.cPLUG archive pack / unpack. [plug_entry_t × nfiles] — index, immediately after header [file data] — plugin blobs, sequentially packed Integrity check: SHA-256 over everything after the sha256[] field. Set sha256 to all-zero in the header to skip the check.
src/plugin/plugin_builder.cautomatic plugin compiler and packager. At platform startup, reads a plugin config (plugins.conf or in-memory array), compiles each plugin source with gcc, packs the binaries into a PLUG archive, and calls plugin_load_archive_from_memory() to install all plugins in one shot.
src/plugin/plugin_embedded.cautoload the PLUG archive embedded in the binary. At build time tools/plugpack packs INTERNAL PIC-ELFs into a signed PLUG archive and emits the bytes into embedded_plug.gen.c, which is linked into memfd. At startup plugin_load_embedded() verifies the archive and loads plugins from anonymous memory (no disk trace).
src/plugin/plugin_embedded.hplugins baked into the binary at build time. plugin_embedded_* symbols are defined in embedded_plug.gen.c, which tools/plugpack regenerates during the build. Git keeps an empty stub (blob_len == 0) so the tree still builds without running the packer — just without embedded plugins.
src/plugin/plugin_loader.cexternal (elf_load) and internal (manual ELF) plugin loaders. External loader (v2 — no dlopen for file/memory loading): - From file: read_file() → internal_load_from_memory() - From memory: internal_load_from_memory() directly External symbols are still resolved via dlsym(RTLD_DEFAULT, name)
src/plugin/plugin_manager.cthread-safe plugin registry. Maintains a singly-linked list of loaded plugins protected by a POSIX mutex (PTHREAD_MUTEX_INITIALIZER — no explicit init needed). Default loading uses the in-memory ELF loader in plugin_loader.c. PLUGIN_DLOPEN is the explicit glibc dlopen / memfd+dlopen path.
src/plugin/plugin_pic.cPIC lifecycle guard. STAB-113: Prevent unload/update while inflight CLI/hook callbacks or dependent capabilities exist. Returns EBUSY when active users present.
src/plugin/plugin_revoke.cРеализация plugin / revoke
src/plugin/plugin_sandbox.cРеализация plugin / sandbox
src/plugin/plugin_trust.cUnified trust policy for PLUG/PIC/MSX/DSL artefacts. STAB-111: Single trust-policy entry-point covering algorithm check, signer ID verification, expiry, revocation and audit reason.
src/plugin/plugin_type.cname/suffix → type, type → VFS root. Invariant: unknown name stays PLUGIN_AUTO. The caller (archive mount, plugpack, vfs put) decides the fallback. This file must not pull XIO — plugpack links it as a host tool.
Контракты API / 0 заголовков

Отдельный заголовок из include/platx/ не закреплён за этим доменом в справочнике. Внутренние границы перечислены в составе файлов; управление и публичные связи описаны в соседних подсистемах.

38

poe

Преобразование представления нагрузок и runtime-мосты
src/poe/Исполнение и расширения11 файлов0 API headers

POE предоставляет обфускацию/deobfuscation и research bridges к MSX/XIO/eBPF. Его нужно трактовать как optional transform/tooling, а не как криптографическую или isolation boundary. Industrial path обязан иметь обратимость, versioned format, bounds и полный provenance.

Граница ответственности

  • Security authenticity/confidentiality обеспечивают crypto/signature, не POE.
  • Transforms не выполняют payload и не меняют lifecycle самостоятельно.
  • Anti-debug и advanced bridges доступны только research policy/profile.

Устройство подсистемы

  • Transform descriptor задаёт format/version, parameters, input/output limits, deterministic/random requirements и reverse capability.
  • Pipeline выполняет validate → size plan → transform → optional verify roundtrip → envelope metadata.
  • Configuration immutable generation; reload не меняет in-flight operation.
  • Bridge adapters получают typed buffers/handles через XIO/MSX/eBPF contracts.

Поток работы

  • Input artifact/bytes → policy/format resolve.
  • Bounded transform pipeline.
  • Envelope hash/metadata → output.
  • Optional reverse/verify → audit report.

Отказ и восстановление

  • Malformed envelope/unknown version reject before allocation.
  • Output size overflow/OOM leaves no partial success.
  • Reverse mismatch destroys temporary plaintext and returns integrity error.
  • Bridge unavailable explicit UNAVAILABLE, no shell fallback.

Основные возможности

  • Transforms and restores payload representations.
  • Anti-debug and policy/config surfaces.
  • Bridges MSX, XIO and eBPF-oriented workflows.
Архитектурные детали и инварианты

Overview

SHA-256 hash chain over execution receipts. Every EFFECT receipt must be preceded by CLAIM and DECISION (INV-POE-01). Chain tip is anchored into the audit ring for tamper evidence.

Invariants

INV-POE-01 | Every EFFECT (type=2) must have CLAIM (type=0) + DECISION (type=1) as predecessors in the chain

Receipt Types

- 0 CLAIM — what is being requested

- 1 DECISION — policy/gate outcome

Components

- poe_chain.c — append/verify/tip; embedded SHA-256

- poe_anchor.c — anchor tip into audit ring; INV-POE-01 gate

Управление и диагностика

Корневые команды: poe. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / poe →
Состав подсистемы / 11 файлов
Файл / компонентНазначение и граница
src/poe/poe.hпубличный C API обфускации для DRM, CLI, скриптов и XIO. Голый буфер: poe_obfuscate / poe_deobfuscate не ставят и не снимают poe_file_hdr_t. Конверт — только у CLI-файла. Ошибка — <0 без передачи *out. Неизвестная version файла — отказ.
src/poe/poe_anchor.canchor POE chain tip into audit ring (INV-POE-01)
src/poe/poe_antidebug.cenvironment probes used by POE / DRM. Release (default): full anti-debug set. Developer build: make CFLAGS+='-DDEBUG_BUILD' every probe is a no-op and reports "safe".
src/poe/poe_bridge.hвнутренний C-интерфейс к C++ POEngine. Не включать напрямую: используйте poe.h. Этот заголовок включается только из poe_subsys.c. Экспортирует функции из poe_bridge.cpp (g++, extern "C"). Используемые компоненты: Level 8 — ObfuscatedEncryption: AES-128-ECB + PKCS#7
src/poe/poe_bridge_sys.cprocess I/O for poe_bridge.cpp. C++ must not include xio.h (C11 _Atomic / stdatomic). These wrappers are the only XIO touch from the POEngine bridge.
src/poe/poe_chain.cРеализация poe / chain
src/poe/poe_cli.cCLI-команды POEngine (PLATX Phase 7+). poe obfuscate [--level=] [--seed=] [--key-id=] [--out=] poe deobfuscate [--level=] [--seed=] [--key-id=] [--out=] poe info poe test [--level=] poe config [--level=] [--seed=]
src/poe/poe_ebpf.ceBPF-сенсоры для обнаружения деобфускации POEngine. Регистрирует eBPF-зонды (tracepoints / kprobes) для перехвата попыток анализа или деобфускации защищённых данных из пространства ядра. - Сенсор на sys_ptrace → корреляция с poe_env_is_safe() - Сенсор на sys_process_vm_readv → чтение памяти другого процесса
src/poe/poe_script.cбиндинги POEngine для .ms DSL-скриптов. Регистрирует функции в msx_ctx через msx_register_ns(). Доступен только если msx.h присутствует в дереве сборки. DSL-функции (namespace "poe"): poe.obfuscate(data, level, seed) → obfuscated string poe.deobfuscate(data, level, seed)→ plain string
src/poe/poe_subsys.cреализация публичного API модуля POEngine (PLATX Phase 7+). Реализует функции из poe.h: poe_obfuscate / poe_deobfuscate — применить/снять уровни обфускации poe_is_obfuscated — проверить POE-заголовок в данных poe_recommend_config — рекомендуемая конфигурация
src/poe/poe_xio.cобфусцированное чтение/запись через XIO. Предоставляет poe_xio_read_obf() / poe_xio_write_obf() — прозрачную обёртку, которая на лету де/обфусцирует данные при обращении к файлу или сокету. I/O через xio_get_for("poe", FILE). Нет vtable — ошибка, без libc fallback.
Контракты API / 0 заголовков

Отдельный заголовок из include/platx/ не закреплён за этим доменом в справочнике. Внутренние границы перечислены в составе файлов; управление и публичные связи описаны в соседних подсистемах.

39

proxy

Адаптеры IPC и сетевого посредничества
src/proxy/Связь и ввод-вывод23 файлов0 API headers

Proxy реализует adapters, routes, sessions, filters, auth, TLS, SOCKS/HTTP и traffic tap. Для промышленного качества широкая поверхность должна быть декомпозирована на маленький route/session core и independently testable protocol adapters.

Граница ответственности

  • Proxy не владеет generic transport registry; adapters resolve transport/XIO capabilities.
  • TLS/auth secrets через crypto/keyring, не global config strings.
  • Capture/tap passive и quota-bound; не меняет forwarding policy скрытно.

Устройство подсистемы

  • Proxy instance имеет listeners/adapters, immutable route generation, session table, filter/auth policy и quotas.
  • Adapter interface accept/connect/read/write/close/status поверх XIO.
  • Route compiler validates graph/no loops, target versions и failover; atomic swap generation.
  • Session owner содержит downstream/upstream handles, state, deadlines, counters и cancellation.

Поток работы

  • Ingress accept/message → auth/filter.
  • Route resolve → upstream connect.
  • Bidirectional bounded pumps via XIO.
  • Close/error → cancel both directions → session cleanup/audit.

Отказ и восстановление

  • Half-open/connect timeout closes both sides.
  • Route reload keeps old sessions policy-defined, new generation for new sessions.
  • Backpressure stops reads, no unbounded buffers.
  • TLS/auth failure no fallback plaintext unless explicit separate route.

Основные возможности

  • Unix/TCP/message-queue/shared-memory adapters.
  • Route and forwarding model with filters and sessions.
  • SOCKS/HTTP/TLS/upstream/auth/ACL support.
  • Traffic tap/capture and cipher/key controls.
Архитектурные детали и инварианты

Overview

HTTP/HTTPS intercept, decode, modify, scan, replay, fuzz and report pipeline. Gated by PLATX_HAS_PROXY compile-time flag. Unavailable in MIL mode (§7.32).

Invariants

- §7.31: all proxy functions guarded by #ifdef PLATX_HAS_PROXY

- §7.32: MIL mode flag → proxy_intercept_record() returns -2

Pipeline Modules

A | proxy_intercept.c | Record intercepted URL + metadata

B | proxy_modify.c | In-place header/body modification

C | proxy_replay.c | Store and replay requests

D | proxy_scan.c | Pattern scan for SQLi/XSS/traversal

E | proxy_compare.c | Diff two request buffers

F | proxy_encode.c | URL encode/decode, base64

G | proxy_inject.c | Append/replace body payload

H | proxy_fuzz.c | Built-in + custom fuzzing payloads

I | proxy_report.c | JSON findings report

J | proxy_export.c | POE chain anchor per intercept

Static Dimensions (§SEC-3)

- Replay buffer: 32 × 4096 bytes

- Fuzz payloads: 32 × 256 bytes

Управление и диагностика

Корневые команды: proxy, ipcproxy. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / proxy →
Состав подсистемы / 23 файлов
Файл / компонентНазначение и граница
src/proxy/cmd_proxy.cnamespace "proxy" (alias "ipcproxy"): управление IPC Proxy. proxy start [--unix=PATH] [--tcp=HOST:PORT] | stop | status | adapters proxy add-unix --name=N --path=P [--server] [--auth=T] proxy add-tcp --name=N --host=H --port=P [--server] [--auth=T] proxy add-msgq --name=N [--max-msgs=K]
src/proxy/ipcp.cядро IPC Proxy: IPCP-кадры, адаптеры, маршруты, фильтры, шифрование payload, статистика, receive-queue и poll()-reactor.
src/proxy/ipcp.hIPC Proxy: унифицированный router/bridge IPCP-сообщений между процессами через разные транспорты (UNIX / TCP / POSIX MQ / SHM). Поток (см. схему ТЗ): incoming (UNIX/TCP/MQ/SHM) → IPCP decode → auth → filters → src/dst routing → outgoing adapter. Формат IPCP (бинарный, little-endian):
src/proxy/ipcp_io.hipcp.c's one door for all eight typed I/O ops. Private to this TU. Lives in a header so a door test can include the choke point without pulling in the full proxy stack, keyring, crypto or Non-8 ops — socket, bind, listen, poll, setsockopt, mmap, munmap, ftruncate, unlink, time — are not among the eight and stay on the slot
src/proxy/proxy_auth.cProxy auth/encryption negotiation hardening. STAB-124: Prevent downgrade_check, anti_replay, auth_required, min_cipher.
src/proxy/proxy_compare.ccompare two HTTP requests/responses (module E)
src/proxy/proxy_core.cобщая инфраструктура IPC Proxy (Этап 1). Один poll()-реактор обслуживает ВСЕ слушатели и ВСЕ сессии прокси. Приняв соединение, ядро отдаёт его обработчику протокола (proxy_handler_t), зарегистрированному под именем. Обработчик разбирает рукопожатие и командует
src/proxy/proxy_decode.cРеализация proxy / decode
src/proxy/proxy_encode.cРеализация proxy / encode
src/proxy/proxy_export.cexport proxy intercept record into POE chain (module J)
src/proxy/proxy_forward.cПростейший адаптер поверх обобщённого ядра сессий (proxy_core.c): цель фиксирована правилом, поэтому рукопожатия нет — как только соединение принято, ядро сразу соединяется с target и включает сырой relay. Служит образцом для более сложных обработчиков (socks5/http/tls), которые задают
src/proxy/proxy_fuzz.cfuzz endpoint with generated payloads (module H)
src/proxy/proxy_http.cДва режима, оба поверх обобщённого ядра (proxy_core.c), без своего I/O: • CONNECT host:port — HTTPS-туннель: соединяемся с целью, отвечаем "200 Connection Established" и переключаемся в сырой relay (как SOCKS); определяем цель, переписываем строку запроса в origin-form и форвардим
src/proxy/proxy_inject.cpayload injection into HTTP request body (module G)
src/proxy/proxy_intercept.cHTTP/HTTPS intercept (PLATX_HAS_PROXY; not in MIL mode)
src/proxy/proxy_modify.crequest/response modification (module B)
src/proxy/proxy_replay.crequest replay (module C)
src/proxy/proxy_report.cgenerate text/JSON report of proxy findings (module I)
src/proxy/proxy_scan.cpattern scanning for security indicators (module D)
src/proxy/proxy_socks.cНе рабочий путь (connect → handshake → send/recv → switch → disconnect), Реализует только серверную сторону: разбирает приветствие и запрос клиента, определяет цель и отдаёт её ядру (PROXY_CONNECT). Весь ввод-вывод, connect, relay и захват ведёт ядро (proxy_core.c) — здесь чистый разбор протокола, без
src/proxy/proxy_tap.cРеализация proxy / tap
src/proxy/proxy_tap.hточка наблюдения за ретранслируемым трафиком. relay в ядре несёт готовую полезную нагрузку прикладного уровня (для TLS — уже расшифрованную). Чтобы её можно было и открыть в Wireshark, и прогнать через штатный анализатор потоков txpstack (nl_flow_*), tap СИНТЕЗИРУЕТ вокруг
src/proxy/proxy_tls.cПерехват TLS: терминируем соединение клиента, предъявляя ПОДМЕННЫЙ сертификат для запрошенного имени (SNI), подписанный нашим корневым CA; параллельно сами устанавливаем TLS к настоящей цели. Между двумя TLS-сессиями — расшифрованный ЗАВИСИМОСТЬ ОТ OpenSSL — ТОЛЬКО В РАНТАЙМЕ. Бинарь не линкуется с libssl/
Контракты API / 0 заголовков

Отдельный заголовок из include/platx/ не закреплён за этим доменом в справочнике. Внутренние границы перечислены в составе файлов; управление и публичные связи описаны в соседних подсистемах.

40

pxapi

Совместимость публичного API и проекция записей платформы
src/pxapi/Управление и контракты3 файлов1 API headers

PXAPI предоставляет внешний контракт поверх существующих механизмов PLATX. Адаптер сохраняет семантику исходного владельца данных и не создаёт параллельный управляющий контур. Контракт версии, размер структуры и отказ при несовместимости являются частью интерфейса.

Граница ответственности

  • PXAPI предоставляет внешний контракт поверх существующих механизмов PLATX. Адаптер сохраняет семантику исходного владельца данных и не создаёт параллельный управляющий контур. Контракт версии, размер структуры и отказ при несовместимости являются частью интерфейса.

Устройство подсистемы

  • backward-compat shim: legacy audit calls → PLATX API v1 records. API0 callers used platx_audit_event(type_code, payload, len) directly. This shim wraps those calls in a PX_SCHEMA_RECEIPT envelope so they flow through the v1 pipeline without modifying legacy call-sites.
  • PLATX public API v1 implementation. static BSS wire ring. pxapi_v1_drain() allocates — it is a low-freq export call, not a hot path.
  • PXAPI Core v1: конверт записи и канонический кодек. Кодек делает ровно три вещи, которых нет у ad-hoc структур из инвентаря API0: фиксирует порядок полей, различает обязательное и опциональное и сохраняет чужое неизвестное опциональное поле при пересылке.

Управление и диагностика

Домен не регистрирует собственную корневую команду. Его контракт используется вызывающими подсистемами через API и интерфейсы core, fabric.

Справочник CLI / pxapi →
Состав подсистемы / 3 файлов
Файл / компонентНазначение и граница
src/pxapi/platx_api_compat.cbackward-compat shim: legacy audit calls → PLATX API v1 records. API0 callers used platx_audit_event(type_code, payload, len) directly. This shim wraps those calls in a PX_SCHEMA_RECEIPT envelope so they flow through the v1 pipeline without modifying legacy call-sites.
src/pxapi/platx_api_v1.cPLATX public API v1 implementation. static BSS wire ring. pxapi_v1_drain() allocates — it is a low-freq export call, not a hot path.
src/pxapi/pxapi_core.cPXAPI Core v1: конверт записи и канонический кодек. Кодек делает ровно три вещи, которых нет у ad-hoc структур из инвентаря API0: фиксирует порядок полей, различает обязательное и опциональное и сохраняет чужое неизвестное опциональное поле при пересылке.
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/pxapi_core.h
/* platx/pxapi_core.h — PXAPI Core v1: общий конверт записи (карточка API1).
 *
 * Инвентарь API0 намерил в дереве 42 разных типа «действия» и 33 «вердикта».
 * Это не богатство словаря, а его отсутствие: ни у одного из них нет общего
 * заголовка, поэтому запись нельзя ни версионировать, ни переслать на другую
 * ОС, ни отличить свежую от протухшей, не зная заранее, чья она.
 *
 * Этот заголовок добавляет не сорок третий тип, а конверт, в котором все
 * остальные начинают быть сравнимыми:
 *
 *   заголовок записи   schema_id, версия, размер, время, поколение источника
 *   principal          кто действовал, от чьего имени, кто одобрил, кто применил
 *   resource ref       чем является предмет, а не как он сейчас называется
 *   epistemic          наблюдали, вывели, предположили или не смотрели вовсе
 *   quality            насколько точно и насколько независимо
 *   taint              откуда пришло значение и снимал ли кто-то с него метку
 *   result             восемь ответов «что теперь делать» плюс числовой код
 *
 * ЧЕГО ЗДЕСЬ НЕТ, И ЭТО НАМЕРЕННО
 * ───────────────────────────────
 * Нового словаря причин. `plat_reason_t` из platx/reason.h уже канон: восемь
 * ответов, отображения из чужих кодов и проверяемое свойство «имеет ли смысл
 * повторять». Второй словарь означал бы, что оператор видит то один ответ, то
 * другой, в зависимости от того, чей код вернул отказ.
 *
 * Второго канонического кодека. Правила канонизации TLV уже описаны в
 * sense_schema.h и исполняются sense_tlv.c; здесь та же модель, применённая к
 * записям Core.
 *
 * ЗАМОРОЗКА
 * ─────────
 * PXAPI_CORE_ABI 1. Размер каждой структуры закреплён _Static_assert: поле,
 * вставленное в середину, ломает сборку, а не разбор у потребителя. Это
 * единственный ABI-freeze gate Core v1 (`pxapi-core-contract0`).
 */

/* Заморозка раскладки должна работать и у потребителя на C++: _Static_assert —
 * ключевое слово C11, в C++ его нет.  Проверка одна и та же, слово разное. */
#define PX_FROZEN(cond, msg) static_assert(cond, msg)
#define PX_FROZEN(cond, msg) _Static_assert(cond, msg)

#define PXAPI_CORE_ABI        1u
#define PXAPI_CORE_SCHEMA_MAJOR 1u
#define PXAPI_CORE_SCHEMA_MINOR 0u

#define PX_ID_MAX     64u
#define PX_HINT_MAX  128u
#define PX_TEXT_MAX  160u

/* ── Пространства schema_id: firewall между Core и профилями ──────────────
 * schema_id — стабильное число, а НЕ хеш имени: хеш меняется от переименования
 * поля, и запись, которую никто не менял, становится чужой.
 *
 * Верхние 16 бит — профиль. Defensive-сборка отказывается разбирать запись
 * Offensive-пространства и наоборот: это тот самый capability firewall между
 * Core и профилями, но исполняемый кодеком, а не описанный в документе.
 */
#define PX_NS_CORE       0x0000u
#define PX_NS_DEFENSIVE  0x0001u
#define PX_NS_OFFENSIVE  0x0002u
#define PX_NS_LAB        0x00FFu

#define PX_SCHEMA(ns, id)   (((uint32_t)(ns) << 16) | (uint32_t)(id))
#define PX_SCHEMA_NS(sid)   ((uint16_t)((sid) >> 16))
#define PX_SCHEMA_LOCAL(sid) ((uint16_t)((sid) & 0xFFFFu))

/* Записи Core v1. Номера выданы один раз и не переиспользуются. */
#define PX_SCHEMA_RESULT      PX_SCHEMA(PX_NS_CORE, 0x0001)
#define PX_SCHEMA_OBSERVATION PX_SCHEMA(PX_NS_CORE, 0x0002)
#define PX_SCHEMA_CLAIM       PX_SCHEMA(PX_NS_CORE, 0x0003)
#define PX_SCHEMA_INTENT      PX_SCHEMA(PX_NS_CORE, 0x0004)
#define PX_SCHEMA_RECEIPT     PX_SCHEMA(PX_NS_CORE, 0x0005)

/* ── Заголовок записи (§6.1) ─────────────────────────────────────────────── */
#define PX_HDR_F_SYNTHETIC     (1u << 0)  /* запись из симуляции, не из мира */
#define PX_HDR_F_REDACTED      (1u << 1)
#define PX_HDR_F_REPLAY        (1u << 2)  /* повтор ранее записанного */
#define PX_HDR_F_WALL_UNSURE   (1u << 3)  /* стенным часам верить нельзя */
/* Вывод разбора, а не намерение производителя: устанавливается px_decode()
 * при сохранении неизвестных опциональных полей и НИКОГДА не попадает на
 * провод — px_encode() его снимает.  Благодаря этому px_relay() отдаёт байты
 * ровно те же, что пришли, и подпись над заголовком переживает пересылку. */
#define PX_HDR_F_HAS_EXTENSION (1u << 4)  /* несёт неизвестные опциональные TLV */

typedef struct px_record_hdr_v1 {
    uint32_t schema_id;
    uint16_t schema_major;      /* меняется при несовместимой семантике */
    uint16_t schema_minor;
    uint32_t struct_size;       /* tail-совместимость локального ABI */
    uint32_t flags;
    uint64_t record_id_hi;
    uint64_t record_id_lo;
    uint64_t created_mono_ns;   /* CLOCK_MONOTONIC — единственная основа свежести */
    uint64_t created_wall_ms;   /* для человека; сам по себе не доказывает свежесть */
    uint64_t producer_generation; /* 0 запрещено для живого производителя */
} px_record_hdr_v1_t;

PX_FROZEN(sizeof(px_record_hdr_v1_t) == 56, "px_record_hdr_v1_t заморожен");

/* ── Principal (§6.2) ────────────────────────────────────────────────────── */
typedef enum px_principal_kind_v1 {
    PX_P_NONE = 0,
    PX_P_HUMAN = 1,
    PX_P_SERVICE,
    PX_P_MODULE,
    PX_P_CHILD,
    PX_P_NODE,
    PX_P_CENTRAL,
    PX_P_MSX_SCRIPT,
    PX_P_DSL_MANIFEST,
    PX_P_AI_AGENT,
    PX_P_POLICY_ENGINE,
    PX_P_ACTION_PROVIDER,
    PX_P_WITNESS,
    PX_P_MAX
} px_principal_kind_v1_t;

typedef struct px_principal_v1 {
    uint32_t kind;                    /* px_principal_kind_v1_t */
    uint32_t _pad;
    uint64_t generation;              /* 0 отвергается для живой записи */
    char     trust_domain[PX_ID_MAX];
    char     stable_id[PX_ID_MAX];
} px_principal_v1_t;

PX_FROZEN(sizeof(px_principal_v1_t) == 144, "px_principal_v1_t заморожен");

/* Цепочка. Одной строкой `user` она не заменяется: «кто нажал», «от чьего
 * имени», «кто одобрил» и «кто применил» — четыре разных ответственности, и
 * расследование начинается ровно с того, что они разошлись. */
typedef struct px_actor_chain_v1 {
    px_principal_v1_t actor;         /* непосредственный инициатор */
    px_principal_v1_t on_behalf_of;  /* делегировавший; kind NONE = никто */
    px_principal_v1_t approver;
    px_principal_v1_t executor;      /* кто применил эффект */
    px_principal_v1_t witness;       /* кто подтвердил */
} px_actor_chain_v1_t;

PX_FROZEN(sizeof(px_actor_chain_v1_t) == 720, "px_actor_chain_v1_t заморожен");

/* ── Resource reference (§6.3) ───────────────────────────────────────────── */
typedef enum px_resource_kind_v1 {
    PX_RES_NONE = 0,
    PX_RES_PROCESS = 1,
    PX_RES_FILE,
    PX_RES_SOCKET,
    PX_RES_MODULE,
    PX_RES_PAGE,
    PX_RES_SERVICE,
    PX_RES_NODE,
    PX_RES_SESSION,
    PX_RES_MAX
} px_resource_kind_v1_t;

/* key_hi/key_lo — стабильный ключ предмета (для процесса: свёртка
 * boot:pidns:pid:starttime:exec_gen). Путь, PID, имя службы и handle живут в
 * `hint` и идентичностью не являются: они меняются, переиспользуются и
 * подделываются, а расследование обязано пережить всё три. */
typedef struct px_resource_ref_v1 {
    uint32_t kind;
    uint32_t _pad;
    uint64_t node_id;
    uint64_t boot_id;
    uint64_t generation;
    uint64_t key_hi;
    uint64_t key_lo;
    char     hint[PX_HINT_MAX];
} px_resource_ref_v1_t;

PX_FROZEN(sizeof(px_resource_ref_v1_t) == 176, "px_resource_ref_v1_t заморожен");

/* ── Epistemic state (§6.4) ──────────────────────────────────────────────── */
typedef enum px_epistemic_v1 {
    PX_E_NONE = 0,
    PX_E_OBSERVED = 1,
    PX_E_DERIVED,
    PX_E_HYPOTHESIS,
    PX_E_PROPOSED,
    PX_E_ASSUMED,
    PX_E_CONFLICTED,
    PX_E_UNOBSERVED,
    PX_E_UNOBSERVABLE,
    PX_E_SYNTHETIC,
    PX_E_MAX
} px_epistemic_v1_t;

/* Запрещённые переходы (§6.4). Проверяются функцией, а не соглашением:
 * PROPOSED→OBSERVED без нового наблюдения, HYPOTHESIS→любое «проверено» без
 * свидетеля, SYNTHETIC→реальное доказательство, UNOBSERVED→«ложь». */
int px_epistemic_transition_allowed(px_epistemic_v1_t from,
                                    px_epistemic_v1_t to,
                                    int have_new_observation,
                                    int have_witness);

/* ── Quality и independence (§6.5) ───────────────────────────────────────── */
typedef enum px_quality_level_v1 {
    PX_Q_UNAVAILABLE = 0,
    PX_Q_PARTIAL     = 1,
    PX_Q_COMPATIBLE  = 2,
    PX_Q_EXACT       = 3,
    PX_Q_MAX
} px_quality_level_v1_t;

typedef struct px_quality_v1 {
    uint32_t level;                  /* px_quality_level_v1_t */
    uint32_t proof_class;
    uint32_t independence_group;     /* совпал — не два подтверждения, а одно */
    uint32_t attacker_controllable;  /* маска классов полей под влиянием субъекта */
    uint64_t freshness_ns;           /* интервал, в котором утверждение верно */
    uint64_t coverage_epoch;
    uint32_t privacy_class;
    uint32_t lineage_steps;          /* сколько преобразований прошло значение */
} px_quality_v1_t;

PX_FROZEN(sizeof(px_quality_v1_t) == 40, "px_quality_v1_t заморожен");

/* ── Taint (§6.6) ────────────────────────────────────────────────────────── */
#define PX_TAINT_TRUSTED_PLATFORM   (1u << 0)
#define PX_TAINT_AUTHENTICATED_PEER (1u << 1)
#define PX_TAINT_ATTACKER_CTRL      (1u << 2)
#define PX_TAINT_MODEL_DERIVED      (1u << 3)
#define PX_TAINT_SYNTHETIC          (1u << 4)
#define PX_TAINT_REDACTED           (1u << 5)
#define PX_TAINT_UNKNOWN_ORIGIN     (1u << 6)

typedef struct px_taint_v1 {
    uint32_t classes;        /* PX_TAINT_* */
    uint32_t sanitizer_id;   /* 0 = метка не снималась */
    uint64_t lineage_hash;   /* свёртка цепочки преобразований */
} px_taint_v1_t;

PX_FROZEN(sizeof(px_taint_v1_t) == 16, "px_taint_v1_t заморожен");

/* Метка наследуется транзитивно: результат несёт всё, чем были помечены
 * входы. Снять её может только названный санитайзер, и снятие записывается в
 * lineage — иначе «очищено» невозможно отличить от «забыли пометить». */
px_taint_v1_t px_taint_merge(px_taint_v1_t a, px_taint_v1_t b);
int px_taint_sanitize(px_taint_v1_t *t, uint32_t sanitizer_id,
                      uint32_t clears_classes);

/* ── Result (§23) ────────────────────────────────────────────────────────── */
typedef enum px_domain_code_v1 {
    PX_OK = 0,
    PX_BAD_SCHEMA = 1,
    PX_VERSION_UNSUPPORTED,
    PX_NONCANONICAL,
    PX_SIGNATURE_INVALID,
    PX_PROVENANCE_UNTRUSTED,
    PX_IDENTITY_INCOMPLETE,
    PX_TARGET_STALE,
    PX_PROVIDER_STALE,
    PX_POLICY_STALE,
    PX_LEASE_EXPIRED,
    PX_BUDGET_EXCEEDED,
    PX_GAP_INTERSECTS_WINDOW,
    PX_EVIDENCE_INSUFFICIENT,
    PX_EVIDENCE_CONFLICTED,
    PX_TAINT_UNSANITIZED,
    PX_ACTION_UNSUPPORTED,
    PX_SEMANTIC_DOWNGRADE,
    PX_APPROVAL_REQUIRED,
    PX_OBLIGATION_UNSATISFIED,
    PX_PREPARE_FAILED,
    PX_COMMIT_UNKNOWN_OUTCOME,
    PX_VERIFY_FAILED,
    PX_PARTIAL,
    PX_COMPENSATION_FAILED,
    PX_UNCOMPENSATED,
    PX_REPLAY_DETECTED,
    PX_SESSION_STALE,
    PX_PROFILE_FOREIGN,      /* запись чужого профильного пространства */
    PX_DOMAIN_MAX
} px_domain_code_v1_t;

typedef struct px_result_v1 {
    uint32_t reason_class;   /* plat_reason_t */
    uint32_t domain_code;    /* px_domain_code_v1_t */
    uint32_t flags;
    uint32_t _pad;
    uint64_t correlation_id;
    char     detail[PX_TEXT_MAX];
    char     next_action[PX_TEXT_MAX];
} px_result_v1_t;

PX_FROZEN(sizeof(px_result_v1_t) == 344, "px_result_v1_t заморожен");

/* Человеческий текст в program logic не участвует и может локализоваться;
 * решения принимаются по reason_class и domain_code. Неизвестный код никогда
 * не отображается в OK — это отдельная проверка, а не соглашение. */
int px_result_set(px_result_v1_t *r, plat_reason_t cls, uint32_t domain,
                  uint64_t corr, const char *detail);
const char *px_domain_code_name(uint32_t code);

/* Отображение типизированной недоступности фабрики провайдеров в канонический
 * словарь. Существует потому, что иначе платформа имеет два способа сказать
 * «недоступно» — находка инвентаря API0. */
plat_reason_t px_reason_from_prov_unavail(uint32_t prov_unavail);
uint32_t      px_domain_from_prov_unavail(uint32_t prov_unavail);

/* ── Заголовок: заполнение и проверка ────────────────────────────────────── */
int px_hdr_init(px_record_hdr_v1_t *h, uint32_t schema_id,
                uint64_t producer_generation);

/* Проверяет то, что нельзя починить у потребителя: версию, размер, поколение,
 * принадлежность пространству. `profile_ns` — пространство, которое эта сборка
 * имеет право принимать (PX_NS_CORE принимается всегда). */
int px_hdr_check(const px_record_hdr_v1_t *h, uint16_t profile_ns,
                 px_result_v1_t *out);

/* ── Канонический кодек ──────────────────────────────────────────────────
 * Правила те же, что у SENSE, и по той же причине: две канонизации одного и
 * того же — это две подписи под разными байтами.
 *
 *   - поля идут в возрастающем порядке типа; повтор обязательного поля — отказ;
 *   - обязательные поля помечены старшим битом типа;
 *   - неизвестное ОБЯЗАТЕЛЬНОЕ поле → UNSUPPORTED (запись не понята);
 *   - неизвестное опциональное поле сохраняется байт в байт при пересылке,
 *     и запись помечается PX_HDR_F_HAS_EXTENSION.
 */
#define PX_TLV_REQ           0x8000u
#define PX_TLV_HDR_BYTES     4u
#define PX_RECORD_MAX     65520u

/* Обязательные поля Core v1 */
#define PX_T_HDR         (PX_TLV_REQ | 0x0001u)
#define PX_T_ACTOR       (PX_TLV_REQ | 0x0002u)
#define PX_T_RESOURCE    (PX_TLV_REQ | 0x0003u)
#define PX_T_EPISTEMIC   (PX_TLV_REQ | 0x0004u)
/* Опциональные */
#define PX_T_QUALITY     0x0010u
#define PX_T_TAINT       0x0011u
#define PX_T_RESULT      0x0012u
#define PX_T_PAYLOAD     0x0013u

typedef struct px_record_v1 {
    px_record_hdr_v1_t  hdr;
    px_actor_chain_v1_t actors;
    px_resource_ref_v1_t resource;
    uint32_t            epistemic;      /* px_epistemic_v1_t */
    uint32_t            have;           /* маска PX_HAVE_* */
    px_quality_v1_t     quality;
    px_taint_v1_t       taint;
    px_result_v1_t      result;

    const uint8_t      *payload;        /* не копируется кодеком */
    uint32_t            payload_len;

    /* Неизвестные опциональные TLV, сохранённые для пересылки байт в байт. */
    const uint8_t      *ext;
    uint32_t            ext_len;
} px_record_v1_t;

#define PX_HAVE_QUALITY (1u << 0)
#define PX_HAVE_TAINT   (1u << 1)
#define PX_HAVE_RESULT  (1u << 2)
#define PX_HAVE_PAYLOAD (1u << 3)
#define PX_HAVE_EXT     (1u << 4)

/* 0 при успехе; иначе домашний код в `err` и отрицательный возврат. */
int px_encode(const px_record_v1_t *rec, uint8_t *buf, size_t bufsz,
              size_t *written, px_result_v1_t *err);
int px_decode(const uint8_t *wire, size_t len, uint16_t profile_ns,
              px_record_v1_t *out, px_result_v1_t *err);

/* Пересылка: перекодировать разобранную запись, сохранив неизвестные
 * опциональные поля. Ретранслятор, теряющий чужие поля, тихо превращает
 * запись в другую. */
int px_relay(const px_record_v1_t *in, uint8_t *buf, size_t bufsz,
             size_t *written, px_result_v1_t *err);
41

pxsig

Происхождение, доверие и поиск сигнатурных признаков
src/pxsig/Наблюдение и исследование13 файлов4 API headers

PXSIG отделяет доставку данных, оценку их происхождения и сигнатурное совпадение. Срок, время, источник и контекст набора участвуют в обработке. Признак остаётся признаком: домен не превращает совпадение в универсальный вердикт и не получает право на действие из одной находки.

Граница ответственности

  • PXSIG отделяет доставку данных, оценку их происхождения и сигнатурное совпадение. Срок, время, источник и контекст набора участвуют в обработке. Признак остаётся признаком: домен не превращает совпадение в универсальный вердикт и не получает право на действие из одной находки.

Устройство подсистемы

  • адаптер CLI домена к консоли платформы. Здесь НЕТ логики команд: она в sig_cli.c и печатает в FILE*, чтобы её можно было прогнать без консоли. Две реализации одной команды рано или поздно начинают отвечать по-разному, и разойдутся они молча. Вывод собирается во временный поток в памяти и отдаётся консоли одним
  • дескриптор модуля PXSIG по MODULE_CONTRACT. Что здесь намеренно НЕТ и почему: - malloc/calloc: экземпляр один (PLAT_MOD_FLAG_SINGLETON) и статический; память узла (каталог, индексы, два поколения снимков) выделена статически в sig_host.c. Гейт «ноль динамической памяти» домена распространяется и
  • автономный CLI домена. Существует потому, что src/pxsig в поставляемый профиль НЕ входит: глагол `pxsig` внутри platx появится только когда владелец внесёт домен в mk/source-manifests.mk. До тех пор CLI обязан быть запускаемым и проверяемым — иначе «CLI есть» было бы утверждением без исполнения.
  • каталог правил: идентичность, слияние, отзыв, учёт. ЧТО ЗДЕСЬ РЕШАЕТСЯ, кроме хранения. 1. СЛИЯНИЕ НЕ ТЕРЯЕТ ИСТОЧНИК (PXSIG-02). Один и тот же индикатор, пришедший из двух feed'ов, — это ОДНА запись и ДВА происхождения. Замена происхождения при слиянии превращает «знали из двух мест» в

Управление и диагностика

Корневые команды: pxsig. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / pxsig →
Состав подсистемы / 13 файлов
Файл / компонентНазначение и граница
src/pxsig/cmd_pxsig.cадаптер CLI домена к консоли платформы. Здесь НЕТ логики команд: она в sig_cli.c и печатает в FILE*, чтобы её можно было прогнать без консоли. Две реализации одной команды рано или поздно начинают отвечать по-разному, и разойдутся они молча. Вывод собирается во временный поток в памяти и отдаётся консоли одним
src/pxsig/pxsig_module.cдескриптор модуля PXSIG по MODULE_CONTRACT. Что здесь намеренно НЕТ и почему: - malloc/calloc: экземпляр один (PLAT_MOD_FLAG_SINGLETON) и статический; память узла (каталог, индексы, два поколения снимков) выделена статически в sig_host.c. Гейт «ноль динамической памяти» домена распространяется и
src/pxsig/pxsigctl_main.cавтономный CLI домена. Существует потому, что src/pxsig в поставляемый профиль НЕ входит: глагол `pxsig` внутри platx появится только когда владелец внесёт домен в mk/source-manifests.mk. До тех пор CLI обязан быть запускаемым и проверяемым — иначе «CLI есть» было бы утверждением без исполнения.
src/pxsig/sig_catalog.cкаталог правил: идентичность, слияние, отзыв, учёт. ЧТО ЗДЕСЬ РЕШАЕТСЯ, кроме хранения. 1. СЛИЯНИЕ НЕ ТЕРЯЕТ ИСТОЧНИК (PXSIG-02). Один и тот же индикатор, пришедший из двух feed'ов, — это ОДНА запись и ДВА происхождения. Замена происхождения при слиянии превращает «знали из двух мест» в
src/pxsig/sig_cli.cФайловый ввод-вывод живёт ЗДЕСЬ, а не в ядре домена: контракт разбора работает над буфером, и это проверяется гейтом `t_pxsig_no_io.sh` по списку неразрешённых символов ядровых объектников. Если fopen однажды появится в `sig_wire.c`, гейт упадёт — так и задумано.
src/pxsig/sig_consumer.cсовместимость, самопроверка, активация и статус. Здесь заканчивается путь «доставлено → работает», и здесь же его можно оборвать четырьмя разными способами. Все четыре названы отдельно: resolve — пакет не для этого потребителя; self-test — правила собрались, но не срабатывают на своих же векторах;
src/pxsig/sig_host.cсостояние узла PXSIG и приём объектов. Это АДАПТЕР, а не ядро: он держит статическую память узла и сводит вместе проверку, каталог, снимки и активацию. Ядро (`sig_wire`, `sig_verify`, `sig_catalog`, `sig_snapshot`, `sig_consumer`, `sig_receipt`) остаётся без
src/pxsig/sig_msx.cnamespace `pxsig` для скриптового движка MSX. НАПРАВЛЕНИЕ ЗАВИСИМОСТИ. Этот файл принадлежит PXSIG и включает внутренний заголовок MSX, а не наоборот. MSX ничего не знает про PXSIG и не линкуется с ним: домен, не входящий в поставку, не имеет права тянуть за собой
src/pxsig/sig_receipt.cканонический activation receipt. Receipt отвечает на вопрос «что именно сейчас работает и почему остальное не работает». Поэтому в нём рядом стоят пять чисел — installed, active, disabled, expired, unsupported — и они НЕ сводятся к одному «правил N». Свод к одному числу и есть тот способ, которым «база актуальна» начинает
src/pxsig/sig_snapshot.coverlay владельца, проекции для потребителей, снимок. Снимок — это ответ на вопрос «какие правила работали в тот момент». Поэтому он неизменяем, несёт поколение и digest, и его нельзя освободить из-под читателя: находка ссылается на поколение, и если память под ним
src/pxsig/sig_transport.cприём объектов извне и публикация статуса. ЧТО ЭТОТ ФАЙЛ УТВЕРЖДАЕТ, И ЧЕГО ОН НЕ ДЕЛАЕТ. Утверждает: транспорт не даёт полномочий. Объект, пришедший от Central или от соседа по mesh, попадает в ту же функцию `pxsig_host_ingest`, что и файл с диска, и проходит те же проверки подписи, роли, порога, эпохи,
src/pxsig/sig_verify.cпроверка пакета: доверие, время, область, откат. Порядок проверок выбран так, чтобы более дешёвая и более определённая причина отказа называлась первой. Владельцу важно знать, что именно не так: «ключ отозван» и «подпись не сошлась» требуют разных действий, а
src/pxsig/sig_wire.cканонический формат пакета ThreatMod: разбор и сборка. Это единственное место, где недоверенные байты превращаются в структуры, поэтому здесь действуют более жёсткие правила, чем в остальном домене. 1. НЕОДНОЗНАЧНОСТЬ ОТВЕРГАЕТСЯ. Одному содержимому соответствует ровно
Контракты API / 4 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/invariants.h
/* platx/invariants.h — constitution. Not optional style. Not TZ.
 *
 * 1. Core never depends on a domain module.
 * 2. Modules never call each other via implementation symbols.
 * 3. Cross-module work goes only through versioned capabilities
 *    from the Core registry (abi->require / provide / revoke).
 * 4. plat_abi_t holds Core primitives only. Domain APIs are capabilities.
 * 5. One module descriptor. Isolation (EMBEDDED|CHILD) is a deploy policy.
 * 6. CHILD sees the same plat_abi via an IPC proxy; provides[] are
 *    registered as worker-side proxy vtables.
 * 7. Stub does not know modules. Child sandbox is applied by Core child-host.
 * 8. Recovery is the only decide (RESTART / FAIL). Lifecycle is the only
 *    state changer. leftover / CLI / event do not decide.
 * 9. Resource owner is (module_id, instance_id, generation), not a string.
 * 10. EMBEDDED threads are created only via abi->task->spawn.
 * 11. plat_event is Core fan-out: "X happened", not "do Y". Bounded, typed.
 *     Unsubscribe waits that subscriber's foreign inflight (not a single
 *     global emitter). A callback may free ud only after unsubscribe returns.
 *     Not a bus, not JSON, not persistent, not audit, not Event Hub,
 *     not a 5th registry. Finish src/core/plat_event.c. Do not spawn a
 *     second event stack. mbus stays; bridge is event→mbus only, not
 *     the reverse by default. Do not invent a new plat_owner_t for events;
 *     name + generation is enough until proven separately.
 * 12. CI rejects module imports of src/ and of platform_*, subsys_*, taskmgr_*.
 * 13. Module/capability identity is a logical name (storage.* / netstack.* /
 *     transport.* / protocol.* / test.*). CLI verbs are not identities.
 * 14. Hades is a domain module. Core never depends on it.
 * 15. Supervisor is a picture, not an executor: no start()/stop().
 *     An event callback does not restart a process, does not take a core
 *     lock, and does not call apply / lifecycle start.
 * 16. Four registries. Do not add a fifth. Collapse is v5, not now.
 * 17. XIO executes I/O. Provider executes, Policy chooses, Hook
 *     observes or limits, XIM mediates CHILD. None of them decide
 *     lifecycle or recovery. src/xio is not ra2c_xio.
 * 18. Not every Linux syscall is an XIO operation. xio_t is I/O
 *     execution, not a libc dump. New ops need a caller in src/,
 *     not another function pointer for its own sake.
 *
 * Path (one way, fail closed):
 *
 *   fact → plat_event (core, bounded, typed)
 *     ├── log / audit
 *     ├── supervisor (picture, not executor)
 *     └── recovery (only decide: RESTART / FAIL)
 *           → lifecycle (only mutates state)
 *             → module/child START = spawn+IPC+sandbox+handshake+caps
 *
 * Events/Supervisor — ship plan (not an alpha gate):
 *
 *   0. Do not break ALPHA_GATE, one-brain, leftover empty, child PID2/budget.
 *      An event queue is not a ready condition.
 *   1. Bounded ring. emit does not block lifecycle. overflow = drop+counter.
 *      Types only from the working path: MODULE_FAILED / MODULE_RUNNING,
 *      RECOVERY_RESTART / RECOVERY_BUDGET_EXHAUSTED, TASK_EXITED / child
 *      death, PLUGIN_LOADED (hook → emit).
 *   2. Two subscribers: logger and supervisor aggregate. Recovery does not
 *      subscribe to events in order to decide restart (already watch/snapshot).
 *   3. leftover: sensor emit, not apply. Do not bring back a local budget
 *      or leftover backoff.
 *   4. mbus bridge + CLI "supervisor events" only after loopback:
 *      crash → RECOVERY_RESTART → PID2 with a new generation;
 *      budget=1 + second crash → EXHAUSTED + FAILED, no new PID.
 *
 *   Later, not step 1: emit_sync PRE_STOP; supervisor restart/history/trace
 *   CLI; correlation / parent_event_id; filters / unsubscribe_owner after
 *   6–8 live types and two subscribers.
 *
 *   Hard: one emit per fact; subscribe carries generation (DESTROY →
 *   callbacks are dead); callback does not take a core lock and does not
 *   call apply / lifecycle start.
 *
 * How not to write:
 *   Event Hub · 5th registry · leftover/CLI/event decide · supervisor
 *   start()/stop() · callback restarts a process · "do Y" instead of
 *   "X happened" · replace mbus · plat_owner_t invented for events ·
 *   emit_sync PRE_STOP in step 1 · Event-queue as an alpha / ready gate.
 */
#define PLATX_CORE_ABI_MAJOR  1u
#define PLATX_CORE_ABI_MINOR  0u
#define PLATX_CORE_ABI        (((uint32_t)PLATX_CORE_ABI_MAJOR << 16) | \
                               (uint32_t)PLATX_CORE_ABI_MINOR)
include/platx/pxsig_host.h
/* platx/pxsig_host.h — узел PXSIG: состояние домена, приём объектов, CLI.
 *
 * РАЗДЕЛЕНИЕ, РАДИ КОТОРОГО ЭТОТ ЗАГОЛОВОК ОТДЕЛЬНЫЙ.
 * `pxsig_v1.h` и `pxsig_node_v1.h` — чистые контракты: ноль I/O, ноль malloc,
 * ноль обращений к часам. Всё, что читает файлы, печатает, принимает объекты
 * из транспорта и держит singleton узла, живёт ЗДЕСЬ и в адаптерах
 * `src/pxsig/{sig_cli,sig_transport,sig_msx,pxsig_module}.c`.
 *
 * Граница не декоративная: она проверяется гейтом `t_pxsig_no_io.sh`, который
 * смотрит неразрешённые символы ядровых объектников. Если однажды в
 * `sig_wire.c` появится fopen, гейт упадёт — так и задумано.
 */
/* Ёмкости узла. Заданы здесь, а не в ядре: это решение о РАЗМЕРЕ УЗЛА,
 * а не о формате. Память статическая — домен обещал не выделять. */
#define PXSIG_HOST_MAX_IOCS      4096u
#define PXSIG_HOST_MAX_RULES     2048u
#define PXSIG_HOST_MAX_TOMBS      256u
#define PXSIG_HOST_IOC_INDEX    16384u   /* >= 2*MAX_IOCS, степень двойки  */
#define PXSIG_HOST_RULE_INDEX    8192u
#define PXSIG_HOST_PACK_BYTES  (4u * 1024u * 1024u)

/* Откуда пришёл объект. ЭТО ТОЛЬКО ПРОВЕНАНС. Ни одно значение не даёт
 * полномочий и не смягчает ни одной проверки: пакет из mesh проверяется
 * ровно так же, как пакет с диска. Иначе «доверенный сосед» становился бы
 * способом обойти подпись. */
typedef enum pxsig_source {
    PXSIG_SRC_LOCAL_FILE = 1,
    PXSIG_SRC_CENTRAL    = 2,
    PXSIG_SRC_MESH_PEER  = 3,
    PXSIG_SRC_OPERATOR   = 4
} pxsig_source_t;

const char *pxsig_source_name(pxsig_source_t s);

/* Итог приёма одного объекта. */
typedef struct pxsig_ingest_result {
    uint8_t         accepted;
    pxsig_reason_t  reason;
    uint64_t        sequence;
    uint8_t         content_digest[PXSIG_DIGEST_LEN];
    uint32_t        records_applied;
    pxsig_source_t  source;
    char            detail[PXSIG_REASON_MAX];
} pxsig_ingest_result_t;

/* Узел. Один на процесс (singleton дескриптора модуля). */
typedef struct pxsig_host pxsig_host_t;

pxsig_host_t *pxsig_host(void);            /* статический экземпляр        */
int  pxsig_host_reset(pxsig_host_t *h);    /* пустое состояние             */

/* Установить корни доверия. Отдельное полномочие: это НЕ обновление
 * сигнатур (PXSIG-05). Домен не принимает trust store из пакета. */
int  pxsig_host_set_trust(pxsig_host_t *h, const pxsig_trust_t *t);

/* Приём объекта из ЛЮБОГО источника. Проверка одна и та же.
 * now_us == 0 — время недостоверно (не «пропустить проверки»). */
int  pxsig_host_ingest(pxsig_host_t *h, const uint8_t *bytes, size_t len,
                       pxsig_source_t src, const char *tenant, uint64_t now_us,
                       pxsig_ingest_result_t *out);

/* Пересобрать снимки потребителей и активировать их.
 * Вариант без пакета — для случая, когда изменился локальный overlay, а
 * нового содержимого не приходило: тестовых векторов нет, и это заявляется
 * явно. Вариант с пакетом гоняет самопроверку по ЕГО векторам. */
int  pxsig_host_refresh(pxsig_host_t *h, uint64_t now_us);
int  pxsig_host_refresh_pack(pxsig_host_t *h, const pxsig_pack_t *pack,
                             uint64_t now_us);

const pxsig_catalog_t  *pxsig_host_catalog(const pxsig_host_t *h);
const pxsig_snapshot_t *pxsig_host_snapshot(const pxsig_host_t *h,
                                            pxsig_domain_t dom);
const pxsig_node_state_t *pxsig_host_state(const pxsig_host_t *h);
const pxsig_trust_t      *pxsig_host_trust(const pxsig_host_t *h);

/* Восстановить durable-состояние (после перезапуска). Понижение
 * accepted_sequence отвергается: восстановление не должно быть способом
 * откатить узел. */
int pxsig_host_state_restore(pxsig_host_t *h, const pxsig_node_state_t *st);
pxsig_overlay_t *pxsig_host_overlay(pxsig_host_t *h);
int  pxsig_host_receipts(const pxsig_host_t *h,
                         const pxsig_activation_receipt_t **out, uint32_t *n);

/* ── CLI ───────────────────────────────────────────────────────────────────
 * Логика вынесена сюда и печатает в FILE*, чтобы её можно было прогнать без
 * консоли платформы. `cmd_pxsig.c` — тонкий адаптер к console_printf, а не
 * вторая реализация тех же команд. */
int pxsig_cli(int argc, char **argv, FILE *out, FILE *err);

/* Долговечная часть состояния узла: корни доверия и пара
 * (accepted_sequence, active_digest). Каталог сюда НЕ входит — он
 * восстанавливается повторным приёмом пакетов, и это записано, а не
 * подразумевается. Без сохранения этой пары anti-rollback переживал бы
 * только до перезапуска, то есть не работал бы (PXSIG-10). */
int pxsig_state_save(const pxsig_host_t *h, const char *path);
int pxsig_state_load(pxsig_host_t *h, const char *path);

/* Прогон последовательности команд из файла в ОДНОМ процессе: состояние узла
 * живёт в памяти, и без этого «trust add» и «ingest» никогда не встретились
 * бы в одном запуске. */
int pxsig_cli_script(const char *path, FILE *out, FILE *err);

/* ── публикация статуса наружу ─────────────────────────────────────────────
 * Через слабый хук fabric-доставки. Если он не слинкован — NOLINK и никаких
 * побочных эффектов: домен не заводит собственный транспорт. */
int pxsig_publish_status(const pxsig_host_t *h, char *detail, size_t cap);

/* Регистрация глагола `pxsig` в консоли платформы. Вызывается ТОЛЬКО из
 * register_cmds дескриптора модуля — команда существует ровно тогда, когда
 * существует подсистема. Реализация — cmd_pxsig.c (тонкий адаптер). */
void pxsig_register_console(void);

/* ── MSX ───────────────────────────────────────────────────────────────────
 * Регистрация namespace `pxsig` в контексте скрипта. Вызывается ТОЛЬКО из
 * register_script дескриптора модуля: namespace виден в .ms лишь когда
 * подсистема присутствует. void* = msx_ctx_t*. */
void pxsig_bind_msx(void *msx_ctx);
include/platx/services/pxsig_node_v1.h
/* platx/services/pxsig_node_v1.h — каталог, снимки и контракт потребителя.
 *
 * Здесь живёт то, что отделяет ДОСТАВКУ от АКТИВАЦИИ. Между проверенным
 * пакетом и работающим правилом стоят четыре разных отказа:
 *   resolve  — пакет вообще не для этого потребителя (домен/движок/схема);
 *   prepare  — содержимое не собралось или не влезло в объявленный бюджет;
 *   commit   — потребитель не смог переключить поколение;
 *   overlay  — правило есть, но локальная политика его отключила.
 * Ни один из них не сокращается до «база не обновилась»: в статусе видно,
 * ЧТО именно не действует и почему (ТЗ §8).
 *
 * Памяти домен не выделяет: массивы каталога и снимков принадлежат
 * вызывающему. Причина та же, что в NDR: домен, который сам себе выделяет
 * память, не может честно ответить на вопрос «сколько ты стоишь узлу».
 */
#define PXSIG_MAX_PROV_SOURCES 4u
#define PXSIG_MAX_CONSUMERS    8u
#define PXSIG_MAX_OVERLAY    256u

/* ── происхождение ─────────────────────────────────────────────────────────
 * Слияние дубликатов НЕ теряет источники (PXSIG-02). Поэтому у каждой
 * записи каталога есть отдельная запись происхождения, и она растёт при
 * слиянии, а не заменяется. */
typedef struct pxsig_prov {
    uint8_t  n_sources;
    uint8_t  source_id[PXSIG_MAX_PROV_SOURCES];
    uint8_t  first_pack[PXSIG_DIGEST_LEN];
    uint8_t  last_pack[PXSIG_DIGEST_LEN];
    uint64_t first_sequence, last_sequence;
    uint32_t merges;          /* сколько раз запись сливалась              */
    uint8_t  inactive;        /* отозвана tombstone'ом                     */
    uint8_t  expired;         /* по expiry самой записи                    */
} pxsig_prov_t;

/* Раздельный учёт классов (ТЗ §6): миллион IP не равен миллиону сигнатур. */
typedef struct pxsig_counts {
    uint32_t iocs, file_rules;
    uint32_t revisions_superseded;   /* заменённых ревизий                 */
    uint32_t duplicates_merged;      /* слияний без потери источника       */
    uint32_t inactive_revoked;
    uint32_t expired;
    uint32_t rejected_conflict;      /* чужой publisher под тем же id      */
    uint32_t unsupported;
} pxsig_counts_t;

/* Индекс идентичностей. Необязателен: без него слияние линейно сканирует
 * каталог, то есть весь приём пакета квадратичен по числу записей (замерено:
 * 40k записей — сотни миллисекунд, и рост нелинейный). С индексом поиск
 * записи по идентичности становится амортизированно постоянным.
 *
 * Память под индекс даёт вызывающий, как и под всё остальное: домен, который
 * сам себе выделяет память, не может честно ответить, сколько он стоит узлу.
 * Ёмкость индекса обязана быть не меньше удвоенной ёмкости массива записей
 * и степенью двойки — иначе индекс отвергается, а не работает медленно. */
typedef struct pxsig_index {
    uint32_t *slots;      /* 0 = пусто, иначе номер записи + 1              */
    uint32_t  cap;        /* степень двойки, >= 2 * cap записей             */
} pxsig_index_t;

typedef struct pxsig_catalog {
    pxsig_ioc_t       *iocs;
    pxsig_prov_t      *ioc_prov;
    uint32_t           n_iocs, cap_iocs;
    pxsig_file_rule_t *rules;
    pxsig_prov_t      *rule_prov;
    uint32_t           n_rules, cap_rules;
    pxsig_tombstone_t *tombs;
    uint32_t           n_tombs, cap_tombs;
    pxsig_index_t      ioc_idx, rule_idx;   /* пустые = линейный поиск      */
    pxsig_counts_t     counts;
} pxsig_catalog_t;

int pxsig_catalog_init(pxsig_catalog_t *c,
                       pxsig_ioc_t *iocs, pxsig_prov_t *ioc_prov, uint32_t cap_iocs,
                       pxsig_file_rule_t *rules, pxsig_prov_t *rule_prov,
                       uint32_t cap_rules,
                       pxsig_tombstone_t *tombs, uint32_t cap_tombs);

/* Подключить индексы. Вызывается сразу после init и до первого apply:
 * индексировать уже заполненный каталог этот вызов не умеет и честно
 * отказывает, вместо того чтобы построить неполный индекс. */
int pxsig_catalog_set_index(pxsig_catalog_t *c,
                            uint32_t *ioc_slots, uint32_t ioc_cap,
                            uint32_t *rule_slots, uint32_t rule_cap);

/* Влить ПРОВЕРЕННЫЙ пакет. Непроверенный отвергается: каталог не место, где
 * решают, доверять ли содержимому. */
int pxsig_catalog_apply(pxsig_catalog_t *c, const pxsig_pack_t *pack,
                        char *why, size_t why_cap);

/* Пометить истёкшие записи по времени. now_us == 0 (недостоверное время)
 * НИЧЕГО не помечает и возвращает PXSIG_E_TIME_UNTRUSTED: «срок истёк»
 * по неизвестным часам — это не факт. */
int pxsig_catalog_expire(pxsig_catalog_t *c, uint64_t now_us);

/* ── локальный overlay ─────────────────────────────────────────────────────
 * Отдельно версионируемая политика владельца. Обновление издателя её НЕ
 * стирает и не включает отключённый детектор молча (PXSIG-16, PXSIG-05). */
typedef enum pxsig_ov_action {
    PXSIG_OV_DISABLE   = 1,
    PXSIG_OV_ALLOWLIST = 2   /* индикатор считается ложным для этого узла  */
} pxsig_ov_action_t;

typedef struct pxsig_ov_entry {
    pxsig_ident_t ident;      /* revision != 0 — привязка к точной ревизии */
    uint8_t       action;
    uint8_t       pin_revision;
} pxsig_ov_entry_t;

typedef struct pxsig_overlay {
    pxsig_ov_entry_t entries[PXSIG_MAX_OVERLAY];
    uint32_t         n;
    uint32_t         generation;
    uint32_t         conflicts;   /* неоднозначные переносы между ревизиями */
} pxsig_overlay_t;

int pxsig_overlay_add(pxsig_overlay_t *o, const pxsig_ident_t *id,
                      pxsig_ov_action_t act, int pin_revision);

/* Решение overlay для записи. Возвращает:
 *   0 — правило действует;
 *   1 — отключено overlay;
 *   2 — КОНФЛИКТ: overlay привязан к другой ревизии. Правило остаётся
 *       ОТКЛЮЧЁННЫМ и попадает в conflicts. Молча включать детектор,
 *       который владелец отключал, нельзя — даже если ревизия сменилась. */
int pxsig_overlay_decide(pxsig_overlay_t *o, const pxsig_ident_t *id);

/* ── снимок ────────────────────────────────────────────────────────────────
 * Неизменяемый рабочий набор потребителя: точное поколение, digest, счётчик
 * читателей. revoke не освобождает память, пока снимок читают (PXSIG-12). */
typedef struct pxsig_snapshot {
    uint8_t  domain;
    uint64_t generation;
    uint64_t bundle_id;
    uint8_t  digest[PXSIG_DIGEST_LEN];
    uint32_t readers;
    uint8_t  revoked;
    uint8_t  sealed;
    uint32_t bytes_used;

    /* Проекция для NDR: ровно те массивы, которых ждёт intel.match:v1. */
    ndr_intel_snapshot_t ndr;
    ndr_ioc_ip_t     *ndr_ips;
    ndr_ioc_domain_t *ndr_domains;
    ndr_ioc_ja3_t    *ndr_ja3s;

    /* Проекция для AV: отсортированные sha256 файловых правил. */
    const pxsig_file_rule_t **av_rules;
    uint32_t                  n_av_rules;
} pxsig_snapshot_t;

/* Сводка сборки снимка. Каждое число отвечает на вопрос «почему правил в
 * снимке меньше, чем в каталоге» — без него уменьшение набора незаметно. */
typedef struct pxsig_prepare_report {
    uint32_t considered;
    uint32_t installed;
    uint32_t disabled_overlay;
    uint32_t conflict_overlay;
    uint32_t expired;
    uint32_t revoked;
    uint32_t unsupported;
    uint32_t bytes_used;
    uint32_t self_tests_run, self_tests_passed;
    pxsig_reason_t reason;
    char     detail[PXSIG_REASON_MAX];
} pxsig_prepare_report_t;

/* ── контракт потребителя ──────────────────────────────────────────────── */
typedef struct pxsig_consumer {
    uint8_t  domain;
    char     engine[PXSIG_ENGINE_MAX];
    uint32_t engine_version;
    uint16_t max_schema;
    uint32_t max_rules;          /* объявленный ресурсный конверт          */
    uint32_t max_bytes;
    uint8_t  supports_hot_swap;  /* иначе commit требует maintenance       */
    uint8_t  supports_drain;
} pxsig_consumer_t;

typedef struct pxsig_resolution {
    uint8_t        accepted;
    pxsig_reason_t reason;
    char           detail[PXSIG_REASON_MAX];
} pxsig_resolution_t;

/* Совместим ли пакет с потребителем. Неподдержанная семантика даёт ЯВНЫЙ
 * отказ с причиной, а не тихое упрощение правила (PXSIG-04). */
int pxsig_resolve(const pxsig_consumer_t *c, const pxsig_pack_t *pack,
                  pxsig_resolution_t *out);

/* Сборка проекции. Память под массивы снимка даёт вызывающий. */
int pxsig_prepare_ndr(const pxsig_catalog_t *cat, pxsig_overlay_t *ov,
                      const pxsig_consumer_t *c, uint64_t now_us,
                      ndr_ioc_ip_t *ips, uint32_t cap_ips,
                      ndr_ioc_domain_t *doms, uint32_t cap_doms,
                      ndr_ioc_ja3_t *ja3s, uint32_t cap_ja3s,
                      pxsig_snapshot_t *snap, pxsig_prepare_report_t *rep);

int pxsig_prepare_av(const pxsig_catalog_t *cat, pxsig_overlay_t *ov,
                     const pxsig_consumer_t *c, uint64_t now_us,
                     const pxsig_file_rule_t **out_rules, uint32_t cap_rules,
                     pxsig_snapshot_t *snap, pxsig_prepare_report_t *rep);

/* Проверка снимка тестовыми векторами пакета ДО активации. Без этого
 * «правила проверены» — заявление без проверки. Возвращает число
 * несовпадений с ожиданием (0 = все прошли) или -reason. */
int pxsig_self_test(const pxsig_snapshot_t *snap, const pxsig_pack_t *pack,
                    pxsig_prepare_report_t *rep);

/* Поиск по AV-проекции: 1 — совпало, 0 — нет, -reason при неопечатанном
 * снимке. Неопечатанный снимок использовать нельзя. */
int pxsig_av_lookup(const pxsig_snapshot_t *snap,
                    const uint8_t digest[PXSIG_DIGEST_LEN],
                    const pxsig_file_rule_t **out);

/* Читатели снимка. Между acquire и release снимок не исчезает, даже будучи
 * отозванным: находка не должна пережить своё основание, но и читатель не
 * должен читать освобождённое. */
int pxsig_snapshot_acquire(pxsig_snapshot_t *s);
int pxsig_snapshot_release(pxsig_snapshot_t *s);
int pxsig_snapshot_revoke(pxsig_snapshot_t *s);
/* 0 — можно освобождать память снимка; PXSIG_E_INFLIGHT — ещё читают. */
int pxsig_snapshot_retire(pxsig_snapshot_t *s);

/* ── активация и статус ────────────────────────────────────────────────── */
typedef struct pxsig_activation_receipt {
    uint8_t  domain;
    char     engine[PXSIG_ENGINE_MAX];
    uint64_t generation;
    uint8_t  snapshot_digest[PXSIG_DIGEST_LEN];
    uint64_t sequence;
    uint32_t overlay_generation;
    uint32_t installed, compiled, active, disabled, expired, unsupported;
    uint32_t self_tests_run, self_tests_passed;
    uint64_t committed_us;
    uint8_t  ok;
    pxsig_reason_t reason;
    char     detail[PXSIG_REASON_MAX];
} pxsig_activation_receipt_t;

/* Переключение поколения у потребителя. Возвращает 0 и receipt; при отказе
 * ПРЕЖНИЙ снимок остаётся действующим — это и есть «до успешного commit
 * действует прежний snapshot». */
int pxsig_commit(pxsig_consumer_t *c, pxsig_snapshot_t *old_snap,
                 pxsig_snapshot_t *new_snap, const pxsig_pack_t *pack,
                 const pxsig_prepare_report_t *rep, uint64_t now_us,
                 pxsig_activation_receipt_t *out);

/* Активация БЕЗ пакета — для пересборки снимка после изменения локального
 * overlay, когда нового содержимого не приходило. Пакета под рукой нет,
 * поэтому и тестовых векторов нет: вызывающий ЯВНО заявляет это флагом
 * had_no_vectors. Так «самопроверок ноль» перестаёт быть лазейкой: с
 * пакетом, который векторы несёт, ноль запусков — отказ. */
int pxsig_commit_id(pxsig_consumer_t *c, pxsig_snapshot_t *old_snap,
                    pxsig_snapshot_t *new_snap, uint64_t sequence,
                    const pxsig_prepare_report_t *rep, uint64_t now_us,
                    int had_no_vectors, pxsig_activation_receipt_t *out);

/* Обновить durable-состояние узла ПОСЛЕ успешного commit потребителя.
 * Отдельный вызов, потому что ACK не должен опережать durable commit. */
int pxsig_node_commit(pxsig_node_state_t *st, const pxsig_pack_t *pack,
                      uint64_t now_us);

/* ── группа согласованности ────────────────────────────────────────────────
 * Обязательная междоменная связь: все участники prepare до commit любого.
 * Частичная активация группы запрещена (PXSIG-13). */
typedef struct pxsig_group {
    uint32_t group_id;
    uint8_t  n_members;
    uint8_t  prepared[PXSIG_MAX_CONSUMERS];
    uint8_t  committed[PXSIG_MAX_CONSUMERS];
} pxsig_group_t;

int pxsig_group_prepare(pxsig_group_t *g, uint8_t member);
/* 0 — можно коммитить; PXSIG_E_STATE — не все участники готовы. */
int pxsig_group_can_commit(const pxsig_group_t *g);

/* ── статус узла ───────────────────────────────────────────────────────── */
typedef enum pxsig_node_health {
    PXSIG_HEALTH_OK       = 0,
    PXSIG_HEALTH_PARTIAL  = 1,   /* часть потребителей не обновлена        */
    PXSIG_HEALTH_DEGRADED = 2,   /* активный набор отозван, замены нет     */
    PXSIG_HEALTH_STALE    = 3    /* свежесть вне policy / offline          */
} pxsig_node_health_t;

const char *pxsig_health_name(pxsig_node_health_t h);

/* Свод статуса по нескольким receipt'ам. Один обновлённый потребитель НЕ
 * делает узел «актуальным» (ТЗ §7). */
int pxsig_node_health(const pxsig_activation_receipt_t *r, uint32_t n,
                      pxsig_node_health_t *out);

/* Канонический JSON статуса/receipt для Console и улик. */
int pxsig_receipt_render(const pxsig_activation_receipt_t *r, char *buf,
                         size_t cap, size_t *len_out);
include/platx/services/pxsig_v1.h
/* platx/services/pxsig_v1.h — PXSIG: типы, пакет ThreatMod и доверие.
 *
 * ГРАНИЦА ДОМЕНА, зафиксированная в самом заголовке:
 *   PXSIG владеет каталогом, происхождением, версиями, проверкой, сборкой,
 *   активацией, отзывом и историей наборов. Потребитель владеет ПРИМЕНЕНИЕМ
 *   правил к своим данным и объяснением находки. Здесь поэтому нет ни одного
 *   типа, описывающего «как искать» — только «что доставлено, чем подтверждено
 *   и что из этого разрешено этому потребителю».
 *
 * ПРАВИЛА ЭТОГО ДОМЕНА (следствие ТЗ и разбора соседних полос):
 *   1. Ноль I/O, ноль malloc, ноль обращений к часам. Байты пакета, память
 *      под разбор и ТЕКУЩЕЕ ВРЕМЯ приходят от вызывающего. Время — аргумент,
 *      потому что «часы узла врут» — это состояние, которое обязано быть
 *      выразимым (TIME_UNTRUSTED), а не незаметным.
 *   2. Подпись подтверждает ПРОИСХОЖДЕНИЕ, не качество правил. Проверенная
 *      подпись никогда не сокращается до «содержимое можно применять».
 *   3. Доставка файла не является активацией. Между ними стоят resolve,
 *      prepare, бюджеты и commit потребителя — каждый со своей причиной отказа.
 *   4. Пакет сигнатур не может нести код, менять trust roots, policy или
 *      права на эффекты. Это обеспечено ФОРМАТОМ: секции содержат только
 *      типизированные записи фиксированной формы, неизвестный тип секции —
 *      отказ, а не пропуск.
 */
/* ── идентичность capability ───────────────────────────────────────────── */
#define PLAT_NS_SIGNATURES            "signatures"
#define PLAT_NAME_SIG_CATALOG         "signatures.catalog"
#define PLAT_NAME_SIG_SNAPSHOT        "signatures.snapshot"
#define PLAT_NAME_SIG_UPDATE          "signatures.update"
#define PLAT_NAME_SIG_STATUS          "signatures.status"
#define PLAT_NAME_INTEL_MATCH         "intel.match"

#define PXSIG_V1                      0x00010000u
#define PXSIG_WIRE_MAGIC              "PXSIGPK1"
#define PXSIG_WIRE_MAGIC_LEN          8u
#define PXSIG_SCHEMA_VERSION          1u

/* ── пределы формата ───────────────────────────────────────────────────────
 * Все пределы — часть КОНТРАКТА, а не деталь реализации: разбор с иными
 * пределами принял бы пакет, который другая сторона отвергнет. */
#define PXSIG_DIGEST_LEN        32u
#define PXSIG_SIG_LEN           64u
#define PXSIG_PUBKEY_LEN        32u
#define PXSIG_PUBLISHER_MAX     16u
#define PXSIG_NAMESPACE_MAX     24u
#define PXSIG_TENANT_MAX        16u
#define PXSIG_ENGINE_MAX        24u
#define PXSIG_DOMAIN_NAME_MAX  128u   /* IOC-домен, как в intel.match:v1     */
#define PXSIG_FAMILY_MAX        32u
#define PXSIG_MAX_SECTIONS      16u
#define PXSIG_MAX_SIGNATURES     4u
#define PXSIG_MAX_DEPS           8u
#define PXSIG_MAX_CONTRACTS      8u
#define PXSIG_MAX_RECORDS    65536u   /* записей в одном пакете              */
#define PXSIG_MAX_PACK_BYTES (16u * 1024u * 1024u)
#define PXSIG_REASON_MAX       160u

/* ── коды причин ───────────────────────────────────────────────────────────
 * Причина всегда сопровождает отказ. Разница между ними — не косметика:
 * BAD_SIGNATURE и UNTRUSTED_KEY требуют разных действий владельца, а
 * ROLLBACK и STALE_SEQUENCE описывают разные атаки. */
typedef enum pxsig_reason {
    PXSIG_OK                  = 0,
    PXSIG_E_ARG               = 1,   /* аргументы вызова                    */
    PXSIG_E_TRUNCATED         = 2,   /* байты кончились раньше структуры    */
    PXSIG_E_MAGIC             = 3,
    PXSIG_E_SCHEMA            = 4,   /* версия схемы не поддержана          */
    PXSIG_E_MALFORMED         = 5,   /* поле вне допустимого диапазона      */
    PXSIG_E_AMBIGUOUS         = 6,   /* неоднозначная сериализация          */
    PXSIG_E_LIMIT             = 7,   /* превышен предел формата/бюджета     */
    PXSIG_E_DIGEST            = 8,   /* digest секции не сошёлся            */
    PXSIG_E_BAD_SIGNATURE     = 9,   /* подпись не проверилась              */
    PXSIG_E_UNTRUSTED_KEY     = 10,  /* ключа нет в trust store             */
    PXSIG_E_WRONG_ROLE        = 11,  /* ключ есть, но не той роли           */
    PXSIG_E_REVOKED_KEY       = 12,
    PXSIG_E_THRESHOLD         = 13,  /* подписей меньше требуемого числа    */
    PXSIG_E_TRUST_EPOCH       = 14,  /* эпоха доверия старше принятой       */
    PXSIG_E_ROLLBACK          = 15,  /* sequence не больше принятой         */
    PXSIG_E_NOT_YET_VALID     = 16,
    PXSIG_E_EXPIRED           = 17,
    PXSIG_E_TIME_UNTRUSTED    = 18,  /* время узла недостоверно             */
    PXSIG_E_WRONG_TENANT      = 19,
    PXSIG_E_SCOPE             = 20,  /* содержимое вне разрешённой области  */
    PXSIG_E_DEPENDENCY        = 21,  /* зависимость отсутствует             */
    PXSIG_E_BASE_MISMATCH     = 22,  /* delta не к тому базовому digest     */
    PXSIG_E_UNSUPPORTED       = 23,  /* семантика/движок не поддержаны      */
    PXSIG_E_ENGINE_MISMATCH   = 24,
    PXSIG_E_BUDGET            = 25,  /* не хватает объявленных ресурсов     */
    PXSIG_E_CONFLICT          = 26,  /* конфликт overlay / идентичностей    */
    PXSIG_E_REVOKED_CONTENT   = 27,  /* правило/пакет отозваны              */
    PXSIG_E_STATE             = 28,  /* фаза не та                          */
    PXSIG_E_INFLIGHT          = 29,  /* снимок ещё читают                   */
    PXSIG_E_NOSPACE           = 30,  /* памяти вызывающего не хватило       */
    PXSIG_E_INTERNAL          = 31
} pxsig_reason_t;

const char *pxsig_reason_name(pxsig_reason_t r);

/* ── домены потребителей ───────────────────────────────────────────────── */
typedef enum pxsig_domain {
    PXSIG_DOM_AV       = 0,
    PXSIG_DOM_NDR      = 1,
    PXSIG_DOM_EDR      = 2,
    PXSIG_DOM_FIREWALL = 3,
    PXSIG_DOM_WAF      = 4,
    PXSIG_DOM_SANDBOX  = 5,
    PXSIG_DOM__COUNT   = 6
} pxsig_domain_t;

#define PXSIG_DOM_BIT(d) (1u << (uint32_t)(d))
const char *pxsig_domain_name(pxsig_domain_t d);

/* ── типы секций ───────────────────────────────────────────────────────────
 * Список ЗАКРЫТ. Неизвестный тип секции — отказ разбора, а не пропуск:
 * «пропустить непонятное» означает применить пакет, часть которого не
 * понята, и отчитаться об этом как об успехе. Именно так в пакет сигнатур
 * попадает то, чего в нём быть не должно. */
typedef enum pxsig_section_type {
    PXSIG_SEC_IOC_SET       = 1,
    PXSIG_SEC_FILE_RULES    = 2,
    PXSIG_SEC_NETWORK_RULES = 3,
    PXSIG_SEC_EVENT_RULES   = 4,
    PXSIG_SEC_TEST_VECTORS  = 5,
    PXSIG_SEC_TOMBSTONES    = 6,
    PXSIG_SEC__MAX          = 6
} pxsig_section_type_t;

const char *pxsig_section_name(pxsig_section_type_t t);

typedef enum pxsig_pack_kind {
    PXSIG_PACK_FULL    = 1,
    PXSIG_PACK_DELTA   = 2,
    PXSIG_PACK_OVERLAY = 3    /* локальная policy: отключения и allowlist   */
} pxsig_pack_kind_t;

typedef enum pxsig_channel {
    PXSIG_CH_RELEASE = 1,
    PXSIG_CH_CANARY  = 2,
    PXSIG_CH_LAB     = 3      /* лабораторные наборы: LAB scope у находок   */
} pxsig_channel_t;

typedef enum pxsig_scope {
    PXSIG_SCOPE_PUBLIC     = 1,
    PXSIG_SCOPE_INTERNAL   = 2,
    PXSIG_SCOPE_RESTRICTED = 3
} pxsig_scope_t;

typedef enum pxsig_role {
    PXSIG_ROLE_INGEST      = 1,
    PXSIG_ROLE_SIGN        = 2,
    PXSIG_ROLE_TRUST_ADMIN = 3
} pxsig_role_t;

/* ── записи ────────────────────────────────────────────────────────────────
 * Идентичность записи: publisher + namespace + id + revision (ТЗ §2).
 * Ревизия отделена от версии пакета намеренно: пакет может выйти заново с
 * теми же правилами, и наоборот. */
typedef struct pxsig_ident {
    char     publisher[PXSIG_PUBLISHER_MAX];
    char     ns[PXSIG_NAMESPACE_MAX];
    uint32_t id;
    uint32_t revision;
} pxsig_ident_t;

typedef enum pxsig_ioc_type {
    PXSIG_IOC_IPV4   = 1,
    PXSIG_IOC_IPV6   = 2,
    PXSIG_IOC_DOMAIN = 3,
    PXSIG_IOC_JA3    = 4,
    PXSIG_IOC_SHA256 = 5
} pxsig_ioc_type_t;

typedef struct pxsig_ioc {
    pxsig_ident_t    ident;
    uint8_t          type;          /* pxsig_ioc_type_t                    */
    uint8_t          prefix_len;    /* для IP; 32/128 = точный адрес       */
    uint8_t          confidence;    /* 0..100                              */
    uint8_t          severity;      /* 0..100                              */
    uint8_t          suffix_match;  /* домен: совпадает и любой поддомен   */
    uint8_t          source_id;     /* источник ВНУТРИ каталога            */
    uint64_t         known_since_us;/* когда СТАЛО известно (не когда было)*/
    uint64_t         expiry_us;     /* 0 = без срока у самого индикатора   */
    uint8_t          addr[16];      /* IP                                  */
    char             name[PXSIG_DOMAIN_NAME_MAX];  /* домен               */
    char             hex[65];       /* ja3 (32) или sha256 (64) + NUL      */
} pxsig_ioc_t;

/* Правило по файлу. Здесь НЕТ поля «действие»: пакет сигнатур не несёт
 * разрешения на эффект (ТЗ §9, PXSIG-18). Есть только признак и его вес. */
typedef struct pxsig_file_rule {
    pxsig_ident_t ident;
    uint8_t       digest[PXSIG_DIGEST_LEN];   /* sha256 файла             */
    char          family[PXSIG_FAMILY_MAX];
    uint8_t       severity;
    uint8_t       confidence;
    uint8_t       source_id;
    uint64_t      known_since_us;
    uint64_t      expiry_us;
} pxsig_file_rule_t;

/* Отзыв. Отдельная запись, а не удаление: удалённое правило неотличимо от
 * никогда не существовавшего, и «вернуть прежнее содержимое» тогда нельзя
 * отличить от отката. */
typedef struct pxsig_tombstone {
    pxsig_ident_t ident;          /* revision 0 = отозваны все ревизии     */
    uint64_t      since_sequence;
    uint8_t       reason;         /* pxsig_reason_t, причина отзыва        */
} pxsig_tombstone_t;

/* Тестовый вектор: то, на чём пакет обязан сработать (или НЕ сработать).
 * Без них «правило проверено» — заявление без проверки. */
typedef struct pxsig_test_vector {
    pxsig_ident_t ident;        /* какое правило проверяет                 */
    uint8_t       expect_match; /* 1 — обязано совпасть, 0 — обязано нет   */
    uint8_t       kind;         /* pxsig_ioc_type_t или 0 для файла        */
    uint8_t       addr[16];
    char          name[PXSIG_DOMAIN_NAME_MAX];
    char          hex[65];
    uint8_t       digest[PXSIG_DIGEST_LEN];
} pxsig_test_vector_t;

/* ── манифест ──────────────────────────────────────────────────────────── */
typedef struct pxsig_contract {
    uint8_t  domain;                    /* pxsig_domain_t                  */
    char     engine[PXSIG_ENGINE_MAX];
    uint32_t min_version;
    uint32_t max_version;
} pxsig_contract_t;

typedef struct pxsig_limits {
    uint32_t max_rules;
    uint32_t max_ioc;
    uint32_t max_compiled_bytes;
    uint32_t max_match_cost;   /* оценка стоимости сопоставления            */
} pxsig_limits_t;

typedef struct pxsig_section_info {
    uint8_t  type;             /* pxsig_section_type_t                     */
    uint32_t n_records;
    uint32_t bytes;
    uint8_t  digest[PXSIG_DIGEST_LEN];  /* подпись покрывает digest И размер */
} pxsig_section_info_t;

typedef struct pxsig_manifest {
    uint16_t schema_version;
    uint8_t  kind;             /* pxsig_pack_kind_t                        */
    uint8_t  channel;          /* pxsig_channel_t                          */
    uint8_t  scope;            /* pxsig_scope_t                            */
    char     publisher[PXSIG_PUBLISHER_MAX];
    char     tenant[PXSIG_TENANT_MAX];
    uint64_t sequence;         /* монотонна в паре (publisher, channel)    */
    uint32_t trust_epoch;
    uint64_t issued_us, not_before_us, expiry_us;
    uint8_t  base_digest[PXSIG_DIGEST_LEN];   /* для DELTA                 */
    uint8_t  deps[PXSIG_MAX_DEPS][PXSIG_DIGEST_LEN];
    uint8_t  n_deps;
    uint32_t domains;          /* маска PXSIG_DOM_BIT                      */
    pxsig_contract_t contracts[PXSIG_MAX_CONTRACTS];
    uint8_t  n_contracts;
    uint32_t coherence_group;  /* 0 = независимый пакет                    */
    pxsig_limits_t limits;
    uint32_t unpacked_bytes;
    pxsig_section_info_t sections[PXSIG_MAX_SECTIONS];
    uint8_t  n_sections;
    uint8_t  content_digest[PXSIG_DIGEST_LEN];  /* по подписываемым байтам */
} pxsig_manifest_t;

/* ── доверие ───────────────────────────────────────────────────────────────
 * Роли РАЗДЕЛЕНЫ (PXSIG-08): ключ подписи содержимого не может менять
 * состав trust store, и наоборот. Проверка роли — часть проверки подписи,
 * а не отдельная политика где-то выше. */
typedef struct pxsig_key {
    uint32_t key_id;
    uint8_t  role;                       /* pxsig_role_t                   */
    uint8_t  revoked;
    uint32_t epoch;                      /* эпоха, в которой ключ введён   */
    uint64_t valid_from_us, valid_to_us; /* 0 = без границы                */
    uint8_t  pub[PXSIG_PUBKEY_LEN];
} pxsig_key_t;

#define PXSIG_MAX_KEYS 16u

typedef struct pxsig_trust {
    pxsig_key_t keys[PXSIG_MAX_KEYS];
    uint8_t     n_keys;
    uint32_t    epoch;             /* текущая принятая эпоха доверия       */
    uint8_t     threshold_release; /* подписей для канала RELEASE          */
    uint8_t     threshold_canary;
    uint8_t     threshold_lab;
} pxsig_trust_t;

/* ── состояние узла (anti-rollback, durable) ───────────────────────────────
 * Пара (accepted_sequence, active_digest) обязана быть согласованной после
 * сбоя в ЛЮБОЙ фазе: ACK не опережает durable commit (PXSIG-10). Поэтому
 * состояние — обычная структура, которую владелец сохраняет сам, а не
 * скрытый файл внутри домена. */
typedef struct pxsig_node_state {
    char     publisher[PXSIG_PUBLISHER_MAX];
    uint8_t  channel;
    uint64_t accepted_sequence;
    uint8_t  active_digest[PXSIG_DIGEST_LEN];
    uint32_t trust_epoch;
    uint64_t committed_us;
} pxsig_node_state_t;

/* ── разобранный пакет ─────────────────────────────────────────────────────
 * Указатели смотрят В БУФЕР ВЫЗЫВАЮЩЕГО. Копий нет: копия — это второе
 * место, где содержимое может разойтись с тем, что подписано. */
typedef struct pxsig_pack {
    const uint8_t *bytes;
    size_t         len;
    pxsig_manifest_t manifest;
    uint8_t        n_sigs;
    uint32_t       sig_key_id[PXSIG_MAX_SIGNATURES];
    const uint8_t *sig[PXSIG_MAX_SIGNATURES];
    size_t         signed_len;      /* сколько байт покрыто подписью       */
    const uint8_t *sec_data[PXSIG_MAX_SECTIONS];
    uint32_t       sec_len[PXSIG_MAX_SECTIONS];
    uint8_t        verified;        /* прошёл pxsig_verify()               */
} pxsig_pack_t;

/* ── разбор и проверка ─────────────────────────────────────────────────────
 * pxsig_parse НЕ проверяет подпись и не смотрит на время: это разбор.
 * pxsig_verify проверяет подпись, роли, порог, эпоху, время, tenant/scope,
 * зависимости и anti-rollback. Разделены потому, что «разобралось» и
 * «можно применять» — разные утверждения, и путать их опасно. */
int pxsig_parse(const uint8_t *bytes, size_t len, pxsig_pack_t *out,
                char *why, size_t why_cap);

/* now_us == 0 означает НЕДОСТОВЕРНОЕ время: проверки not-before/expiry
 * тогда не «пропускаются», а дают PXSIG_E_TIME_UNTRUSTED, если пакет
 * вообще имеет временные границы. */
int pxsig_verify(pxsig_pack_t *pack, const pxsig_trust_t *trust,
                 const pxsig_node_state_t *state, const char *tenant,
                 uint64_t now_us, char *why, size_t why_cap);

/* Замкнутость зависимостей — свойство НАБОРА пакетов, а не одного пакета,
 * поэтому проверяется отдельно от pxsig_verify: список уже принятых digest'ов
 * знает вызывающий. */
int pxsig_check_dependencies(const pxsig_pack_t *pack,
                             const uint8_t (*have)[PXSIG_DIGEST_LEN],
                             uint32_t n_have, char *why, size_t why_cap);

/* Чтение записей секции. Возвращает число прочитанных записей или -reason.
 * max — ёмкость приёмника у вызывающего; нехватка — PXSIG_E_NOSPACE, а не
 * молчаливое усечение набора правил. */
/* Поштучное чтение — основной способ. Массовое чтение ниже удобно тестам,
 * но требует буфера на всю секцию; каталог и потребители идут поштучно
 * именно поэтому. */
int pxsig_section_count(const pxsig_pack_t *p, uint8_t type);
int pxsig_ioc_at(const pxsig_pack_t *p, uint32_t idx, pxsig_ioc_t *out);
int pxsig_rule_at(const pxsig_pack_t *p, uint32_t idx, pxsig_file_rule_t *out);
int pxsig_tombstone_at(const pxsig_pack_t *p, uint32_t idx,
                       pxsig_tombstone_t *out);
int pxsig_test_vector_at(const pxsig_pack_t *p, uint32_t idx,
                         pxsig_test_vector_t *out);

int pxsig_read_iocs(const pxsig_pack_t *p, pxsig_ioc_t *out, uint32_t max);
int pxsig_read_file_rules(const pxsig_pack_t *p, pxsig_file_rule_t *out,
                          uint32_t max);
int pxsig_read_tombstones(const pxsig_pack_t *p, pxsig_tombstone_t *out,
                          uint32_t max);
int pxsig_read_test_vectors(const pxsig_pack_t *p, pxsig_test_vector_t *out,
                            uint32_t max);

/* Сборка пакета (издатель/стенд). Пишет в буфер вызывающего; возвращает
 * длину или -reason. Подпись ставится отдельно, чтобы signer оставался
 * изолированным: сборщик готовит байты, signer их подписывает. */
int pxsig_build(const pxsig_manifest_t *m,
                const pxsig_ioc_t *iocs, uint32_t n_iocs,
                const pxsig_file_rule_t *rules, uint32_t n_rules,
                const pxsig_tombstone_t *tombs, uint32_t n_tombs,
                const pxsig_test_vector_t *tvs, uint32_t n_tvs,
                uint8_t *out, size_t cap);

/* Диапазон подписываемых байт в собранном пакете. Signer подписывает ИМЕННО
 * его; никакой второй канонизации у подписывающей стороны нет. */
int pxsig_signed_range(const uint8_t *bytes, size_t len, size_t *signed_len);

/* Дописать подпись к собранному пакету. */
int pxsig_attach_signature(uint8_t *bytes, size_t len, size_t cap,
                           uint32_t key_id, const uint8_t sig[PXSIG_SIG_LEN],
                           size_t *new_len);
42

ra2c

Аутентифицированные сессии и управление связью между узлами
src/ra2c/Связь и ввод-вывод34 файлов2 API headers

RA2C предоставляет сессию как модуль платформы. Идентичность peer, согласованный материал канала, поколение и состояние обоих концов связи определяют допустимые операции. Переключение транспорта сохраняет требования аутентификации и ограничения повторов.

Граница ответственности

  • RA2C предоставляет сессию как модуль платформы. Идентичность peer, согласованный материал канала, поколение и состояние обоих концов связи определяют допустимые операции. Переключение транспорта сохраняет требования аутентификации и ограничения повторов.

Устройство подсистемы

  • RA2C single-transport layer PlatX educational system-programming platform Layer 1: Primitive backends tcp · udp · udp_bc · http · ws · dns · icmp · coap · mqtt · ntp dns_raw · doh · dot · arp · icmp_ts · tcp_opt · udp_ntp · vlan · quic · https Layer 2: DSL/manifest composer
  • RA2C single-transport eBPF covert-channel layer PlatX educational system-programming platform Wraps all 8 covert transports from covert_raw.h into ra2c_backend_t vtable instances. Transport connection is managed via a pseudo-fd table: connect() returns (EBPF_FD_BASE + slot); send/recv/close decode the slot.
  • In-memory PLUG archive transfer implementation. No disk I/O. Multiple concurrent tx_id. Streaming SHA-256. Thread-safety: none at this layer — callers hold session lock.
  • DNS / DoH / DoT wire helpers. No sockets, no malloc.
Архитектурные детали и инварианты

Overview

The transport stack is split into three layers:

Wire Protocol (ra2c_header_t, 20 bytes)

Offset | Size | Field | Value

0 | 4 | magic | 0x52413243 ("RA2C")

4 | 1 | version | 1

5 | 1 | type | ra2c_msg_t

6 | 2 | flags | FRAME_FLAG_ENCRYPTED=1

8 | 4 | seq | monotonic TX counter

12 | 4 | session_crc | crc32(session_id)

16 | 4 | payload_len | bytes following (≤65536)

Key Exchange

- X25519 ECDH (ephemeral, one keypair per session)

- HKDF-SHA256: salt=session_id(+PSK if set), info="RA2C-v1-session"

- AEAD: ChaCha20-Poly1305, nonce per-frame (12 bytes prepended)

Backends

Name | File | TLS | Dgram | Notes

tcp | transport.c | No | No | Default

tls | transport_tls.c | Yes | No | TLS 1.3

unix | transport_ipc.c | No | No | UNIX domain socket

loop | transport_loop.c | No | No | Test loopback

ws | transport_ws.c | No | No | WebSocket tunnel

Rate Limiting (ra2c_ratelimit.c)

Static slot table (64 /24 prefixes), token bucket, RA2C_RL_MAX_PER_WINDOW=128

connections per 60 seconds per /24. No malloc.

Replay Protection (ra2c_replay.c)

Sliding window of RA2C_SEQ_WINDOW=16 bits. Late in-window delivers;

seq_rx never rewinds; out-of-window = RA2C_ERR_PROTO.

Управление и диагностика

Корневые команды: ra2c-single, ra2c-ebpf-single, ra2c, ra2c-ebpf. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / ra2c →
Состав подсистемы / 34 файлов
Файл / компонентНазначение и граница
src/ra2c/ra2c_backend.cRA2C single-transport layer PlatX educational system-programming platform Layer 1: Primitive backends tcp · udp · udp_bc · http · ws · dns · icmp · coap · mqtt · ntp dns_raw · doh · dot · arp · icmp_ts · tcp_opt · udp_ntp · vlan · quic · https Layer 2: DSL/manifest composer
src/ra2c/ra2c_backend.hbackend vtable, registry types, raw-connect helpers. Live path: ra2c_backend.c (bytes) + ra2c_xio.h (session) + ra2c_session.c (CLI). ra2c.c is not this stack. This header has no session/wire protocol.
src/ra2c/ra2c_backend_ebpf.cRA2C single-transport eBPF covert-channel layer PlatX educational system-programming platform Wraps all 8 covert transports from covert_raw.h into ra2c_backend_t vtable instances. Transport connection is managed via a pseudo-fd table: connect() returns (EBPF_FD_BASE + slot); send/recv/close decode the slot.
src/ra2c/ra2c_blob_channel.cIn-memory PLUG archive transfer implementation. No disk I/O. Multiple concurrent tx_id. Streaming SHA-256. Thread-safety: none at this layer — callers hold session lock.
src/ra2c/ra2c_blob_channel.hIn-memory PLUG archive transfer (no disk). Multiple concurrent tx_id transfers are supported. All assembly buffers live in malloc'd RAM; nothing touches the filesystem. Sender side: blob_tx_* functions Receiver side: blob_rx_* functions
src/ra2c/ra2c_dns_frame.cDNS / DoH / DoT wire helpers. No sockets, no malloc.
src/ra2c/ra2c_dns_frame.hportable DNS / DoH / DoT framing for RA2C backends. No sockets. Linux covert_raw and Windows T-DOH/T-DOT both encode the same 30-byte covert payload: query = base32 QNAME labels + marker "cvrtr" (RFC 1035) reply = TXT RR carrying the raw payload DoT = 2-octet length prefix + DNS message (RFC 7858 / RFC 1035 TCP)
src/ra2c/ra2c_ebpf.hra2c_backend_t for covert channels + userspace mirrors. Session wire is ra2c_xio (ebpf_session_t.proto). These backends move bytes. Multi-channel rotation is fd policy in ra2c_session_ebpf.c. ra2c.c is not this stack. Covert IDs 0–9 move bytes on BPF/covert fds. IDs 10–18 are the same
src/ra2c/ra2c_export.cExport session statistics to poe (proof-of-execution). One compact record per session, written at disconnect time. A3-W1-F014: the first revision of this file was written against an API that does not exist — it read s->stats.* (ra2c_xio_session_t has no such member)
src/ra2c/ra2c_export.hОбъявления типов и интерфейсов
src/ra2c/ra2c_mil.cMIL mode adds per-frame HMAC-SHA256 signing to the audit trail and enforces stricter connection policy: FRAME_FLAG_MIL (0x0002) must be set on every frame
src/ra2c/ra2c_mod.cthin platx adapt for existing RA2C. Invariant: this file does not speak the RA2C wire protocol and does not create sessions. I/O stays in ra2c_session.c. This file does not decide restart — only plat_recovery_watch, then plat_lifecycle_request. After STOP the cap is gone; after START it is findable again.
src/ra2c/ra2c_mod.hprotocol.ra2c as a platx module. Not a new RA2C protocol.
src/ra2c/ra2c_mod_caps.cCapability advertisement builder/parser/handler. MSG_MOD_CAPS_ADV (0x81) wire layout (TLV, Canon C19): TLV_CAPS_ADV_PROTO_VERSION [4] u32 BE — protocol version TLV_CAPS_ADV_NODE_ID [8] u64 BE — sender node_id TLV_CAPS_ADV_MERKLE_ROOT [32] SHA-256 Merkle root of all caps
src/ra2c/ra2c_mod_caps.hCapability advertisement for dynamic module protocol. 1. ra2c_mod_on_session_up() [strong, overrides xio.c weak stub] is called. 2. It enumerates local caps via plat_cap_foreach, builds MSG_MOD_CAPS_ADV and sends it over the session. 3. When the peer's MSG_MOD_CAPS_ADV arrives (on_mod_msg type=0x81),
src/ra2c/ra2c_mod_central.cCentral DYNMOD V2 node implementation. Security: no disk writes; no trust-store mutation; BLOB send only on explicit operator action after MSG_CAP_ACCEPT.
src/ra2c/ra2c_mod_central.hCentral DYNMOD V2 node: in-memory archive store and BLOB-channel sender. Security invariants: - Archives are verified with plug_verify() before entering the store. - NO archive is written to disk at any time. - Trust store is NEVER modified here. - BLOB send is initiated only by the local operator (or a policy engine);
src/ra2c/ra2c_mod_mesh.cmesh propagation for DYNMOD V2 module offers. Security invariants (strictly enforced): - Inbound MSG_MOD_OFFER only surfaces an offer record (mmod_offer_t). - NO archive is auto-fetched or auto-loaded. - Trust store is NEVER modified via mesh messages. - Offer callback MUST NOT initiate a fetch without explicit operator action.
src/ra2c/ra2c_mod_mesh.hmesh propagation for DYNMOD V2 module offers. Security model (strictly enforced): - Mesh peers may OFFER modules; the local operator must explicitly ACCEPT. - No archive is auto-fetched or auto-loaded from a mesh message. - MSG_MOD_OFFER with MOD_FLAG_MESH_ROUTED only surfaces an offer record.
src/ra2c/ra2c_mod_proto.hwire types for DYNMOD V2 capability-negotiation protocol. All new MSG types live in 0x80–0x97 (free range per ra2c_xio.h). They are ONLY accepted when keyed=1 AND peer_auth=1. TLV wire primitive (Canon C19): [type u8][len u16 BE][value[len]]
src/ra2c/ra2c_mod_receipt.cEd25519-signed execution receipt (TZ_RA2C_DYNMOD_V2). Encoding order (body = fields 0x01–0x0B, signed as a whole): 0x01 ARTIFACT_DIGEST [32] sha256 of the PLUG archive 0x02 INSTANCE_ID [24] node_id(BE u64) + generation(BE u32) + nonce[12] 0x03 BINDING_ID [24] session_id[16] + cap_gen(BE u32) + seq(BE u32)
src/ra2c/ra2c_mod_receipt.hEd25519-signed execution receipt for RA2C modules. A receipt proves that a given artifact (identified by PLUG SHA-256) ran on this node under a specific binding, at a specific time, with a measured health state. The receipt body is signed with the node's Ed25519 identity
src/ra2c/ra2c_mod_rx.cinbound DYNMOD V2 message handler. Security invariants (strictly enforced): - BLOB load fires ONLY when session is keyed + peer_auth. - MSG_CAP_ACCEPT never triggers an automatic BLOB send. - Trust store is never modified here. - Reassembly buffer is capped at CENTRAL_RECV_MAX_SIZE (8 MiB).
src/ra2c/ra2c_mod_rx.hinbound dynamic-module message handler (DYNMOD V2). Security invariants: - MSG_BLOB_END triggers verify+load ONLY with an authenticated, keyed session. - MSG_CAP_ACCEPT NEVER auto-initiates a BLOB send; it only fires a callback. - Trust store is NEVER modified here.
src/ra2c/ra2c_msx_binding.cnative MSX door into ra2c_xio. Not operator policy. Path: ra2c.connect → handle → send/recv/switch/disconnect. Session lives in a slot owned by that resident VM. Teardown of the VM disconnects first (no leftover keyed, no revoke of a dead owner's fd). I/O stays in ra2c_xio_*. Binding must not open a raw BSD socket.
src/ra2c/ra2c_msx_binding.hthin MSX ↔ ra2c_xio. Not an operator script. Connect returns a handle owned by that resident VM. send/recv/switch/ disconnect take the handle. VM teardown disconnects; no leftover keyed.
src/ra2c/ra2c_probe.cConnection probe: verify reachability without data transfer. Performs handshake only; disconnects before exchanging any payload.
src/ra2c/ra2c_ratelimit.cConnection rate limiting without malloc. Tracks new-connection rate per source /24 using a static slot table. Algorithm: token-bucket per /24 prefix. - RA2C_RL_SLOTS static slots, LRU eviction when full. - Each slot: (prefix_be, count, window_start_ms). - ra2c_ratelimit_check() → 0 = allow, -1 = reject.
src/ra2c/ra2c_replay.cReplay-protection sequence-number window. out-of-window → RA2C_ERR_PROTO, not dead session. Window = bitmask of RA2C_SEQ_WINDOW bits. bit i set → seq (last_seq - i) has been received. bit 0 → last_seq itself. No malloc. No locks — caller holds session mutex.
src/ra2c/ra2c_session.cRA2C session CLI over a single-transport backend One process-global session (g_xsess). Live only when active && keyed && fd >= 0. Frame I/O and AEAD live in ra2c_xio.c. Working path: connect/listen → handshake → cmd/file → switch/rekey → disconnect. collect / sandbox are not on that path — they refuse.
src/ra2c/ra2c_session_ebpf.cRA2C full-session layer over eBPF covert channels Extends ra2c_backend_ebpf.c with a complete session protocol: Layer 1 Covert transports — backends from covert_raw.h + XDP/TC BPF Layer 2 RA2C framing — ChaCha20-Poly1305 AEAD, seq tracking Layer 3 Multi-channel — rotate 1-4 transports per frame
src/ra2c/ra2c_stats.cTransport statistics without malloc. Atomic counters; thread-safe snapshot via ra2c_transport_stats_get().
src/ra2c/ra2c_xio.clive RA2C session over XIO. Working path: connect/attach → handshake → send/recv → switch/rekey → disconnect. After handshake the session is keyed+active with a live fd, or it is closed. No ACK without a channel, no leftover fd without a key. Unauthenticated ECDH needs allow_legacy or (non-agent) RA2C_INSECURE_ECDH=1.
src/ra2c/ra2c_xio.hlive RA2C session: wire + X25519 + AEAD. Working path is single (bytes) → this header (session) → advanced (CLI). ra2c.c is not the stack and is not linked. ra2c_msg_t — wire types (MSG_HELLO … MSG_ERROR) ra2c_xio_frame_t — canonical packed frame (this is the wire, not a copy)
Контракты API / 2 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/ra2c.h
/* include/platx/ra2c.h — Public RA2C wire protocol API.
 *
 * Single authoritative header for ra2c_header_t (wire frame), constants,
 * invariant contracts, and counters.  Does NOT include session internals
 * (ra2c_xio.h) or backend vtable (ra2c_single.h).
 *
 * Tasks: 1.02 static_assert(sizeof(ra2c_header_t)==20)
 *        1.06 MAX_RA2C_PAYLOAD
 *        1.11 INV-RA2C-01: nonce overflow → close
 *        1.12 INV-RA2C-02: AEAD auth fail → disconnect + audit
 *        1.13 INV-RA2C-03: bad magic → disconnect, no reply
 *        1.33 public include/ra2c.h
 *        1.38 INV-TRANSPORT-01: no profile → no connection
 *        1.39 transport counters
 *
 * agent3 wave1
 */
/* ── Wire protocol constants ─────────────────────────────────────────────── */

#define RA2C_MAGIC          0x52413243u   /* "RA2C"                           */
#define RA2C_VERSION        1u            /* protocol version readers enforce  */
#define MAX_RA2C_PAYLOAD    65536u        /* §1.05: payload_len must not exceed */
/* MUST stay equal to RA2C_MAX_PAYLOAD in src/transport/ra2c_xio.h (A3-W1-F003) */
/* A3-W1-F001: src/transport/ra2c_xio.h defines the same four names. Guard the
 * duplicates so both headers can coexist in one TU, then assert the values
 * still agree — a silent divergence between them would corrupt the wire. */
#define RA2C_SESSION_ID_LEN 16u
#define RA2C_KEY_LEN        32u
#define RA2C_TAG_LEN        16u
#define RA2C_NONCE_LEN      12u
#define RA2C_SEQ_WINDOW     16u           /* replay: late-in-window delivers   */
static_assert(RA2C_SESSION_ID_LEN == 16, "RA2C_SESSION_ID_LEN diverged");
static_assert(RA2C_KEY_LEN        == 32, "RA2C_KEY_LEN diverged");
static_assert(RA2C_SEQ_WINDOW     == 16, "RA2C_SEQ_WINDOW diverged");
static_assert(MAX_RA2C_PAYLOAD    == 65536u, "MAX_RA2C_PAYLOAD diverged");

/* ── Wire frame (packed; all multi-byte fields in network byte order) ──────
 *
 *  Offset  Size  Field
 *  ------  ----  -----
 *     0      4   magic        = RA2C_MAGIC
 *     4      1   version      = RA2C_VERSION
 *     5      1   type         (ra2c_msg_t)
 *     6      2   flags        (FRAME_FLAG_*)
 *     8      4   seq          monotonic TX counter
 *    12      4   session_crc  crc32(session_id)
 *    16      4   payload_len  bytes following header
 *  Total   20
 */
typedef struct {
typedef struct __attribute__((packed)) {
    uint32_t magic;        /* RA2C_MAGIC                                    */
    uint8_t  version;      /* RA2C_VERSION                                  */
    uint8_t  type;         /* ra2c_msg_t                                    */
    uint16_t flags;        /* FRAME_FLAG_*                                  */
    uint32_t seq;          /* monotonic TX counter (host→wire: htonl)       */
    uint32_t session_crc;  /* crc32(session_id) sanity check                */
    uint32_t payload_len;  /* bytes following header (incl. nonce+tag)      */
} ra2c_header_t;

/* 1.02: layout must stay exactly 20 bytes — checked at compile time */
static_assert(sizeof(ra2c_header_t) == 20,
              "ra2c_header_t must be exactly 20 bytes");
#define FRAME_FLAG_ENCRYPTED 0x0001u    /* payload: nonce(12)||ciphertext||tag(16) */
#define FRAME_FLAG_MIL       0x0002u    /* MIL-mode extra fields present           */
static_assert(FRAME_FLAG_ENCRYPTED == 0x0001u, "FRAME_FLAG_ENCRYPTED diverged");

/* ── Invariant contracts ─────────────────────────────────────────────────── */

/*
 * INV-RA2C-01: nonce overflow → connection close (never wraparound)
 *
 *   When seq_tx would reach UINT32_MAX the sender MUST close the connection
 *   and open a new session (or trigger a rekey).  Wraparound is forbidden
 *   because it would reuse AEAD (key,nonce) pairs.
 *
 *   Implementation: ra2c_xio_send_frame() checks before increment:
 *     if (s->seq_tx >= RA2C_SEQ_MAX) { ra2c_xio_disconnect(s); return RA2C_INV_ERR_NONCE; }
 */
#define RA2C_SEQ_MAX        0xFFFFFFF0u  /* close 16 below UINT32_MAX for headroom */
#define RA2C_INV_ERR_NONCE  (-15)        /* nonce exhaustion — close, not retry    */

/*
 * INV-RA2C-02: Poly1305 authentication failure → immediate disconnect + audit
 *
 *   Any AEAD decryption failure MUST result in:
 *     1. Immediate session close (no partial delivery)
 *     2. Audit record: type=AUDIT_RA2C_AUTH_FAIL, session_id, peer addr
 *     3. No error details returned to remote peer
 *
 *   Implementation: ra2c_xio_decrypt() on auth fail returns RA2C_INV_ERR_AUTH;
 *   recv_frame() on RA2C_ERR_AUTH calls ra2c_xio_disconnect() + audit_append().
 */
#define RA2C_INV_ERR_AUTH   (-13)        /* == ra2c_result_t RA2C_ERR_AUTH         */

/*
 * INV-RA2C-03: bad magic or version → disconnect without reply (no oracle)
 *
 *   A frame with wrong magic or unknown version MUST:
 *     1. Drop the connection silently (no error frame sent)
 *     2. Not log payload details (prevents oracle)
 *
 *   Implementation: recv_frame() magic/version check → LOG_W + disconnect,
 *   return RA2C_INV_ERR_PROTO.  No MSG_ERROR reply.
 */
#define RA2C_INV_ERR_PROTO   (-4)        /* == ra2c_result_t RA2C_ERR_PROTO        */

/*
 * INV-TRANSPORT-01: connection without valid profile HMAC is impossible.
 * §SEC-0: transport_init() refuses to start if profile HMAC verification fails.
 */
#define RA2C_INV_ERR_NO_PROFILE (-16)    /* transport refused: profile HMAC fail   */

/* ── Replay protection state ─────────────────────────────────────────────── */

/* Managed by ra2c_replay.c; embedded in ra2c_xio_session_t */
typedef struct {
    uint32_t last_seq;                  /* highest accepted seq              */
    uint32_t window;                    /* bitmask: bit i = (last_seq-i) ok  */
} ra2c_replay_t;

/* Returns 0 if seq is fresh, -1 if duplicate/rewind */
int  ra2c_replay_check(ra2c_replay_t *r, uint32_t seq);
/* Advance window after successful decryption */
void ra2c_replay_advance(ra2c_replay_t *r, uint32_t seq);

/* ── Rate limiting state ─────────────────────────────────────────────────── */

/* Managed by ra2c_ratelimit.c; one per listening socket */
typedef struct {
    uint32_t conn_count;     /* active connections from same /24             */
    uint32_t reject_count;   /* connections rejected in current window       */
    uint64_t window_start_ms;
    /* token bucket: no malloc, fixed static table of RA2C_RL_SLOTS entries */
} ra2c_ratelimit_t;

#define RA2C_RL_MAX_PER_WINDOW  128      /* new connections per 60s per /24  */
#define RA2C_RL_WINDOW_MS       60000u

int  ra2c_ratelimit_check(ra2c_ratelimit_t *rl, uint32_t src_ip_be,
                          uint64_t now_ms);
void ra2c_ratelimit_reset(ra2c_ratelimit_t *rl, uint64_t now_ms);

/* ── Transport counters ──────────────────────────────────────────────────── */

/* 1.39: exported to plat_compstat via transport_stats_get() */
typedef struct {
    uint64_t connections;      /* total connections accepted                 */
    uint64_t bytes_tx;         /* total bytes transmitted                    */
    uint64_t bytes_rx;         /* total bytes received                       */
    uint64_t auth_fails;       /* INV-RA2C-02 events                        */
    uint64_t disconnects;      /* total disconnections                       */
    uint64_t nonce_closes;     /* INV-RA2C-01 events (nonce overflow)       */
    uint64_t proto_drops;      /* INV-RA2C-03 events (bad magic)            */
    uint64_t replay_drops;     /* replay-rejected frames                    */
    uint64_t rate_limit_drops; /* rate-limited connections                  */
} ra2c_transport_stats_t;

/* Thread-safe snapshot; ra2c_stats.c implements with atomic loads */
void ra2c_transport_stats_get(ra2c_transport_stats_t *out);

/* 1.39: counter setters — ra2c_stats.c, atomic, no malloc (§SEC-3).
 * Declared here so the transport can raise them without a private header. */
void ra2c_stat_inc_connections(void);
void ra2c_stat_add_bytes_tx(uint64_t n);
void ra2c_stat_add_bytes_rx(uint64_t n);
void ra2c_stat_inc_auth_fails(void);
void ra2c_stat_inc_disconnects(void);
void ra2c_stat_inc_nonce_closes(void);
void ra2c_stat_inc_proto_drops(void);
void ra2c_stat_inc_replay_drops(void);
void ra2c_stat_inc_rate_drops(void);

/* ── CRC-32 helper (ra2c_single.c implements) ────────────────────────────── */

uint32_t ra2c_crc32(const void *data, size_t len);
include/platx/services/ra2c_v1.h
/* platx/services/ra2c_v1.h — registry identity for protocol.ra2c.
 * Not a session/protocol API. Presence of this vtable is the live cap.
 */
#define PLAT_RA2C_V1   0x00010000u

struct plat_ra2c_v1 {
    uint32_t struct_size;
    int (*health)(void);
    /* Optional. Non-zero = live session claims ready, but PSK/pin/identity
     * was configured and peer_auth is still false. Cap ≠ session: idle with
     * PSK configured must return 0 here. */
    int (*auth_gap)(void);
    /* Optional. MBus-over-RA2C: corr_id and deadline_ms are mandatory
     * (zero is ARGS, not a default). Missing pointer or dead session is
     * UNAVAILABLE — not a silent drop. */
    int (*push)(const char *type, const void *payload, size_t len,
                uint64_t corr_id, uint32_t deadline_ms);
};

/* Base size: health present. Trailing fields are optional by struct_size. */
#define PLAT_RA2C_V1_BASE_SIZE \
    (offsetof(struct plat_ra2c_v1, health) + \
     sizeof(((struct plat_ra2c_v1 *)0)->health))
43

sandbox

Подтверждаемая изоляция исследовательских нагрузок
src/sandbox/Доверие и защита12 файлов4 API headers

Sandbox согласует требуемую границу задачи с возможностями конкретного провайдера. Probe показывает механизмы хоста, ready — основания допуска, run — исход нагрузки, доказанность изоляции и полноту наблюдения. Завершение включает перепись остатков и отдельный результат очистки.

Граница ответственности

  • Sandbox согласует требуемую границу задачи с возможностями конкретного провайдера. Probe показывает механизмы хоста, ready — основания допуска, run — исход нагрузки, доказанность изоляции и полноту наблюдения. Завершение включает перепись остатков и отдельный результат очистки.

Устройство подсистемы

  • адаптер CLI домена PXSANDBOX к консоли платформы. Логики здесь нет: она в sb_cli.c и печатает в FILE*. Вывод собирается в поток в памяти и отдаётся консоли одним куском — console_printf не совместим по сигнатуре с FILE*, а второй набор форматирования означал бы
  • автономный CLI домена PXSANDBOX. Домен в поставляемый профиль не входит (src/sandbox отсутствует в mk/source-manifests.mk намеренно), поэтому глагол `sandbox` внутри platx появится только по решению владельца. До тех пор CLI обязан быть запускаемым и проверяемым — иначе «CLI есть» было бы утверждением без
  • CLI домена PXSANDBOX. Здесь появляется всё, чего нет в ядре домена: печать, разбор аргументов, статический координатор процесса. Ядро остаётся тем же — оно ничего не ЧТО ЭТОТ CLI ПОКАЗЫВАЕТ, И ПОЧЕМУ ИМЕННО ЭТО. Песочница ценна не тем, что «запустила файл», а тем, что может ответить:
  • Sandbox Coordinator: очередь, квоты, состояние задания. ГРАНИЦЫ ОТВЕТСТВЕННОСТИ, зафиксированные в коде, а не только в ТЗ: — координатор НЕ исполняет workload в своём процессе (этого здесь просто нет: единственный путь к исполнению — вызовы provider->*);

Управление и диагностика

Корневые команды: sandbox. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / sandbox →
Состав подсистемы / 12 файлов
Файл / компонентНазначение и граница
src/sandbox/cmd_sandbox.cадаптер CLI домена PXSANDBOX к консоли платформы. Логики здесь нет: она в sb_cli.c и печатает в FILE*. Вывод собирается в поток в памяти и отдаётся консоли одним куском — console_printf не совместим по сигнатуре с FILE*, а второй набор форматирования означал бы
src/sandbox/sandboxctl_main.cавтономный CLI домена PXSANDBOX. Домен в поставляемый профиль не входит (src/sandbox отсутствует в mk/source-manifests.mk намеренно), поэтому глагол `sandbox` внутри platx появится только по решению владельца. До тех пор CLI обязан быть запускаемым и проверяемым — иначе «CLI есть» было бы утверждением без
src/sandbox/sb_cli.cCLI домена PXSANDBOX. Здесь появляется всё, чего нет в ядре домена: печать, разбор аргументов, статический координатор процесса. Ядро остаётся тем же — оно ничего не ЧТО ЭТОТ CLI ПОКАЗЫВАЕТ, И ПОЧЕМУ ИМЕННО ЭТО. Песочница ценна не тем, что «запустила файл», а тем, что может ответить:
src/sandbox/sb_coordinator.cSandbox Coordinator: очередь, квоты, состояние задания. ГРАНИЦЫ ОТВЕТСТВЕННОСТИ, зафиксированные в коде, а не только в ТЗ: — координатор НЕ исполняет workload в своём процессе (этого здесь просто нет: единственный путь к исполнению — вызовы provider->*);
src/sandbox/sb_internal.hвнутренние функции PXSANDBOX. Не публичный ABI. Модули вне src/sandbox/ этот заголовок не включают.
src/sandbox/sb_os_posix.cслой ОС PXSANDBOX для POSIX/Linux. Пара к platx-windows/win/sandbox/sb_os_win.c. Оба файла реализуют один и тот же набор функций из sb_internal.h; какой из них собирается — решает сборка. Здесь нет ничего про изоляцию: только каталоги, часы и чтение файла без следования ссылке — то, что нужно координатору и CLI.
src/sandbox/sb_profile.cвалидация профиля и его digest. Профиль — единственное место, где записано, что означает «этот уровень защищает договор, который сам ничего не требует. Поэтому здесь: 1. PROCESS_ENFORCED имеет ОБЯЗАТЕЛЬНЫЙ ПОЛ механизмов. Профиль не может назвать себя enforced, потребовав два бита из десяти.
src/sandbox/sb_provider_linux.cпровайдер PROCESS_ENFORCED для Linux. Между установкой ограничений и первой инструкцией workload есть окно, в котором ограничения уже стоят, а недоверенный образ ещё не запущен. Всё устройство этого файла существует ради этого окна: child доходит до gate,
src/sandbox/sb_receipt.cканонический receipt задания. Receipt — это то, что остаётся после задания и по чему судят о нём потом. Поэтому здесь два правила: 1. Порядок полей фиксирован, экранирование строгое. Receipt сравнивают между прогонами и между ОС; «тот же смысл в другом порядке» сравнить
src/sandbox/sb_registry.cстатический реестр провайдеров. Реестр намеренно НЕ динамический. Зарегистрировать чужой код как «границу изоляции» — решение владельца платформы, а не побочный эффект загрузки модуля. Отсутствие провайдера под требуемый boundary даёт отказ с причиной NO_PROVIDER, а не подстановку более слабого уровня.
src/sandbox/sb_types.cимена и граф состояний PXSANDBOX. Граф состояний живёт ЗДЕСЬ и больше нигде. Координатор спрашивает plat_sb_state_transition_ok(); второй список переходов в координаторе означал бы два разных ответа на один вопрос — ровно тот класс дефекта, из-за которого «cancel» выдавали за «exit».
src/sandbox/sb_util.cмелкие общие функции. Ничего специфичного для ОС: часы и каталоги живут в слое ОС (sb_os_posix.c | sb_os_win.c).
Контракты API / 4 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/sandbox_host.h
/* platx/sandbox_host.h — узел PXSANDBOX: CLI и его окружение.
 *
 * Та же граница, что в PXSIG: ядро домена (`sb_provider_linux`, `sb_profile`,
 * `sb_coordinator`, …) не печатает и не читает файлы, а всё, что говорит с
 * человеком, живёт здесь и в `src/sandbox/{sb_cli,cmd_sandbox}.c`.
 *
 * Логика CLI печатает в FILE*, а не в console_ctx_t, чтобы её можно было
 * прогнать без консоли платформы; `cmd_sandbox.c` — тонкий адаптер. Двух
 * реализаций одной команды нет: они расходятся молча.
 */
/* Разбор и исполнение одной команды. argv[0] — имя глагола. */
int plat_sb_cli(int argc, char **argv, FILE *out, FILE *err);

/* Последовательность команд в одном процессе: координатор и его задания
 * живут в памяти, поэтому `run` и `receipt` в отдельных запусках друг друга
 * не видят. */
int plat_sb_cli_script(const char *path, FILE *out, FILE *err);

/* Регистрация глагола `sandbox` в консоли. Вызывается ТОЛЬКО из register_cmds
 * дескриптора модуля — команда существует ровно тогда, когда существует
 * подсистема. */
void plat_sb_register_console(void);
include/platx/services/sandbox_analysis_v1.h
/* platx/services/sandbox_analysis_v1.h — sandbox.analysis:v1 и sandbox.task:v1.
 *
 * Координатор владеет ТОЛЬКО состоянием задания: очередь, квоты, выбор
 * провайдера, отмена, receipt. Он не владеет жизненным циклом модулей
 * (это Core), не решает карантин (это AV/Policy) и не исполняет образец
 * в своём процессе.
 *
 * sandbox.task:v1 — та же машина и те же провайдеры для ШТАТНЫХ задач PLATX
 * (HARDENED_TASK). Отличие не в границе, а в том, что задача не проходит
 * AV scoring и получает scoped broker вместо наблюдения за образцом.
 */
#define PLAT_SB_ANALYSIS_V1  0x00010000u

#define PLAT_SB_TENANT_MAX   64u
#define PLAT_SB_OWNER_MAX    64u
#define PLAT_SB_REASON_TXT   128u

/* Запрос. Хранит ССЫЛКИ, а не команды: путь к образу берётся из профиля и
 * проверенного artifact reference, не из строки вызывающего. */
typedef struct plat_sb_request {
    plat_desc_hdr_t hdr;
    char     owner[PLAT_SB_OWNER_MAX];
    char     tenant[PLAT_SB_TENANT_MAX];
    uint64_t owner_generation;
    char     reason[PLAT_SB_REASON_TXT];
    char     profile_id[PLAT_SB_ID_MAX];
    uint8_t  artifact_digest[PLAT_SB_DIGEST_LEN];
    uint32_t have_artifact_digest;
    char     idempotency_key[PLAT_SB_ID_MAX];
    uint32_t priority;              /* 0 = высший                          */
    uint32_t ttl_ms;
    plat_sb_purpose_t purpose;
    /* Снимок правил, закреплённый за этим заданием. Профиль решает,
     * обязателен ли он; координатор его только проверяет и переносит. */
    plat_sb_rule_snapshot_t rules;
    uint8_t  reserved[32];
} plat_sb_request_t;

#define PLAT_SB_REQUEST_VERSION 1u

/* Квоты. Защищают ОСНОВНОЙ путь PLATX от очереди анализов (ТЗ §7). */
typedef struct plat_sb_quota {
    uint32_t max_concurrent_global;
    uint32_t max_concurrent_per_tenant;
    uint32_t max_queue;
    uint32_t reserve_slots;   /* сколько слотов НЕ отдаётся анализу         */
} plat_sb_quota_t;

typedef struct plat_sb_coordinator plat_sb_coordinator_t;

typedef struct plat_sb_analysis_v1 {
    uint32_t struct_size;
    uint32_t version;

    /* Регистрация профиля. Профиль обязан пройти валидацию; невалидный
     * профиль не попадает в каталог, а не «работает с оговорками». */
    int (*profile_register)(plat_sb_coordinator_t *c,
                            const plat_sb_profile_t *p);
    const plat_sb_profile_t *(*profile_get)(plat_sb_coordinator_t *c,
                                            const char *profile_id);

    /* Подача. Возвращает 0 и job_id при ADMITTED, иначе -plat_sb_reason_t. */
    int (*submit)(plat_sb_coordinator_t *c,
                  const plat_sb_request_t *req,
                  const plat_sb_workload_t *w,
                  char job_id_out[PLAT_SB_ID_MAX]);

    /* Один шаг машины состояний для задания. Синхронный: у координатора нет
     * своего пула потоков — второй scheduler запрещён контрактом. */
    int (*step)(plat_sb_coordinator_t *c, const char *job_id);

    /* Довести задание до SEALED (или терминального отказа). */
    int (*run_to_completion)(plat_sb_coordinator_t *c, const char *job_id);

    int (*state)(plat_sb_coordinator_t *c, const char *job_id,
                 plat_sb_state_t *out);
    int (*cancel)(plat_sb_coordinator_t *c, const char *job_id,
                  const char *owner, uint64_t owner_generation);
    int (*result)(plat_sb_coordinator_t *c, const char *job_id,
                  plat_sb_job_result_t *out);

    /* Канонический receipt задания (JSON, стабильный порядок полей). */
    int (*receipt)(plat_sb_coordinator_t *c, const char *job_id,
                   char *buf, size_t cap, size_t *len_out);

    /* Приёмка ВЫХОДНОГО артефакта задания (ТЗ §5.1 «Результат и реальные
     * эффекты»). Успешное исполнение НЕ делает выход доверенным, поэтому:
     *   — имя выхода — один сегмент внутри рабочего пространства задания,
     *     никаких '/', '..' и абсолютных путей;
     *   — размер сверх max_bytes отвергается целиком, а не усекается;
     *   — считается digest, и он возвращается вызывающему: принимающая
     *     сторона сверяет его со своим ожиданием сама;
     *   — задание, не дошедшее до SEALED с containment=enforced, выхода не
     *     отдаёт вовсе.
     * Возврат 0 и digest_out при приёмке; иначе -plat_sb_reason_t. */
    int (*accept_output)(plat_sb_coordinator_t *c, const char *job_id,
                         const char *output_name, uint64_t max_bytes,
                         uint8_t digest_out[PLAT_SB_DIGEST_LEN],
                         uint64_t *size_out);

    /* Освободить запись завершённого задания и его рабочее пространство.
     * Вызывается владельцем ПОСЛЕ того, как он забрал result/receipt и
     * нужные выходы. Таблица заданий конечна: если владелец не освобождает,
     * координатор переиспользует самые старые терминальные записи сам и
     * считает это в stats.reclaimed — но тогда их receipt уже не получить.
     * Явное освобождение всегда лучше молчаливого вытеснения. */
    int (*release)(plat_sb_coordinator_t *c, const char *job_id);
} plat_sb_analysis_v1_t;

/* Создание/уничтожение координатора. Отдельно от vtable: vtable публикуется
 * как capability, а экземпляр принадлежит доменному модулю. */
plat_sb_coordinator_t *plat_sb_coordinator_create(const plat_sb_quota_t *q);
void plat_sb_coordinator_destroy(plat_sb_coordinator_t *c);
const plat_sb_analysis_v1_t *plat_sb_analysis_v1(void);

/* Наблюдаемость admission: сколько отклонено и почему. Без этих чисел
 * «квоты защищают основной путь» — утверждение, которое нечем проверить. */
typedef struct plat_sb_coord_stats {
    uint32_t submitted, admitted, rejected, sealed;
    uint32_t rejected_quota, rejected_queue, rejected_invalid;
    uint32_t setup_failed, containment_failed, cleanup_failed;
    uint32_t dedup_hits;
    uint32_t released, reclaimed;
    uint32_t running_now, queued_now;
} plat_sb_coord_stats_t;

int plat_sb_coordinator_stats(plat_sb_coordinator_t *c,
                              plat_sb_coord_stats_t *out);
include/platx/services/sandbox_provider_v1.h
/* platx/services/sandbox_provider_v1.h — sandbox.provider:v1.
 *
 * Провайдер доказывает КОНКРЕТНЫЙ execution envelope. Он не решает, можно ли
 * запускать задание (это Policy/admission), не хранит расследования и не
 * является вторым supervisor'ом.
 *
 * ПОЧЕМУ prepare И run РАЗДЕЛЕНЫ.
 * Между ними лежит единственное место, где внешняя сторона может проверить
 * ограничения: workload ещё не выполнил ни одной своей инструкции, а все
 * ограничения уже установлены. Объединить их — значит вернуть «READY по факту
 * существования PID». Поэтому run() обязан отказать, если verify_ready() не
 * дал ready=1 для того же job generation.
 *
 * Порядок: probe → prepare → verify_ready → run → wait → teardown → destroy.
 * cancel() допустим в любом состоянии после prepare.
 */
#define PLAT_SB_PROVIDER_V1  0x00010000u

/* Непрозрачный дескриптор задания у провайдера. Владелец — провайдер. */
typedef struct plat_sb_job plat_sb_job_t;

/* Что этот хост РЕАЛЬНО даёт. Проверяется вызовом ядра, а не по имени ОС.
 * Именно отсутствие такой проверки превращает «профиль требует X» в
 * «профиль называет X». */
typedef struct plat_sb_probe {
    plat_desc_hdr_t hdr;
    uint32_t supported_mech;      /* биты PLAT_SB_MECH_*, проверенные живьём */
    uint32_t boundary_caps;       /* 1<<plat_sb_boundary_t                   */
    uint32_t landlock_abi;
    uint32_t seccomp_available;
    uint32_t userns_unpriv;
    uint32_t cgroup2_delegated;   /* можно ли создать свой узел cgroup       */
    char     host_os[32];
    char     host_release[64];
    char     detail[PLAT_SB_REASON_MAX];
    uint32_t platform_flags;      /* PLAT_SB_PF_*, что хост даёт живьём      */
    uint32_t budget_enforceable;  /* PLAT_SB_BUDGET_*                        */
    uint8_t  reserved[24];
} plat_sb_probe_t;

#define PLAT_SB_PROBE_VERSION 1u

typedef struct plat_sb_provider_v1 {
    uint32_t    struct_size;   /* sizeof; чужая раскладка — не вызывать      */
    uint32_t    version;       /* PLAT_SB_PROVIDER_V1                        */
    const char *id;            /* "linux.process.v1" | "windows.process.v1"  */
    uint32_t    boundary_caps; /* объявленное; probe() говорит о реальном    */

    /* Живая проверка возможностей хоста. Обязателен. */
    int (*probe)(plat_sb_probe_t *out);

    /* Создать задание и УСТАНОВИТЬ все ограничения, остановившись перед
     * первой инструкцией workload. Возвращает 0 и *job_out при успехе;
     * иначе -plat_sb_reason_t и *job_out == NULL. */
    int (*prepare)(const plat_sb_profile_t *profile,
                   const plat_sb_workload_t *workload,
                   plat_sb_job_t **job_out);

    /* Независимая проверка установленного. Провайдер обязан наблюдать факты
     * СНАРУЖИ исполнителя там, где ОС это позволяет, и честно помечать
     * TRUSTED_STUB там, где не позволяет. Идемпотентна. */
    int (*verify_ready)(plat_sb_job_t *job, plat_sb_readiness_t *out);

    /* Отпустить gate и начать исполнение. Отказ, если readiness не получен
     * или получен для другого generation/профиля. */
    int (*run)(plat_sb_job_t *job);

    /* Ждать завершения до deadline_ns (CLOCK_MONOTONIC, 0 = из бюджета). */
    int (*wait)(plat_sb_job_t *job, uint64_t deadline_ns,
                plat_sb_exec_result_t *out);

    /* Запрос отмены. Отмена — НЕ то же самое, что exit исполнителя. */
    int (*cancel)(plat_sb_job_t *job);

    /* Завершить всё дерево, снять ресурсы и переписать остатки. */
    int (*teardown)(plat_sb_job_t *job, plat_sb_census_t *out);

    void (*destroy)(plat_sb_job_t *job);

    /* Идентичность задания для receipt/логов. */
    uint64_t (*job_generation)(const plat_sb_job_t *job);
    int      (*job_root_pid)(const plat_sb_job_t *job);
} plat_sb_provider_v1_t;

#define PLAT_SB_PROVIDER_V1_BASE_SIZE \
    (offsetof(plat_sb_provider_v1_t, job_root_pid) + \
     sizeof(((plat_sb_provider_v1_t *)0)->job_root_pid))

/* Реестр провайдеров. Он маленький и статический: динамическая регистрация
 * чужого кода как «границы изоляции» — отдельное решение владельца. */
const plat_sb_provider_v1_t *plat_sb_provider_find(plat_sb_boundary_t b);
const plat_sb_provider_v1_t *plat_sb_provider_by_id(const char *id);
uint32_t plat_sb_provider_count(void);
const plat_sb_provider_v1_t *plat_sb_provider_at(uint32_t idx);
include/platx/services/sandbox_v1.h
/* platx/services/sandbox_v1.h — общие типы PXSANDBOX. Публичный контракт.
 *
 * Здесь нет реализации и нет include внутренних заголовков src/sandbox.
 * Заголовок описывает ТРИ РАЗДЕЛЬНО ОЦЕНИВАЕМЫХ свойства (ТЗ §1):
 *   containment  — какие ограничения РЕАЛЬНО установлены;
 *   observation  — что наблюдалось и чем;
 *   analysis     — какой вывод и на каком основании.
 * Ни одно из них не выводится из другого. Поэтому в результате три
 * независимых поля, а не один «уровень доверия».
 *
 * ГЛАВНОЕ ПРАВИЛО ФАЙЛА: источник сведения всегда указан рядом со сведением.
 * `plat_sb_attest_t` отделяет наблюдение доверенной стороной снаружи от
 * самоотчёта. Слово READY без источника подтверждения запрещено: именно так
 * study/sandbox сообщал FULL isolation, не установив namespaces
 * (см. study/sandbox/sandbox_isolation.c, ветка FULL).
 */
/* ── идентичность capability ───────────────────────────────────────────── */
#define PLAT_NS_SANDBOX             "sandbox"
#define PLAT_NAME_SANDBOX_PROVIDER  "sandbox.provider"
#define PLAT_NAME_SANDBOX_ANALYSIS  "sandbox.analysis"
#define PLAT_NAME_SANDBOX_TASK      "sandbox.task"
#define PLAT_NAME_SANDBOX_ARTIFACT  "sandbox.artifact"

#define PLAT_CAP_SANDBOX_PROVIDER   PLAT_NAME_SANDBOX_PROVIDER
#define PLAT_CAP_SANDBOX_ANALYSIS   PLAT_NAME_SANDBOX_ANALYSIS
#define PLAT_CAP_SANDBOX_TASK       PLAT_NAME_SANDBOX_TASK

#define PLAT_SB_V1                  0x00010000u

/* ── ограничения размеров (все буферы фиксированы, malloc в горячем пути нет) */
#define PLAT_SB_ID_MAX        64u   /* job id, profile id, provider id      */
#define PLAT_SB_PATH_MAX     512u
#define PLAT_SB_ARGS_MAX      32u
#define PLAT_SB_ENV_MAX       32u
#define PLAT_SB_FD_KEEP_MAX   16u
#define PLAT_SB_RULES_MAX     32u   /* правил доступа к ФС на профиль       */
#define PLAT_SB_DIGEST_LEN    32u   /* SHA-256                              */
#define PLAT_SB_REASON_MAX   192u

/* ── уровень границы (ТЗ §5): ось, независимая от purpose ──────────────── */
typedef enum plat_sb_boundary {
    PLAT_SB_BOUND_NONE              = 0,
    PLAT_SB_BOUND_OBSERVE           = 1,  /* без гарантии containment       */
    PLAT_SB_BOUND_PROCESS_ENFORCED  = 2,
    PLAT_SB_BOUND_VM_DISPOSABLE     = 3
} plat_sb_boundary_t;

/* ── назначение исполнения (ТЗ §5.1) ───────────────────────────────────── */
typedef enum plat_sb_purpose {
    PLAT_SB_PURPOSE_ANALYSIS   = 0,  /* недоверенный образец               */
    PLAT_SB_PURPOSE_PLATX_TASK = 1   /* штатная работа PLATX               */
} plat_sb_purpose_t;

typedef enum plat_sb_secprofile {
    PLAT_SB_SEC_BASELINE = 0,
    PLAT_SB_SEC_HARDENED = 1
} plat_sb_secprofile_t;

/* Единица изоляции (ТЗ §5.1). Общий пул tenant'ов допустимым default не
 * является, поэтому JOB стоит первым и является значением по умолчанию. */
typedef enum plat_sb_unit {
    PLAT_SB_UNIT_JOB             = 0,
    PLAT_SB_UNIT_MODULE_INSTANCE = 1
} plat_sb_unit_t;

/* ── режим сети (ТЗ §8) ────────────────────────────────────────────────── */
typedef enum plat_sb_net {
    PLAT_SB_NET_DENY              = 0,
    PLAT_SB_NET_SIMULATED         = 1,
    PLAT_SB_NET_CONTROLLED_EGRESS = 2
} plat_sb_net_t;

/* ── механизмы ограничения ─────────────────────────────────────────────────
 * Один бит = одно ПРОВЕРЯЕМОЕ свойство, а не название подсистемы ОС.
 * Профиль перечисляет обязательные биты; недостающий обязательный бит —
 * отказ до исполнения, а не понижение профиля.
 *
 * Имена битов исторически линуксовые (они — словарь receipt'ов и улик, и
 * менять его значит обесценить уже собранные). СМЫСЛ каждого бита — свойство,
 * которое на каждой ОС доказывает свой механизм (ТЗ §22, матрица паритета):
 *
 *   бит           свойство                         linux.process.v1   windows.process.v1
 *   USERNS        отдельная identity, без прав     user ns + capset 0 AppContainer SID
 *   MOUNTNS       приватное пространство объектов  mount ns           AC named-object ns
 *   PIDNS         дерево процессов не видит хост   pid ns             Job + UILIMIT + AC
 *   NETNS         сети нет                         net ns             AC без net-capabilities
 *   IPCNS         host IPC недостижим              ipc ns             AC ns + UILIMIT
 *   UTSNS         имя хоста скрыто                 uts ns             — (нет механизма)
 *   CGROUPNS      иерархия ресурсов скрыта         cgroup ns          — (нет механизма)
 *   NNP           привилегии не растут через exec  no_new_privs       токен без привилегий, дети — AC
 *   SECCOMP       сокращение поверхности ядра      seccomp deny-all   mitigation policies
 *   LANDLOCK      правила путей ФС                 landlock           ACE для SID AC на путях правил
 *   RLIMIT        бюджеты процесса                 rlimit             Job limits (см. budget_enforced)
 *   CGROUP        бюджеты дерева                   cgroup v2          Job memory limit
 *   FD_ALLOWLIST  наследуются только названные     close_range        PROC_THREAD_ATTRIBUTE_HANDLE_LIST
 *   IDENTITY      полномочия обнулены              capset 0 + NOROOT  Low IL + 0 привилегий
 *   ROOTDIR       вне правил ФС ничего нет         pivot_root + ro    LPAC (deny-by-default)
 *   CWD           рабочий каталог задан            chdir              PEB.CurrentDirectory
 *   ENVCLEAR      окружение = ровно envp           execve(envp)       PEB.Environment (наблюдаемо!)
 *   TREEKILL      дерево убивается целиком         kill init pid-ns   TerminateJobObject + перепись
 */
#define PLAT_SB_MECH_USERNS        (1u <<  0)
#define PLAT_SB_MECH_MOUNTNS       (1u <<  1)
#define PLAT_SB_MECH_PIDNS         (1u <<  2)
#define PLAT_SB_MECH_NETNS         (1u <<  3)
#define PLAT_SB_MECH_IPCNS         (1u <<  4)
#define PLAT_SB_MECH_UTSNS         (1u <<  5)
#define PLAT_SB_MECH_CGROUPNS      (1u <<  6)
#define PLAT_SB_MECH_NNP           (1u <<  7)  /* no_new_privs               */
#define PLAT_SB_MECH_SECCOMP       (1u <<  8)  /* deny-by-default фильтр     */
#define PLAT_SB_MECH_LANDLOCK      (1u <<  9)
#define PLAT_SB_MECH_RLIMIT        (1u << 10)
#define PLAT_SB_MECH_CGROUP        (1u << 11)  /* лимиты cgroup v2           */
#define PLAT_SB_MECH_FD_ALLOWLIST  (1u << 12)
#define PLAT_SB_MECH_IDENTITY      (1u << 13)  /* выделенная identity        */
#define PLAT_SB_MECH_ROOTDIR       (1u << 14)  /* отдельный корень ФС        */
#define PLAT_SB_MECH_CWD           (1u << 15)
#define PLAT_SB_MECH_ENVCLEAR      (1u << 16)
#define PLAT_SB_MECH_TREEKILL      (1u << 17)  /* доказанное убийство дерева */
#define PLAT_SB_MECH__COUNT        18u
#define PLAT_SB_MECH_ALL           ((1u << PLAT_SB_MECH__COUNT) - 1u)

/* ── источник подтверждения ─────────────────────────────────────────────────
 * Порядок значений = порядок доверия по возрастанию; сравнение `>=`
 * осмысленно и используется в проверке готовности.
 *
 * HOST_OBSERVED — доверенная сторона СНАРУЖИ наблюдала факт независимо от
 *   исполнителя (для Linux-провайдера это /proc/<pid>/{ns,status,limits,
 *   cgroup,fd,root,cwd,environ}, прочитанные родителем).
 * TRUSTED_STUB  — факт сообщил платформенный pre-exec код ДО execve образа.
 *   Он ещё не является недоверенным workload'ом, но его слово — не
 *   независимое наблюдение. Так помечается landlock: у ядра 6.8 нет
 *   внешнего показателя, читаемого из /proc.
 * GUEST_AGENT   — сведения изнутри уже запущенного workload. Никогда не
 *   удовлетворяют требованию readiness. */
typedef enum plat_sb_attest {
    PLAT_SB_ATTEST_NONE          = 0,
    PLAT_SB_ATTEST_GUEST_AGENT   = 1,
    PLAT_SB_ATTEST_TRUSTED_STUB  = 2,
    PLAT_SB_ATTEST_HOST_OBSERVED = 3
} plat_sb_attest_t;

/* ── коды причин отказа ────────────────────────────────────────────────────
 * Причина всегда сопровождает отказ: «нет провайдера» и «провайдер есть, но
 * механизм не установился» — разные события для владельца. */
typedef enum plat_sb_reason {
    PLAT_SB_OK                    = 0,
    PLAT_SB_E_INVAL               = 1,  /* запрос не прошёл валидацию       */
    PLAT_SB_E_NO_PROVIDER         = 2,  /* нет провайдера под boundary/OS   */
    PLAT_SB_E_UNSUPPORTED_MECH    = 3,  /* хост не даёт обязательный механизм */
    PLAT_SB_E_SETUP_FAILED        = 4,  /* установка ограничения не удалась */
    PLAT_SB_E_READINESS_UNPROVEN  = 5,  /* нет подтверждения нужного уровня */
    PLAT_SB_E_ADMISSION           = 6,  /* квоты/очередь/резерв             */
    PLAT_SB_E_TIMEOUT             = 7,
    PLAT_SB_E_CANCELLED           = 8,
    PLAT_SB_E_CONTAINMENT         = 9,  /* граница нарушена или не доказана */
    PLAT_SB_E_CLEANUP             = 10, /* остатки после teardown           */
    PLAT_SB_E_STATE               = 11, /* переход состояния запрещён       */
    PLAT_SB_E_GENERATION          = 12, /* stale generation / чужой job     */
    PLAT_SB_E_QUOTA               = 13,
    PLAT_SB_E_DIGEST              = 14, /* input/output digest не совпал    */
    PLAT_SB_E_INTERNAL            = 15
} plat_sb_reason_t;

const char *plat_sb_reason_name(plat_sb_reason_t r);
const char *plat_sb_mech_name(uint32_t single_bit);
const char *plat_sb_boundary_name(plat_sb_boundary_t b);
const char *plat_sb_attest_name(plat_sb_attest_t a);

/* Разложить маску в «a|b|c» для receipt/логов. Всегда NUL-терминирует.
 * Возвращает число байт, которые ПОТРЕБОВАЛИСЬ БЫ (как snprintf). */
size_t plat_sb_mech_mask_str(uint32_t mask, char *out, size_t cap);

/* ── правило доступа к файловой системе ───────────────────────────────────
 * Пути — только абсолютные, без "..", разрешаются провайдером до применения.
 * Это НЕ shell-команда и не произвольный host path из запроса вызывающего:
 * координатор принимает лишь пути, разрешённые профилем. */
#define PLAT_SB_FS_READ    (1u << 0)
#define PLAT_SB_FS_WRITE   (1u << 1)
#define PLAT_SB_FS_EXEC    (1u << 2)

typedef struct plat_sb_fs_rule {
    char     path[PLAT_SB_PATH_MAX];
    uint32_t access;   /* PLAT_SB_FS_*                                     */
} plat_sb_fs_rule_t;

/* ── бюджеты ─────────────────────────────────────────────────────────────── */
typedef struct plat_sb_budget {
    uint32_t wall_ms;        /* 0 = запрещено: профиль обязан задать предел */
    uint32_t cpu_ms;
    uint64_t rss_bytes;
    uint32_t max_procs;
    uint64_t fsize_bytes;    /* максимальный размер создаваемого файла      */
    uint32_t max_fds;
    uint32_t max_output_bytes;
} plat_sb_budget_t;

/* ── закреплённый снимок правил (ТЗ PXSIG §4, SB-18) ──────────────────────
 * Прогон анализа обязан знать, КАКИЕ правила действовали. Не «последние», а
 * конкретное поколение с конкретным digest: иначе повторить разбор находки
 * через месяц нечем. Идентичность приходит от PXSIG; sandbox её только
 * закрепляет и переносит в receipt, своей базы правил не заводя. */
typedef struct plat_sb_rule_snapshot {
    uint32_t have;
    uint32_t domain;        /* домен потребителя у PXSIG                   */
    uint64_t generation;
    uint8_t  digest[PLAT_SB_DIGEST_LEN];
} plat_sb_rule_snapshot_t;

/* ── профиль исполнения ────────────────────────────────────────────────────
 * Профиль — это ЗАКРЕПЛЁННЫЙ документ: у него есть digest, и readiness
 * привязывается к нему. Изменение профиля меняет digest и делает старые
 * receipt'ы неприменимыми — это и есть смысл поля. */
typedef struct plat_sb_profile {
    plat_desc_hdr_t      hdr;
    char                 id[PLAT_SB_ID_MAX];
    plat_sb_boundary_t   boundary;
    plat_sb_purpose_t    purpose;
    plat_sb_secprofile_t security;
    plat_sb_unit_t       unit;
    plat_sb_net_t        net;

    uint32_t             required_mech;  /* обязательно; иначе отказ        */
    uint32_t             optional_mech;  /* отсутствие → PARTIAL coverage   */

    /* Минимальный уровень подтверждения для КАЖДОГО обязательного механизма.
     * PROCESS_ENFORCED-профили обязаны требовать не ниже TRUSTED_STUB и
     * никогда не принимают GUEST_AGENT. */
    plat_sb_attest_t     min_attest;

    plat_sb_budget_t     budget;

    plat_sb_fs_rule_t    fs[PLAT_SB_RULES_MAX];
    uint32_t             n_fs;

    /* Идентичность внутри границы. host_uid_distinct=1 требует отдельного
     * host-uid и без привилегий недостижим — тогда отказ, а не «почти». */
    uint32_t             inner_uid;
    uint32_t             inner_gid;
    uint32_t             host_uid_distinct;

    /* Требовать закреплённый снимок правил. Для профиля анализа образца это
     * норма: результат без указания действовавших правил невоспроизводим. */
    uint32_t             require_rule_snapshot;

    uint8_t              digest[PLAT_SB_DIGEST_LEN]; /* заполняет валидатор */
    uint8_t              reserved[32];
} plat_sb_profile_t;

#define PLAT_SB_PROFILE_VERSION  1u

/* ── описание работы ────────────────────────────────────────────────────────
 * Только ссылки: путь к образу и аргументы, проверенные вызывающим доменом.
 * Никаких shell-строк, function pointer и raw pointer между процессами. */
typedef struct plat_sb_workload {
    plat_desc_hdr_t hdr;
    char     image_path[PLAT_SB_PATH_MAX];
    uint8_t  image_digest[PLAT_SB_DIGEST_LEN];
    uint32_t have_image_digest;
    char     argv[PLAT_SB_ARGS_MAX][PLAT_SB_PATH_MAX];
    uint32_t argc;
    char     envp[PLAT_SB_ENV_MAX][PLAT_SB_PATH_MAX];
    uint32_t envc;                    /* ENVCLEAR: наследования нет вообще  */
    int      keep_fds[PLAT_SB_FD_KEEP_MAX];
    uint32_t n_keep_fds;
    char     workdir[PLAT_SB_PATH_MAX];   /* рабочее пространство job       */
    uint8_t  reserved[32];
} plat_sb_workload_t;

#define PLAT_SB_WORKLOAD_VERSION  1u

/* ── доказательство готовности ────────────────────────────────────────────
 * required — из профиля; applied — что провайдер УТВЕРЖДАЕТ, что установил;
 * attest[i] — чем это подтверждено для механизма с битом i.
 * Готовность = для каждого обязательного бита attest >= profile->min_attest.
 * PID существует — не подтверждение (ТЗ §7). */
typedef struct plat_sb_readiness {
    plat_desc_hdr_t  hdr;
    uint32_t         required_mech;
    uint32_t         applied_mech;
    uint32_t         verified_mech;   /* attest >= min_attest               */
    uint8_t          attest[PLAT_SB_MECH__COUNT];  /* plat_sb_attest_t      */
    uint32_t         landlock_abi;    /* 0 = нет                            */
    uint32_t         seccomp_mode;    /* из /proc/<pid>/status              */
    uint32_t         seccomp_filters;
    uint32_t         n_open_fds;      /* наблюдено снаружи                  */
    uint32_t         inner_uid_seen;
    uint32_t         inner_gid_seen;
    uint64_t         ns_inode[8];     /* user,mnt,pid,net,ipc,uts,cgroup,-  */
    uint64_t         host_ns_inode[8];
    /* Отрицательные пробы, выполненные доверенным stub ДО execve образа.
     * Каждая — попытка сделать запрещённое; ожидается отказ. */
    uint32_t         probes_run;
    uint32_t         probes_denied_as_expected;
    uint8_t          profile_digest[PLAT_SB_DIGEST_LEN];
    uint64_t         job_generation;
    int              ready;           /* 1 только если всё выше сошлось     */
    plat_sb_reason_t reason;
    char             reason_text[PLAT_SB_REASON_MAX];

    /* Идентичность границы для НЕЗАВИСИМОЙ переписи (ТЗ §22). Смысл задаёт
     * провайдер: linux.process.v1 — иноды pid- и net-namespace;
     * windows.process.v1 — host pid и generation, из которых строится имя
     * Job Object. Инструмент переписи открывает границу САМ по этой
     * идентичности, а не через дескриптор провайдера: иначе «остатков нет»
     * означало бы «провайдер так сказал». */
    uint64_t         boundary_id[2];

    /* Какие поля бюджета ОС реально ограничивает (PLAT_SB_BUDGET_*). Linux
     * — все; Windows не имеет предела на число handle'ов, а предел размера
     * файла держит хост-сторожем с зерном опроса. Это пишется в receipt, а не
     * прячется в «rlimit: host_observed». */
    uint32_t         budget_enforced;

    /* Платформенные показатели, наблюдённые СНАРУЖИ (PLAT_SB_PF_* для
     * windows.process.v1; Linux пишет 0 — его показатели выше:
     * landlock_abi / seccomp_*). */
    uint32_t         platform_flags;
    char             platform_detail[128];
    uint8_t          reserved[32];
} plat_sb_readiness_t;

#define PLAT_SB_READINESS_VERSION 1u

/* Поля бюджета для readiness.budget_enforced. */
#define PLAT_SB_BUDGET_WALL   (1u << 0)
#define PLAT_SB_BUDGET_CPU    (1u << 1)
#define PLAT_SB_BUDGET_RSS    (1u << 2)
#define PLAT_SB_BUDGET_PROCS  (1u << 3)
#define PLAT_SB_BUDGET_FSIZE  (1u << 4)
#define PLAT_SB_BUDGET_FDS    (1u << 5)
#define PLAT_SB_BUDGET_ALL    0x3fu

/* Платформенные флаги провайдера windows.process.v1. Каждый — факт, который
 * родитель прочитал у ОС о приостановленном процессе (токен, Job, mitigation
 * policy, PEB), а не то, что он попросил при создании. */
#define PLAT_SB_PF_APPCONTAINER      (1u << 0)  /* TokenIsAppContainer          */
#define PLAT_SB_PF_LPAC              (1u << 1)  /* WIN://NOALLAPPPKG            */
#define PLAT_SB_PF_JOB_MEMBER        (1u << 2)  /* IsProcessInJob               */
#define PLAT_SB_PF_JOB_KILL_ON_CLOSE (1u << 3)
#define PLAT_SB_PF_JOB_NO_BREAKAWAY  (1u << 4)
#define PLAT_SB_PF_NO_CHILD_PROC     (1u << 5)  /* ChildProcessPolicy           */
#define PLAT_SB_PF_DYNCODE_DENIED    (1u << 6)
#define PLAT_SB_PF_EXTPOINT_DISABLED (1u << 7)
#define PLAT_SB_PF_IMAGELOAD_LIMITED (1u << 8)
#define PLAT_SB_PF_STRICT_HANDLES    (1u << 9)
#define PLAT_SB_PF_WIN32K_LOCKOUT    (1u << 10)
#define PLAT_SB_PF_INTEGRITY_LOW     (1u << 11)
#define PLAT_SB_PF_NO_PRIVILEGES     (1u << 12)
#define PLAT_SB_PF_NO_NET_CAPS       (1u << 13)
#define PLAT_SB_PF_ENV_OBSERVED      (1u << 14) /* PEB.Environment прочитан     */
#define PLAT_SB_PF_CWD_OBSERVED      (1u << 15)
#define PLAT_SB_PF_ACL_OBSERVED      (1u << 16) /* ACE на путях правил прочитаны*/
#define PLAT_SB_PF_UI_RESTRICTED     (1u << 17) /* JOBOBJECT_BASIC_UI_RESTRICTIONS */
#define PLAT_SB_PF__COUNT            18u

const char *plat_sb_pf_name(uint32_t single_bit);
size_t plat_sb_pf_mask_str(uint32_t mask, char *out, size_t cap);
size_t plat_sb_budget_mask_str(uint32_t mask, char *out, size_t cap);

/* ── результат исполнения (ТЗ §9): три оси, не одна ───────────────────── */
typedef enum plat_sb_exec_outcome {
    PLAT_SB_EXEC_COMPLETED    = 0,
    PLAT_SB_EXEC_CRASHED      = 1,
    PLAT_SB_EXEC_TIMED_OUT    = 2,
    PLAT_SB_EXEC_CANCELLED    = 3,
    PLAT_SB_EXEC_SETUP_FAILED = 4,
    PLAT_SB_EXEC_NOT_RUN      = 5
} plat_sb_exec_outcome_t;

typedef enum plat_sb_containment {
    PLAT_SB_CONTAIN_UNVERIFIED = 0,   /* значение по умолчанию — намеренно  */
    PLAT_SB_CONTAIN_ENFORCED   = 1,
    PLAT_SB_CONTAIN_VIOLATED   = 2
} plat_sb_containment_t;

typedef enum plat_sb_coverage {
    PLAT_SB_COV_UNAVAILABLE = 0,
    PLAT_SB_COV_PARTIAL     = 1,
    PLAT_SB_COV_COMPLETE    = 2        /* complete-for-profile, не «полное» */
} plat_sb_coverage_t;

typedef enum plat_sb_cleanup {
    PLAT_SB_CLEAN_UNVERIFIED = 0,
    PLAT_SB_CLEAN_VERIFIED   = 1,
    PLAT_SB_CLEAN_FAILED     = 2
} plat_sb_cleanup_t;

/* Перепись остатков после teardown. Отрицательный census блокирует
 * переиспользование исполнителя (ТЗ SB-06). */
typedef struct plat_sb_census {
    plat_desc_hdr_t hdr;
    uint32_t leftover_pids;      /* процессы в границе job после teardown   */
    uint32_t leftover_ns;        /* объекты границы, ещё существующие
                                    (Linux: namespace; Windows: Job)        */
    uint32_t leftover_mounts;    /* Linux: bind-точки под workdir;
                                    Windows: ACE для SID задания, которые не
                                    удалось снять с путей правил           */
    uint32_t leftover_fds;
    uint32_t leftover_workdir_bytes;
    uint32_t killed_pids;        /* сколько убито при teardown              */
    uint64_t teardown_ms;
    plat_sb_cleanup_t verdict;
    char     detail[PLAT_SB_REASON_MAX];
    uint8_t  reserved[32];
} plat_sb_census_t;

#define PLAT_SB_CENSUS_VERSION 1u

typedef struct plat_sb_exec_result {
    plat_desc_hdr_t        hdr;
    plat_sb_exec_outcome_t outcome;
    int                    exit_code;    /* значимо при COMPLETED           */
    int                    term_signal;  /* значимо при CRASHED             */
    uint64_t               wall_ms;
    uint64_t               cpu_ms;
    uint64_t               max_rss_kb;
    uint32_t               observed_procs;
    plat_sb_reason_t       reason;
    char                   reason_text[PLAT_SB_REASON_MAX];
    uint8_t                reserved[32];
} plat_sb_exec_result_t;

#define PLAT_SB_EXEC_RESULT_VERSION 1u

/* Итог задания. Сохраняет разделение трёх свойств: успешный exit кода 0
 * не превращает containment в ENFORCED, а отсутствие детекта — в SAFE. */
typedef struct plat_sb_job_result {
    plat_desc_hdr_t        hdr;
    char                   job_id[PLAT_SB_ID_MAX];
    uint64_t               generation;
    char                   provider_id[PLAT_SB_ID_MAX];
    uint8_t                profile_digest[PLAT_SB_DIGEST_LEN];
    plat_sb_exec_result_t  exec;
    plat_sb_containment_t  containment;
    plat_sb_coverage_t     coverage;
    plat_sb_census_t       census;
    plat_sb_readiness_t    readiness;
    plat_sb_rule_snapshot_t rules;   /* что действовало в этом прогоне     */
    uint8_t                reserved[32];
} plat_sb_job_result_t;

#define PLAT_SB_JOB_RESULT_VERSION 1u

/* ── состояния задания (ТЗ §7) ─────────────────────────────────────────── */
typedef enum plat_sb_state {
    PLAT_SB_ST_SUBMITTED      = 0,
    PLAT_SB_ST_ADMITTED       = 1,
    PLAT_SB_ST_PREPARING      = 2,
    PLAT_SB_ST_VERIFIED_READY = 3,
    PLAT_SB_ST_RUNNING        = 4,
    PLAT_SB_ST_STOPPING       = 5,
    PLAT_SB_ST_COLLECTING     = 6,
    PLAT_SB_ST_CLEANING       = 7,
    PLAT_SB_ST_SEALED         = 8,
    PLAT_SB_ST_REJECTED       = 9,
    PLAT_SB_ST_SETUP_FAILED   = 10,
    PLAT_SB_ST_FAILED_CONTAINMENT = 11,
    PLAT_SB_ST_CLEANUP_FAILED = 12
} plat_sb_state_t;

const char *plat_sb_state_name(plat_sb_state_t s);

/* Разрешён ли переход. Единственный источник истины о графе состояний:
 * координатор не держит второй список переходов. */
int plat_sb_state_transition_ok(plat_sb_state_t from, plat_sb_state_t to);
44

seccomp

Построение и применение политики системных вызовов
src/seccomp/Доверие и защита22 файлов1 API headers

Построение и применение политики системных вызовов

Граница ответственности

  • Policy не выдаёт authorization бизнес-операций; только syscall boundary.
  • Module descriptor/profile задаёт minimum policy; child не может ослабить её.
  • XIM notify rules отделены от simple allow/deny, но используют общий compiler/schema.

Устройство подсистемы

  • Policy model содержит architecture, default action, syscall rules, argument predicates, notify set и version.
  • Compiler нормализует names/numbers, проверяет unreachable/conflicts и генерирует BPF program с size limits.
  • Apply sequence устанавливает no_new_privs, проверяет listener requirements и загружает filter до exec/READY.
  • Status records hash/version/mode/kernel features, не полный secret config.

Поток работы

  • Descriptor/profile policy ref → load/verify.
  • Compile + dry-run explanation.
  • Child bootstrap apply.
  • Violation/notify → kernel action/XIM event.

Отказ и восстановление

  • Unknown syscall/arch/oversized program reject.
  • Apply failure before exec terminates child; no unsandboxed fallback.
  • Policy reload для running process невозможен произвольно; new generation/restart.

Основные возможности

  • Builds syscall filtering policy.
  • Feeds child isolation and XIM mediation paths.
  • Supports profile-specific restrictions.
Архитектурные детали и инварианты

Обзор

BPF-фильтр генератор и три профиля: CIVIL / MIL / MINIMAL.

Генератор (seccomp_gen.c)

Каждое правило → 2 инструкции. Первое совпадение побеждает.

Профили

Профиль | Syscall'ов | Описание

CIVIL | ~150 | Полный набор для EDR-агента

MIL | ~55 | Только I/O + BPF + сигналы

MINIMAL | 5 | exit/exit_group/rt_sigreturn/write/clock_gettime

Тесты (TAP)

- t_seccomp_gen: 6 — NULL args, overflow, instruction count

- t_seccomp_block: 2 — fork + forbidden syscall → WIFSIGNALED + SIGSYS

- t_seccomp_civil: 2 — CIVIL не падает, read/write работают

Управление и диагностика

Корневые команды: seccomp. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / seccomp →
Состав подсистемы / 22 файлов
Файл / компонентНазначение и граница
src/seccomp/cmd_seccomp.cnamespace "seccomp": изоляция процессов через Seccomp User Notification (SECCOMP_RET_USER_NOTIF). Маршрутизирует команды вида seccomp run --cmd= [--policy=yaml] [--output=log] seccomp intercept --cmd= --inject= [--policy=yaml] seccomp status --pid=
src/seccomp/dbg_seccomp.cслияние: seccomp.c (из inj/, актуальная версия)
src/seccomp/seccomp_apply.cdispatcher platx_seccomp_apply()
src/seccomp/seccomp_civil.cPLATX CIVIL mode seccomp filter
src/seccomp/seccomp_gen.cLayout: [load_arch][check_arch][load_nr][JEQ+RET]*nrules[default TRAP] Каждое правило → 2 инструкции: JEQ + RET с action из таблицы.
src/seccomp/seccomp_mil.cPLATX MIL mode seccomp filter (строгий) MIL: минимальный набор для EDR-сенсора в высокозащищённой среде. Нет fork/exec/socket после инициализации — только I/O + BPF + сигналы.
src/seccomp/seccomp_minimal.cPLATX MINIMAL mode seccomp filter MINIMAL: только exit/exit_group + rt_sigreturn — последний рубеж обороны. Применяется перед финальным lockdown; процесс может только умереть.
src/seccomp/seccomp_policy.cVersioned seccomp policy management. STAB-135: SECCOMP_POLICY_VERSION, xim_policy, dbg_policy, EXPERIMENTAL.
src/seccomp/seccomp_sigsys.cНаходка W5-F005: обработчика SIGSYS в дереве не было вовсе.
src/seccomp/seccomp_sigsys.hвозвращает SECCOMP_RET_TRAP, то есть ядро шлёт SIGSYS, но в дереве не было НИ ОДНОГО обработчика этого сигнала (24 вхождения строки "SIGSYS" — все комментарии и литералы). Из-за этого platx_sc_counters_get() отдавал seccomp_blocks закрыть было невозможно.
src/seccomp/tests/fuzz_seccomp_gen.cОракул (независимая модель, не переписанный генератор): 1) nrules==0 или max gen обязан вернуть -1; 2) иначе длина ровно 5+2N; 3) действие для nr = действие ПЕРВОГО правила с этим nr (иначе TRAP); 4) любая чужая arch -> TRAP;
src/seccomp/tests/probe_gen_malloc.cПрибор — LD_PRELOAD-счётчик malloc/calloc/realloc (probe_mcount.c). У прибора ОБЯЗАНЫ быть контрольные режимы, иначе ноль ничего не значит: idle — ничего не делаем, ждём 0; malloc1 — один malloc, ждём >=1 (прибор жив); gen — 3 профиля x 1000, ждём 0 (собственно проверка);
src/seccomp/tests/probe_mcount.cLD_PRELOAD счётчик malloc/calloc/realloc; итог в $MC_OUT.
src/seccomp/tests/sc_check.hобщий счётчик проверок для тестов seccomp (волна 5, A2).
src/seccomp/tests/sc_dump.cпечатает состав таблиц профилей ПО СКОМПИЛИРОВАННЫМ данным. Нужен как независимый путь проверки scripts/list_allowed_syscalls.sh,
src/seccomp/tests/sc_tables.cдоступ к статическим таблицам профилей БЕЗ правки их файлов. Приём: включаем .c профиля целиком в этот TU, static-таблица становится видимой изнутри. Файлы seccomp_civil.c/mil.c/minimal.c не меняются.
src/seccomp/tests/sc_vm.hминимальный интерпретатор seccomp-BPF для тестов A2 (волна 5). Нужен, чтобы проверять СОДЕРЖИМОЕ сгенерированного фильтра без root: прогоняем программу на паре (arch, nr) и получаем действие. Поддерживается ровно то подмножество, которое порождает seccomp_gen.c:
src/seccomp/tests/t_int_sp_seccomp.cПроверяется не «оба модуля существуют», а четыре свойства их совместной работы, каждое исполнением в отдельном ребёнке (фильтр необратим): S1 порядок: hardening ставит NO_NEW_PRIVS, после чего seccomp применяется; обратный порядок тоже обязан работать (фильтр сам
src/seccomp/tests/t_seccomp_block.cПроверка ИСПОЛНЕНИЕМ, а не чтением кода: дочерний процесс реально ставит фильтр через prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER) и делает запрещённый syscall; родитель ждёт именно SIGSYS. Обязательные контрольные режимы (урок прошлого прогона: измеритель без
src/seccomp/tests/t_seccomp_civil.cПроверяет СОДЕРЖИМОЕ сгенерированного фильтра интерпретатором sc_vm, а не факт того, что функция вернула не -1.
src/seccomp/tests/t_seccomp_mil.cПроверяется содержимое фильтра интерпретатором sc_vm, плюс свойство «MIL строже CIVIL» и «MINIMAL строже MIL» на общем словаре syscall.
src/seccomp/tests/t_seccomp_sigsys.cПроверяется ИСПОЛНЕНИЕМ: обработчик SIGSYS реально ловит блокированный фильтром вызов, счётчик blocks реально растёт, и platx_sc_counters_get() отдаёт этот счётчик, а не константу. Контрольные режимы обязательны (иначе ноль ничего не доказывает): K0 «обработчик стоит, фильтра нет» -> blocks == 0 (прибор не шумит);
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/platx_seccomp.h
/* platx_seccomp.h — PlatX Seccomp: публичный API (Wave 5, §5.31).
 *
 * Три профиля: CIVIL (стандартный), MIL (строгий), MINIMAL (минимальный).
 * Генератор BPF bytecode из статической таблицы (§5.18, §SEC-3).
 * §INV-SECCOMP-01: запрещённый syscall → SIGSYS (не silent ignore).
 *
 * SPDX-License-Identifier: GPL-2.0
 */

/* ── Профили ──────────────────────────────────────────────────────────── */
typedef enum {
    PLATX_SECCOMP_CIVIL   = 0,
    PLATX_SECCOMP_MIL     = 1,
    PLATX_SECCOMP_MINIMAL = 2
} platx_seccomp_profile_t;

/* ── Одно правило таблицы ─────────────────────────────────────────────── */
typedef struct {
    int      nr;      /* syscall number; -1 = sentinel */
    uint32_t action;  /* SECCOMP_RET_ALLOW / SECCOMP_RET_KILL_PROCESS */
} platx_sc_rule_t;

/* ── Генератор BPF bytecode из таблицы (§SEC-3: static output buffer) ── */
#define PLATX_SC_BPF_MAX  256  /* максимум инструкций */

/* platx_seccomp_gen — генерирует BPF prog из таблицы правил в out[max].
 * Возвращает число инструкций или -1. §SEC-3: output buffer статический. */
int platx_seccomp_gen(const platx_sc_rule_t *rules, size_t nrules,
                      struct sock_filter *out, size_t max);

/* platx_seccomp_apply — применяет BPF prog (prctl PR_SET_SECCOMP).
 * §INV-SECCOMP-01: default action = SECCOMP_RET_TRAP (→SIGSYS). */
int platx_seccomp_apply(platx_seccomp_profile_t profile);

/* Отдельный apply для каждого профиля */
int platx_seccomp_apply_civil(void);
int platx_seccomp_apply_mil(void);
int platx_seccomp_apply_minimal(void);

/* Счётчики */
typedef struct { uint64_t applied; uint64_t blocks; } platx_sc_counters_t;
void platx_sc_counters_get(platx_sc_counters_t *out);
45

secrets

Политика и жизненный цикл секретного материала
src/secrets/Доверие и защита7 файлов4 API headers

Secrets связывает операции над секретами с проверенной идентичностью, политикой и долговечным аудитом. Получатель, назначение, срок, ротация и отзыв рассматриваются в одном контракте. Защищённое размещение определяется профилем; отсутствие обязательного механизма приводит к отказу до выдачи материала.

Граница ответственности

  • Secrets связывает операции над секретами с проверенной идентичностью, политикой и долговечным аудитом. Получатель, назначение, срок, ротация и отзыв рассматриваются в одном контракте. Защищённое размещение определяется профилем; отсутствие обязательного механизма приводит к отказу до выдачи материала.

Устройство подсистемы

  • Vault описывает область хранения и связанный protector. Объект секрета имеет идентичность, тип и ревизию; его значение не передаётся аргументом командной строки.
  • Grant связывает объект, consumer, target и срок. Preview создаёт проверяемый план; apply использует идентичность этого плана.
  • Backup и restore разделяют создание, проверку контейнера, предварительный план и применение. Сертификат и его приватный материал имеют разные операции представления и размещения.

Поток работы

  • Проверить контекст и готовность protector, открыть vault.
  • Создать либо выбрать объект по идентификатору и ревизии.
  • Подготовить план выдачи, ротации, размещения сертификата или восстановления.
  • Применить разрешённый план, записать аудит, отозвать lease при завершении.

Отказ и восстановление

  • Непривязанный provider делает эффект недоступным даже при корректном синтаксисе.
  • Неправильная ревизия, срок либо target не дают использовать старый план.
  • Отказ долговечного аудита и защиты материала учитывается до выдачи чувствительного значения.

Управление и диагностика

В домене есть собственная справка или отдельный parser, но регистрация корневой команды через cmd_register не найдена. Ниже в справочнике сохранена его точная точка входа.

Справочник CLI / secrets →
Состав подсистемы / 7 файлов
Файл / компонентНазначение и граница
src/secrets/pxsecrets_store.cреализация хранилища PXSECRETS S1. pxs_object_get — не hot path, malloc разрешён. Схема DEK (без malloc): info = "pxdek\x00" || vault_id[16] || object_id[16] || version_be[8] DEK = HKDF-SHA256(prk=VRK, info=info, 32 bytes) × 4096 HMAC iterations — временная заглушка, НЕ Argon2id.
src/secrets/pxsecrets_store.hвнутренний контракт хранилища PXSECRETS S1. Только для src/secrets/. Не включать из-за пределов дерева. VRK — Vault Root Key, 256-bit random, завёрнут в protector DEK — Data Encryption Key, уникален на (vault_id, object_id, version) DEK = HKDF-SHA256(VRK, "pxdek" || vault_id || object_id || version_be)
src/secrets/secrets_cli.cРеализация secrets / cli
src/secrets/secrets_lease.cCalled only on a new, unpublished lease. Reinitializing a live lease would reset its budget and is outside this primitive's contract.
src/secrets/secrets_lease.hPrivate lease admission primitive. Policy/ingress constructs these bindings after authentication. Neither this struct nor its constructor is exposed in the public capability or accepted as a wire claim.
src/secrets/secrets_module.cPXSECRETS domain module, S1. S1 добавляет: pxs_store_init/destroy, durable_ready в status(). Authenticated ingress, Policy/Action и audit остаются prerequisites S2+. Базовая директория: PXS_DEFAULT_BASE_DIR (compile-time override) или /var/lib/platx/secrets. Конфигурируемый путь придёт через cfgkey в S2.
src/secrets/secretsctl_main.cStandalone contract inspection client. No local identity is fabricated and
Контракты API / 4 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/reason.h
/* reason.h — S517: восемь разных ответов, а не один «не получилось».
 *
 * ЗАЧЕМ РАЗЛИЧАТЬ
 * ───────────────
 * У оператора после отказа ровно один вопрос: что мне теперь делать. И на
 * него есть ВОСЕМЬ разных ответов, которые обычно сваливают в один.
 *
 *   ОТСУТСТВУЕТ   такого имени мы не знаем вовсе.
 *                 → проверить, что написано; это опечатка или чужая версия.
 *
 *   ВЫКЛЮЧЕНО     имя знаем, оно есть, его выключили.
 *                 → включить. Действие есть, и оно ваше.
 *
 *   НЕ ПОДДЕРЖИВАЕТСЯ  этот выпуск/профиль не умеет и не научится.
 *                 → другой профиль или другой выпуск. Ждать бессмысленно.
 *
 *   НЕДОСТУПНО    умеет, включено, но сейчас не может (нет ядра, нет прав,
 *                 нет соседа).
 *                 → чинить условие. Ждать ОСМЫСЛЕННО.
 *
 *   ОТКАЗАНО      могло бы, но политика запретила.
 *                 → менять политику или просить право. Не чинить.
 *
 *   ДЕГРАДИРОВАЛО работает хуже обещанного, но работает.
 *                 → можно продолжать, зная цену.
 *
 *   УПАЛО         пыталось и не смогло.
 *                 → смотреть причину, повторять осмысленно.
 *
 *   ПРОТУХЛО      было верно, перестало: поколение уехало, аренда истекла.
 *                 → перезапросить. Данные не потеряны, право — да.
 *
 * Разница между «не поддерживается» и «недоступно» — самая дорогая из всех:
 * в первом случае ожидание бессмысленно, во втором это единственно верное
 * действие. Свести их в одно «нет» значит либо заставить ждать вечно, либо
 * прогнать чинить то, что и не могло работать.
 *
 * ЧЕМ ЭТО НЕ ЯВЛЯЕТСЯ
 * ───────────────────
 * Это не новый механизм отказов. У attach свои коды, у gadget свои — они
 * остаются. Здесь только СЛОВАРЬ и полные отображения в него: каждый чужой
 * код обязан иметь ровно один ответ, и ни одно отображение не имеет права
 * склеить два разных смысла в один.
 */
typedef enum {
    PLAT_R_OK          = 0,
    PLAT_R_ABSENT      = 1,
    PLAT_R_DISABLED    = 2,
    PLAT_R_UNSUPPORTED = 3,
    PLAT_R_UNAVAILABLE = 4,
    PLAT_R_DENIED      = 5,
    PLAT_R_DEGRADED    = 6,
    PLAT_R_FAILED      = 7,
    PLAT_R_STALE       = 8,
    PLAT_R_COUNT       = 9
} plat_reason_t;

/* Устойчивое машинное имя. Никогда не NULL, неизвестное — "UNKNOWN". */
const char *plat_reason_id(plat_reason_t r);
/* Человеку — что это значит. */
const char *plat_reason_say(plat_reason_t r);
/* Человеку — ЧТО ДЕЛАТЬ. Отказ без следующего шага — половина отказа. */
const char *plat_reason_next(plat_reason_t r);

/* Имеет ли смысл ждать и повторять. Отличает «не поддерживается» от
 * «недоступно» одним ответом. */
int plat_reason_retry_makes_sense(plat_reason_t r);

/* Отображения чужих кодов. Полные: любой вход даёт ответ, и никогда OK
 * по умолчанию. */
plat_reason_t plat_reason_from_attach_deny(int deny_code);
plat_reason_t plat_reason_from_gadget(int gadget_status);
include/platx/secrets_cli.h
typedef int (*pxs_cli_write_fn)(void *context, const char *text, size_t len);
typedef struct pxs_cli_host {
    void *context;
    pxs_cli_write_fn write;
    const plat_secrets_v1_t *service;
} pxs_cli_host_t;

/* argc includes secrets/passwords/certs as argv[0]. Diagnostics are stable
 * constants; unrecognized arguments are never echoed, even on parse errors. */
plat_reason_t pxs_cli_parse(int argc, const char *const *argv,
                            pxs_request_v1_t *out, const char **detail);
int pxs_cli_run(const pxs_cli_host_t *host, int argc, const char *const *argv);
const char *pxs_cli_usage(void);
const char *pxs_operation_name(uint32_t operation);
include/platx/services/password_kdf_v1.h
#define PLAT_CAP_PASSWORD_KDF "crypto.password_kdf"
#define PLAT_PASSWORD_KDF_V1 0x00010000u
typedef struct plat_password_kdf_params {
    uint32_t memory_kib, passes, lanes, version;
} plat_password_kdf_params_t;

typedef struct plat_password_kdf_v1 {
    uint32_t struct_size, abi_version;
    /* Exact bytes, no strlen, trim or normalization. Salt is exactly 16 bytes;
     * output exactly 32. Only the bounded Argon2id 0x13 parameter range accepted.
     * Password/input/output buffers must not overlap. At most two jobs per
     * linked provider instance; the host also enforces its aggregate budget. */
    plat_reason_t (*derive)(const plat_password_kdf_params_t *params,
                            const void *password, size_t password_len,
                            const uint8_t salt[16], uint8_t out[32]);
} plat_password_kdf_v1_t;

/* Explicit build choices: PLATX_KDF_ARGON2_SYSTEM (system libargon2) or
 * PLATX_KDF_OPENSSL (OpenSSL 3.2+ Argon2 provider). No fast-KDF fallback. */
const plat_password_kdf_v1_t *plat_password_kdf_provider_v1(void);
include/platx/services/secrets_v1.h
#define PLAT_CAP_SECRETS PLAT_NAME_STORAGE_SECRETS
#define PLAT_SECRETS_V1 0x00010000u

typedef enum pxs_operation {
    PXS_STATUS = 1, PXS_HELP, PXS_CAPABILITIES,
    PXS_VAULT_INIT, PXS_VAULT_UNLOCK, PXS_VAULT_LOCK,
    PXS_PUT, PXS_LIST, PXS_DESCRIBE, PXS_GRANT_PREVIEW, PXS_GRANT_APPLY,
    PXS_LEASE_REVOKE, PXS_ROTATE_PREVIEW, PXS_ROTATE_APPLY,
    PXS_BACKUP_CREATE, PXS_BACKUP_VERIFY, PXS_RESTORE_PREVIEW, PXS_RESTORE_APPLY,
    PXS_PASSWORD_ADD, PXS_PASSWORD_GENERATE, PXS_PASSWORD_REVEAL,
    PXS_PASSWORD_IMPORT, PXS_CERT_LIST, PXS_CERT_IMPORT,
    PXS_CERT_VALIDATE, PXS_CERT_DEPLOY_PREVIEW, PXS_CERT_DEPLOY_APPLY,
    PXS_CERT_CSR, PXS_TRUST_PREVIEW, PXS_TRUST_APPLY
} pxs_operation_t;

/* Values describe client intent, never authenticated identity or authority.
 * No password, token, private key or raw value can occur in this record.
 * In-process DTO only: it is NOT a new wire codec or a frozen Core struct. */
typedef struct pxs_request_v1 {
    uint32_t struct_size, abi_version, operation, flags;
    uint8_t vault_id[16], object_id[16], consumer_id[16], target_id[16];
    uint8_t plan_id[16], lease_id[16];
    uint64_t expected_revision;
    uint32_t ttl_seconds, kind, scope, protector, password_length, expiry_days;
    char file_path[1024];
} pxs_request_v1_t;

#define PXS_F_JSON          0x01u
#define PXS_F_PROMPT        0x02u
#define PXS_F_SECURE_VIEW   0x04u
#define PXS_F_PRIVATE_STORE 0x08u

typedef struct pxs_status_v1 {
    uint32_t struct_size, abi_version;
    plat_reason_t reason;
    uint32_t crypto_available, record_format_available;
    uint32_t identity_ready, policy_ready, audit_ready, durable_ready;
} pxs_status_v1_t;

typedef struct plat_secrets_v1 {
    uint32_t struct_size, abi_version;
    plat_reason_t (*status)(pxs_status_v1_t *out);
    /* Execution is intentionally not exposed until authenticated ingress,
     * Policy/Action and durable audit are bound to the same request. */
} plat_secrets_v1_t;
46

security

Общие механизмы усиления защиты процесса и политики
src/security/Доверие и защита12 файлов1 API headers

Security domain связывает общие policy concepts, но не дублирует crypto, secure, seccomp, integrity или command authz. Его целевая роль — policy schema/evaluator facade и security posture aggregation.

Граница ответственности

  • Mechanisms остаются владельцами enforcement.
  • Policy decision typed, versioned, explainable; no hidden global booleans.
  • Default deny for protected operations; optional telemetry policy separate.

Устройство подсистемы

  • Policy input: actor/operation/target context-owner-generation/profile/state/risk/lease.
  • Evaluator loads immutable signed/local policy generation and returns ALLOW/DENY/CONDITIONAL with reason/requirements.
  • Operation dispatcher consumes decision and enforces conditions.
  • Posture aggregator reads secure/seccomp/integrity/crypto facts without changing them.

Поток работы

  • Operation intent → normalized policy input.
  • Evaluate generation → decision/explain.
  • Dispatcher applies/denies → audit.

Отказ и восстановление

  • Policy unavailable/invalid → protected deny.
  • Reload failure keeps old valid generation.
  • Contradictory mechanism state shown as degraded posture.

Основные возможности

  • Centralizes policy checks used by higher-level modules.
  • Connects manifest/deployment choices to hardening requirements.
  • Keeps security metadata separate from mechanism implementations.
Архитектурные детали и инварианты

Обзор

Userspace LSM policy engine: inode / task / socket проверки.

Компоненты

- **platx_lsm_inode.c** — inode_create, inode_unlink, inode_permission, setxattr, getxattr. Защищённые префиксы: /proc/, /sys/kernel/security/, /etc/platx/.

- **platx_lsm_task.c** — task_create, task_kill, task_setuid. Блокирует: setuid→root от !root, kill SIGKILL PID1 от !root.

- **platx_lsm_socket.c** — socket_create, socket_connect, socket_bind. Audit: AF_PACKET raw, bind<1024.

- **platx_lsm_stats.c** — агрегатор атомарных счётчиков.

Вердикты

- PLATX_LSM_ALLOW (0) — разрешить

- PLATX_LSM_DENY (-1) — запретить, inc cap_violations counter

- PLATX_LSM_AUDIT (1) — разрешить + audit

Weak seams

- plat_audit_write(const char *) — audit ring

- pxsp_metrics_inc_cap_violation() — selfprotect metrics

Тесты (TAP)

- t_lsm_hooks: 9 тестов (task + socket)

Управление и диагностика

Корневые команды: secure. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / security →
Состав подсистемы / 12 файлов
Файл / компонентНазначение и граница
src/security/cmd_security.cCLI namespace "secure" for the security hardening module. secure status show process security state secure harden apply all hardening at once secure mlock lock memory pages (mlockall) secure nodump disable core dumps (PR_SET_DUMPABLE=0)
src/security/platx_lsm_inode.cPLATX LSM inode hooks
src/security/platx_lsm_netpolicy.cРеализация platx / lsm / netpolicy
src/security/platx_lsm_netpolicy.hconnect к НЕИЗВЕСТНОМУ адресу → audit + block. platx_lsm_socket.c проверка connect состояла из списка запрещённых портов static const uint16_t s_blocked_ports[] = { 0 }; for (int i = 0; s_blocked_ports[i]; i++) ... Список пуст, а нулём он же и терминируется, поэтому цикл завершался на
src/security/platx_lsm_socket.cPLATX LSM socket hooks
src/security/platx_lsm_stats.cагрегатор статистики всех LSM-хуков
src/security/platx_lsm_task.cPLATX LSM task hooks
src/security/security.cplatform process hardening module. Implements the security_* API declared in security.h. libcap or /proc/self/status for capability queries. Audit integration: every hardening action calls audit_write() if the audit module is initialised. The include is conditional so the module
src/security/security.hplatform hardening / process security module. Wraps Linux process-level security primitives: - mlockall() — lock all virtual memory into RAM (no swap) - prctl(PR_SET_DUMPABLE,0) — disable core dumps - prctl(PR_SET_NO_NEW_PRIVS,1) — prevent privilege escalation
src/security/tests/t_audit_seam.cшов plat_audit_note доходит до структурированного аудита. Зачем этот тест существует модуля SelfProtect) объявляли у себя extern void plat_audit_write(const char *msg) __attribute__((weak)); при том, что настоящий plat_audit_write принимает `const plat_audit_event_t *` и возвращает int. Расхождение не проявлялось
src/security/tests/t_lsm_netpolicy.cКонтроль лжи на первом месте (урок W4-F007/W5-F004/W5-F008: «проверка, которая всегда докладывает чисто»): L0 — до включения политики хук ОБЯЗАН пропускать и считать unpoliced, а blocked обязан быть 0: иначе прибор шумит; L1 — после включения тот же самый connect обязан дать DENY: если L0 и L1
src/security/tests/t_sec_stack.c«Вместе» здесь означает четыре проверяемых факта, а не факт сборки: T1 они линкуются в один бинарь без конфликта символов и без дублей (проверяется самим фактом сборки этого файла со всеми тремя наборами); T2 у них ОДИН общий шов аудита: plat_audit_write определён здесь ровно
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/platx_security.h
/* SPDX-License-Identifier: Apache-2.0
 * platx_security.h — PLATX LSM hook integration API
 * §SEC-1: -Werror
 */

/* результат LSM-проверки */
typedef enum {
    PLATX_LSM_ALLOW  =  0,   /* разрешить */
    PLATX_LSM_DENY   = -1,   /* запретить */
    PLATX_LSM_AUDIT  =  1,   /* разрешить, но записать в audit */
} platx_lsm_verdict_t;

/* контекст inode-проверки */
typedef struct {
    uint64_t ino;
    uint32_t dev_major;
    uint32_t dev_minor;
    uint16_t mode;
    uint32_t uid;
    uint32_t gid;
    char     path[256];
} platx_lsm_inode_ctx_t;

/* контекст task-проверки */
typedef struct {
    pid_t    pid;
    pid_t    tgid;
    uint32_t uid;
    uint32_t gid;
    uint64_t cap_effective;
    char     comm[16];
} platx_lsm_task_ctx_t;

/* контекст socket-проверки */
typedef struct {
    int      family;
    int      type;
    int      protocol;
    uint32_t src_addr;   /* IPv4, network byte order */
    uint32_t dst_addr;
    uint16_t src_port;
    uint16_t dst_port;
} platx_lsm_socket_ctx_t;

/* --- inode hooks --- */
platx_lsm_verdict_t platx_lsm_inode_create(const platx_lsm_inode_ctx_t *ctx);
platx_lsm_verdict_t platx_lsm_inode_unlink(const platx_lsm_inode_ctx_t *ctx);
platx_lsm_verdict_t platx_lsm_inode_permission(const platx_lsm_inode_ctx_t *ctx, int mask);
platx_lsm_verdict_t platx_lsm_inode_setxattr(const platx_lsm_inode_ctx_t *ctx,
                                               const char *name);
platx_lsm_verdict_t platx_lsm_inode_getxattr(const platx_lsm_inode_ctx_t *ctx,
                                               const char *name);

/* --- task hooks --- */
platx_lsm_verdict_t platx_lsm_task_create(const platx_lsm_task_ctx_t *ctx);
platx_lsm_verdict_t platx_lsm_task_kill(const platx_lsm_task_ctx_t *sender,
                                          const platx_lsm_task_ctx_t *target, int sig);
platx_lsm_verdict_t platx_lsm_task_setuid(const platx_lsm_task_ctx_t *ctx,
                                            uint32_t new_uid);

/* --- socket hooks --- */
platx_lsm_verdict_t platx_lsm_socket_create(const platx_lsm_socket_ctx_t *ctx);
platx_lsm_verdict_t platx_lsm_socket_connect(const platx_lsm_socket_ctx_t *ctx);
platx_lsm_verdict_t platx_lsm_socket_bind(const platx_lsm_socket_ctx_t *ctx);

/* статистика */
typedef struct {
    uint64_t inode_allow;
    uint64_t inode_deny;
    uint64_t inode_audit;
    uint64_t task_allow;
    uint64_t task_deny;
    uint64_t socket_allow;
    uint64_t socket_deny;
} platx_lsm_stats_t;

void platx_lsm_stats_get(platx_lsm_stats_t *out);
47

selfprotect

Самозащита платформы, PLATXBoot и проверяемые провайдеры
src/selfprotect/Доверие и защита135 файлов3 API headers

SelfProtect соединяет описанную политику, защищаемые активы и набор провайдеров с явным местом исполнения. PLATXBoot отвечает за загрузочную цепочку; BPF, LKM и гипервизор предоставляют собственные возможности проверки и обеспечения политики. Состояние защиты выражается через health, quality, locus и generation, а события связываются с Forensic и SENSE. Оператор получает раздельные интерфейсы boot, selfprotect и trust.

Граница ответственности

  • Provider реализует frozen vtable; DSL/MSX/Core consumers не включают provider-private kernel/BPF/LKM headers.
  • Отсутствующая mandatory capability всегда UNAVAILABLE; PARTIAL/COMPATIBLE/EXACT quality нельзя маскировать одним boolean healthy.
  • Kernel получает только bounded canonical compiled policy: YAML, regex и arbitrary strings остаются userspace.

Устройство подсистемы

  • sp_abi.h фиксирует 64-bit capabilities, quality/risk/health, hook inventory, provider identity/build id, generation digest и vtable v2.
  • sp_policy.h фиксирует selectors, operations, verdict/visibility/action/exception/conflict model, limits и compiled policy representation.
  • sp_event.h задаёт typed decision, violation, health, maintenance, rollback и integrity events с generation/correlation.
  • sp_forensic.h задаёт evidence/forensic record framing, digest chain, checkpoint/signature and provenance fields.
  • sp_validate.c предоставляет dependency-free C11 validator and SHA-256 path before any provider activation.

Поток работы

  • Signed source policy → parse/normalize outside provider → compile bounded canonical blob.
  • sp_validate checks magic/version/length/counts/capabilities/digest/ordering.
  • Provider validate → prepare/self-test → atomic commit new generation; old generation remains on failure.
  • Operation → selector/decision/enforcement → typed event/forensic record.
  • Maintenance/revoke/rollback → generation transition, hook inventory re-check and evidence checkpoint.

Отказ и восстановление

  • Unknown capability, malformed length/count or digest mismatch is rejected before provider call.
  • Prepare/self-test failure keeps previous generation active and reports exact stage.
  • Required hook/capability loss transitions health to DEGRADED/VIOLATED and enters serialized recovery/policy action.
  • Maintenance expiry must atomically restore policy; stale token/generation cannot extend the window.
  • Forensic queue loss or chain discontinuity is visible and cannot be called exact enforcement evidence.

Основные возможности

  • Rule engine, validator and userspace provider with prepare/commit transaction model.
  • Tamper-evident forensic CLAIM→DECISION→EFFECT_RECEIPT chain with GAP/checkpoint.
  • SP↔MIR bridge shares correlation id and records both evidence planes.
Архитектурные детали и инварианты

Обзор

Подсистема selfprotect (sp_* + pxsp_*) защищает сам процесс PLATX от

внешней модификации, отладчиков и загрузки неподписанных модулей.

Слои

### sp_* — базовый слой (предсуществующий)

sp_state.c/h | CRC-64/XZ реестр регионов, seal/verify

sp_harden.c/h | prctl hardening: NO_NEW_PRIVS, DUMPABLE, CAPBSET_DROP

sp_validate.c/h | Валидация политик

Инварианты

INV-SP-01 | profile_t mprotect PROT_READ после загрузки

INV-SP-02 | При tamper detection → pxsp_lockdown_engage() → abort()

INV-SP-03 | Capabilities dropped до минимума

§SEC-3: нет malloc

Все структуры данных — статические массивы:

- sp_state: sp_region_t g_reg[32]

- pxsp_exceptions: s_pats[64][128]

- pxsp_audit/report: s_line[512], s_buf[512]

- pxsp_watchdog: watchdog_thread работает без heap

Счётчики (§4.39)

Экспортируются в plat_compstat:

Управление и диагностика

Корневые команды: boot, selfprotect. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / selfprotect →
Состав подсистемы / 135 файлов
Файл / компонентНазначение и граница
src/selfprotect/bootctl_main.cPLATX boot chain: автономный бинарь (cli-domains.mk pattern). Стандартная обёртка *ctl_main.c: вызывает plat_boot_cli() из sp_boot_cli.c. Функционально идентичен platxboot_main.c, но следует соглашению cli-domains.mk. Сборка (через cli-domains.mk): bootctl module whitelist-list
src/selfprotect/bpf/sp_bpf_hide.bpf.cPlatX SelfProtect: BPF introspection filter. Hides protected BPF prog/map IDs (SP's own programs, LKM programs, and all HV artifacts registered via the userspace provider) from bpftool and any other userspace tool that walks BPF objects via the bpf(2) syscall.
src/selfprotect/bpf/sp_lsm.bpf.cPlatX SelfProtect: BPF LSM enforcement. Запрещает операции с файлами (открытие, удаление, переименование, mmap, смену атрибутов) и защищает указанные PID от ptrace и kill. Это ЗАПРЕТ в ядре: возврат < 0 из LSM-хука отменяет операцию до её выполнения. Все отклонения эмитируются в ringbuf sp_events для userspace.
src/selfprotect/bpf/sp_module_lsm.bpf.cPLATX SelfProtect: BPF LSM module-load enforcement. Перехватывает kernel_read_file для READING_MODULE: - Если boot chain не верифицирован (sp_boot_state.verified == 0) И bootstrap окно закрыто (sp_module_bootstrap[0] == 0) → DENY - Если модуль не в sp_module_whitelist → DENY
src/selfprotect/bpf/sp_vmlinux_min.hминимальные CO-RE определения для BPF LSM программ. Полный vmlinux.h порождается `bpftool btf dump file /sys/kernel/btf/vmlinux format c`. bpftool в сборочном окружении может отсутствовать, а для этих программ нужен минимальный набор структур. Объявляем их с
src/selfprotect/cmd_boot.cрегистрация глагола `boot` в консоли платформы. Адаптер open_memstream: собирает вывод plat_boot_cli() в буфер, затем одним вызовом console_puts отдаёт в платформенную консоль. Не трогает FILE*, stdout, stderr — весь ввод-вывод только через ctx. Регистрация: register_boot_commands() → REG_boot() → register_all_modules()
src/selfprotect/cmd_selfprotect.cрегистрация глагола `selfprotect` в консоли платформы. Адаптер open_memstream: собирает вывод plat_sp_cli() в буфер, затем одним вызовом console_puts отдаёт в платформенную консоль. Не трогает FILE*, stdout, stderr — весь ввод-вывод только через ctx. Вызывается из register_cmds дескриптора модуля selfprotect.
src/selfprotect/platx_boot_verifier/platx_boot_verifier.cPLATX SelfProtect: EFI boot measurement → sysfs + BPF 1. Читает EFI переменную PlatxBootMeasurement (GUID 3a5eabcd-...) при загрузке модуля через efi.get_variable(). 2. Публикует двоичный sysfs атрибут: /sys/kernel/platx/boot_verified Атрибут возвращает platx_boot_measurement_t (48 байт),
src/selfprotect/platx_boot_verifier/platx_boot_verifier.mod.cРеализация platx / boot / verifier.mod
src/selfprotect/platx_hv/platx_hv.mod.cРеализация platx / hv.mod
src/selfprotect/platx_hv/platx_hv_arch.harchitecture-neutral hypervisor interface. platx_hv supports two virtualization backends: x86-64 : Intel VMX + AMD SVM. The host kernel is placed into VMX non-root or SVM guest mode (host-resident). The backend is selected at INIT from CPUID (VMX preferred
src/selfprotect/platx_hv/platx_hv_arm64.carm64 / EL2 backend. • Probe: establish that this CPU can host EL2 code at all. • Install platx_hv's EL2 vector table through the boot hyp stub. • Configure HCR_EL2 traps and enable host stage-2 translation. • Classify EL2 exceptions and route them through the shared policy
src/selfprotect/platx_hv/platx_hv_arm64.harm64 / EL2 backend definitions. On x86 the module puts the running kernel into VMX non-root operation with VMXON + VMLAUNCH. ARMv8-A has no equivalent instruction: an exception level is a property of where code is executing, and the kernel cannot demote itself into a guest by executing one instruction.
src/selfprotect/platx_hv/platx_hv_arm64_policy.carm64 system-register trap handling. This file is the arm64 counterpart of handle_cr_access() plus handle_wrmsr() on x86: it decodes a trapped MSR/MRS, maps the register onto the arch-neutral security role it plays, and asks the shared policy layer whether the write is permitted.
src/selfprotect/platx_hv/platx_hv_common.harchitecture-neutral module state and shared API. platx_hv_arch.h typed exit actions, exit classes, region map, guest-state accessor API (no state) platx_hv_common.h THIS FILE: module-wide state, event ring, policy buffers, integrity baseline, shared prototypes
src/selfprotect/platx_hv/platx_hv_dev.hinternal kernel-side definitions for the platx_hv LKM. This header is used only within the kernel module. Userspace uses src/selfprotect/sp_hv_manager.h for the ioctl ABI.
src/selfprotect/platx_hv/platx_hv_ept.cExtended Page Table (EPT) management. Because platx_hv is a type-2 HV (the host Linux kernel runs as the "guest"), the EPT must cover all physical memory with a 1:1 (identity) mapping. We build a 4-level EPT: PML4 (1 page) → PDPT (1 page per 512 GB region)
src/selfprotect/platx_hv/platx_hv_ept_guard.cPlatX HV: EPT guard + CR3-based syscall watch. EPT guard (ioctls 50-55): GPA ranges are registered with an access_mask describing which EPT permission bits to deny. The HV walks the EPT and marks each 4 KB page in the range with reduced permissions (e.g. no write, no execute, or no
src/selfprotect/platx_hv/platx_hv_ept_guard.hPlatX HV: EPT guard + CR3-based syscall watch. Kernel-internal header. Defines the in-kernel state for: EPT guard (ioctls 50-55) Marks GPA ranges with reduced EPT permissions. Any access that violates access_mask triggers an EPT violation VMEXIT. The HV checks
src/selfprotect/platx_hv/platx_hv_ept_memhide.cPlatX HV: EPT decoy-page memory concealment. Allocates one "decoy zero-page" at HV initialisation and remaps every protected guest-physical address (GPA) range to it with permissions R=1, W=0, X=0. The guest reads back all-zeros instead of HV or LKM memory contents. Any write or execute attempt faults into the HV via an
src/selfprotect/platx_hv/platx_hv_ept_memhide.hPlatX HV: EPT decoy-page memory concealment API.
src/selfprotect/platx_hv/platx_hv_ksyms.cрезолв неэкспортируемых kernel-символов через kprobe.
src/selfprotect/platx_hv/platx_hv_ksyms.hОбъявления типов и интерфейсов
src/selfprotect/platx_hv/platx_hv_main.cmodule_init/exit, char device, ioctl dispatch. Exposes /dev/platx_hv to the sp_hv_provider userspace daemon. ABI: src/selfprotect/sp_hv_manager.h (magic 'H', ioctl numbers 1–31). Features added over the initial version: • CPU hotplug — new CPUs automatically enter VMX non-root mode.
src/selfprotect/platx_hv/platx_hv_npt.cAMD nested paging for platx_hv. Same identity-map strategy as platx_hv_ept.c, different PTE encoding (long-mode P/RW/NX instead of EPT R/W/X). Reuses platx_hv_g.ept storage: eptp holds nCR3 (nested CR3), not an EPTP value.
src/selfprotect/platx_hv/platx_hv_npt_encode.hAMD nested-paging PTE encoding. bit 0 P present bit 1 R/W writable bit 2 U/S ignored for nested walks; we still set it bit 7 PS large page (2 MiB PDE / 1 GiB PDPTE) bit 63 NX no-execute (honoured) EPT-style permission words (bit0=R, bit1=W, bit2=X). This header is
src/selfprotect/platx_hv/platx_hv_physmap.cThe v1.x implementation classified a faulting guest-physical address as "MMIO" whenever pfn_valid() returned false. That test is necessary but • Device BARs can be programmed into address ranges for which the Linux memory map still reports a valid PFN, so real MMIO is
src/selfprotect/platx_hv/platx_hv_policy.cPolicy enforcement for the platx_hv LKM. The policy blob is pushed from userspace via the PLATX_HV_IOC_POLICY_PUSH ioctl and stored in platx_hv_g.policy_buf / policy_len. The blob format is defined by the platform policy compiler (out of scope for this file). The HV enforcement logic here is deliberately conservative:
src/selfprotect/platx_hv/platx_hv_probe.hOS-free VMX/SVM capability classification. Callers gather CPUID/MSR values; this header only interprets them. windows_rules=1: FOREIGN_HV blocks INIT (Hyper-V owns VT). windows_rules=0: nested virt is allowed (Linux platx_hv). Kernel- and userspace-safe (no linux/ or windows.h).
src/selfprotect/platx_hv/platx_hv_s2.cx86 backend dispatch: VMX vs SVM, EPT vs NPT. platx_hv_main.c and the EPT-named ioctls call through here so the Intel path stays unchanged while AMD uses the NPT/SVM equivalents.
src/selfprotect/platx_hv/platx_hv_s2mmu.carm64 Stage-2 translation for the host. This is the arm64 counterpart of platx_hv_ept.c. Stage-2 sits underneath the host kernel's own (stage-1) page tables: every physical access the host makes is translated a second time through tables the host cannot
src/selfprotect/platx_hv/platx_hv_stubs.cGPR accessors and exception injection used by guard/memhide. gregs[] layout matches both VMX and SVM trampolines: [0]=RAX [1]=RCX [2]=RDX [3]=RBX [5]=RBP … [11]=R11 (slot 4 is RSP).
src/selfprotect/platx_hv/platx_hv_svm.cAMD SVM core for the platx_hv LKM. Host-resident: VMRUN puts the running kernel into SVM guest mode with nested paging. #VMEXIT lands in platx_hv_svm_vmexit_entry (asm) which saves GPRs into ctx->gregs[] (same 0x78 offset as the VMX trampoline).
src/selfprotect/platx_hv/platx_hv_svm.hAMD SVM backend for platx_hv (VMCB, intercepts, NPT). Control-area layout matches Linux kvm struct vmcb_control_area (nested_cr3 at 0xC0, next_rip at 0xD8). Guest save area lives at VMCB + 0x400; fields are accessed by AMD APM offsets, not a C struct, so a padding mismatch cannot silently corrupt RIP/CR3.
src/selfprotect/platx_hv/platx_hv_svm_exit.hAMD SVM exit codes and the compact 64-slot statistics mapping used by PLATX_HV_IOC_VMEXIT_STATS. The ioctl ABI keeps by_reason[64]. Intel VMX reasons already fit that table (SDM Table 27-1). SVM codes do not: CPUID is 0x72, NPF is 0x400. This header maps the intercepts platx_hv actually handles onto the same
src/selfprotect/platx_hv/platx_hv_vmx.cIntel VMX core for the platx_hv LKM. • Detect VMX support and enumerate hardware capabilities. • Allocate per-CPU VMXON/VMCS regions, MSR bitmap, exit stack. • Program the VMCS with host/guest state that mirrors current CPU. • Execute VMXON + VMLAUNCH (per-CPU, via smp_call_function_single).
src/selfprotect/platxboot_cli.cPLATX: полный CLI для управления загрузочной цепочкой. platxboot install — установить PLATX EFI binary в ESP, создать UEFI boot entry platxboot uninstall — удалить PLATX из ESP и UEFI boot order platxboot status — статус boot chain (EFI var + sysfs + flags)
src/selfprotect/platxboot_main.cPLATX SelfProtect: точка входа standalone binary. cc -o platxboot platxboot_main.c platxboot_cli.c \ sp_boot_verify.c sp_module_whitelist.c \ Устанавливается в /usr/local/sbin/platxboot (или /usr/sbin/platxboot). Все команды требуют root (проверяет platxboot_dispatch при необходимости).
src/selfprotect/pxsp_audit.cРеализация pxsp / audit
src/selfprotect/pxsp_exceptions.cРеализация pxsp / exceptions
src/selfprotect/pxsp_hook_memwatch.cтонкие обёртки pxsp_* над sp_hook_restore / sp_memwatch. Позволяет вызывающему коду работать только с pxsp.h.
src/selfprotect/pxsp_lockdown.cРеализация pxsp / lockdown
src/selfprotect/pxsp_metrics.cСчётчики: tamper_detections, lockdown_count, cap_violations. Weak seam к plat_compstat_set: компилируется без core.
src/selfprotect/pxsp_mil.cWeak seam к elf_verify_signature: компилируется без elf/ зоны.
src/selfprotect/pxsp_report.cРеализация pxsp / report
src/selfprotect/pxsp_timer.cРеализация pxsp / timer
src/selfprotect/pxsp_watchdog.cРеализация pxsp / watchdog
src/selfprotect/selfprotect.cРеализация selfprotect
src/selfprotect/selfprotect_cli.cCLI домена самозащиты платформы. Использует реальный sp_abi.h (SP_ABI_VERSION 2) через vtable: plat_sp_get_provider() → sp_provider_vtable_t *vt → vt->health, cap_report, ... Слабая заглушка simulates PARTIAL-провайдер без ядерного перехвата. Реальная реализация регистрирует настоящий провайдер через
src/selfprotect/selfprotectctl_main.cавтономный бинарь selfprotectctl. Связывает selfprotect_cli.c напрямую. Использует слабые stub-реализации из selfprotect_cli.c вместо реального провайдера платформы. gcc -std=c11 -Wall -I include -I src/selfprotect \ -o selfprotectctl \ src/selfprotect/selfprotect_cli.c \
src/selfprotect/sp_abi.hPlatX SelfProtect v2: публичный замороженный ABI. SP0 wave: заморожены capability bits, semantic quality, risk classes, provider vtable v2 и capability contract. - Ни один провайдер не включает внутренние заголовки другого модуля. - Интеграция — только через эту vtable.
src/selfprotect/sp_asset_scan.cнаблюдение защищаемых активов на живой машине. Вся содержательная часть модуля — в sp_asset_state_from_errno(). Остальное обвязка: открыть, посмотреть, посчитать, записать. GREEN: make -f tests/Makefile.sp-locus test-sp-scan
src/selfprotect/sp_asset_scan.hSP1: наблюдение защищаемых активов на живой машине. sp_assets.c умеет принимать наблюдения, но сам ничего не смотрит: это сознательное разделение — правила учёта не должны зависеть от того, как устроен procfs. Здесь вторая половина: тот, кто смотрит. Главное свойство сканера — он умеет отвечать «нечем проверить».
src/selfprotect/sp_assets.cРеализация умышленно скучная: массив, линейный поиск, никакой аллокации. Инвентарь активов — то, что читают, когда всё остальное уже сломалось, и он не должен зависеть от кучи, блокировок или порядка инициализации. Вся содержательная работа — в отказах. См. sp_assets.h, три правила.
src/selfprotect/sp_assets.hТретья нога SelfProtect в R1 рядом с rule engine и forensic chain. До этого файла её не существовало: слово «asset» не встречалось в src/selfprotect/ ни разу, хотя ТЗ перечисляет девять классов активов, а вся защита состоит ровно в том, чтобы знать, ЧТО защищается.
src/selfprotect/sp_boot_cli.cPLATX boot chain CLI: адаптер к PLATX CLI pattern. Экспортирует plat_boot_cli(argc, argv, out, err) — стандартный интерфейс CLI-домена (аналог plat_sp_cli в selfprotect_cli.c). Вызывается из трёх контекстов: 1. cmd_boot.c → основной бинарь platx (platx boot )
src/selfprotect/sp_boot_verify.cPLATX SelfProtect: реализация верификации цепочки загрузки. Реализует публичное API из sp_boot_verify.h: sp_boot_read_measurement() 3-tier EFI var fallback (sysfs LKM → efivars → cmdline) sp_boot_chain_verify() полная верификация (measurement + sysfs + BPF map)
src/selfprotect/sp_boot_verify.hPLATX SelfProtect: верификация цепочки загрузки. Архитектура (три уровня): UEFI → EFI variable PlatxBootMeasurement → platx_boot_verifier.ko → /sys/kernel/platx/boot_verified → sp_boot_chain_verify() → BPF map sp_boot_state Все 5 замечаний закрыты: Z1 (LSM timing) — bootstrap-окно + BPF early-load API
src/selfprotect/sp_bpf_provider.cPlatX SelfProtect SP2: BPF LSM enforcement provider.
src/selfprotect/sp_bpf_provider.hPlatX SelfProtect SP2: BPF LSM enforcement provider. Загружает src/selfprotect/bpf/sp_lsm.bpf.c, наполняет карты запретов и защищённых PID из скомпилированной политики, честно отчитывается о том, что реально работает на текущем ядре. ГЛАВНОЕ СВОЙСТВО: пробник не зависит от libbpf.
src/selfprotect/sp_bpf_sysfilter.cPlatX SelfProtect: BPF introspection filter provider.
src/selfprotect/sp_bpf_sysfilter.hPlatX SelfProtect: BPF introspection filter provider. Manages the bpf/sp_bpf_hide.bpf.o object that hides protected BPF prog and map IDs from bpftool enumeration (BPF_PROG_GET_NEXT_ID / BPF_MAP_GET_NEXT_ID), direct fd acquisition (BPF_PROG_GET_FD_BY_ID / BPF_MAP_GET_FD_BY_ID), and
src/selfprotect/sp_dbg_hv_bridge.hPlatX SelfProtect: dbg ↔ HV injection bridge. This header is included by the dbg injection layer. It provides a single function, sp_dbg_hv_write_mem(), that the dbg module calls BEFORE falling back to its existing /proc//mem or ptrace path. // At the injection site, replace / wrap the existing write:
src/selfprotect/sp_event.hPlatX SelfProtect v2: event record schema. SP0 wave: заморожена схема события решения (decision record). Каждое событие включает provider_id, rule, quality, subject, object, verdict, visibility action, response result, sequence и coverage_epoch. Совместим с C11 + Linux/POSIX. Без внешних зависимостей.
src/selfprotect/sp_forensic.hPlatX SelfProtect v2: FORENSIC record classes. SP0 wave: заморожены record classes, common versioned header, tamper-evident journal structure и attestation-gated RA2C state machine. FORENSIC — плоскость доказательств, не детектор и не исполнитель. Canonical record classes:
src/selfprotect/sp_forensic_chain.cPlatX SelfProtect v2: tamper-evident FORENSIC journal.
src/selfprotect/sp_forensic_chain.hPlatX SelfProtect v2: tamper-evident FORENSIC journal. Реализует hash-chained журнал поверх record classes из sp_forensic.h: CLAIM → DECISION → EFFECT_RECEIPT — причинная цепочка одного события GAP — явный разрыв покрытия CHECKPOINT — Merkle-печать сегмента
src/selfprotect/sp_harden.cреализация hardening собственного процесса. См. sp_harden.h.
src/selfprotect/sp_harden.hPlatX SelfProtect: hardening собственного процесса. 25 913 строк) не было НИ ОДНОГО вызова prctl(). Ни PR_SET_NO_NEW_PRIVS, ни PR_SET_DUMPABLE, ни PR_CAPBSET_DROP. Подсистема самозащиты умела прятать чужие процессы и не умела защитить свой. См. W4-F002. - Каждый шаг возвращает СВОЙ код: применено / не поддержано ядром /
src/selfprotect/sp_hook_adapter.cPlatX SelfProtect v2: Hook TAP → rule engine bridge. SP1 wave: userspace-only; без kernel, без внешних зависимостей.
src/selfprotect/sp_hook_adapter.hPlatX SelfProtect v2: Hook TAP → rule engine bridge. SP1 wave: userspace-only; без kernel, без внешних зависимостей. Соединяет механизм перехвата платформы (Hook TAP) с движком правил sp_re_evaluate(). Hook-модуль заполняет sp_ha_hook_ctx_t, вызывает sp_ha_evaluate(); адаптер транслирует контекст в sp_event_subject_t +
src/selfprotect/sp_hook_restore.cРеализация sp / hook / restore
src/selfprotect/sp_hook_restore.hSelfProtect: целостность хуков и восстановление. Регистрирует эталонные байты каждой хук-функции (первые SP_HR_PROBE_BYTES). На каждом тике watchdog: пересчитывает CRC → при расхождении восстанавливает байты из baseline через временный mprotect(RWX) → эмитирует TAMPER-аудит.
src/selfprotect/sp_hv_abi_guard.cРеализация sp / hv / abi / guard
src/selfprotect/sp_hv_abi_guard.hPure userspace validation for an ioctl request before kernel interaction.
src/selfprotect/sp_hv_ept.cPlatX SelfProtect: EPT/NPT page-shadowing userspace layer.
src/selfprotect/sp_hv_ept.hPlatX SelfProtect: EPT/NPT page-shadowing userspace layer. Wraps the PLATX_HV_IOC_EPT_* ioctls (50-55) from sp_hv_manager.h into a typed API for two use-cases: A. Hiding platx-owned pages from the guest (self-integrity): The daemon's own text/data/stack physical pages are registered as
src/selfprotect/sp_hv_hardening.cW8-HV: defense-in-depth for the HV manager. Four independent layers: 1. Anti-debug gate — abort if TracerPid != 0 2. LKM SHA-256 gate — compare module bytes against expected hash 4. Policy mlock — prevent blob from reaching swap or core dump
src/selfprotect/sp_hv_hardening.hW8-HV: defense-in-depth extensions for the HV manager. Four independent layers, each independently controllable: 1. Anti-debug gate — refuse to load the HV LKM while a tracer is attached. A debugger on the loader process can observe the policy blob, the ioctl sequence, and the fd, so attachment is a precondition
src/selfprotect/sp_hv_inject.cPlatX SelfProtect: HV-based process code injection.
src/selfprotect/sp_hv_inject.hPlatX SelfProtect: HV-based process code injection. Wraps PLATX_HV_IOC_INJECT_EXEC/STATUS/CANCEL/FLUSH (ioctls 70-73) from sp_hv_manager.h into a typed API for the injection layer. When the HV is loaded (sp_hv_inject_available() returns true), code guest process memory by temporarily lifting EPT protections on the target
src/selfprotect/sp_hv_loader.cPlatX SelfProtect: hypervisor LKM loader (Stage 1).
src/selfprotect/sp_hv_loader.hPlatX SelfProtect: hypervisor LKM loader. Stage 1 of the SP→Hypervisor integration. The hypervisor LKM is not embedded in SP. This loader: 1. Reads the LKM file from disk. 2. Validates it through the integrity gate (SHA-256 + Ed25519 via integrity_api_t before the kernel ever sees the bytes.
src/selfprotect/sp_hv_manager.hPlatX SelfProtect: userspace↔hypervisor LKM ABI. This header is included by both the hypervisor LKM (in kernel context, with __KERNEL__ defined) and by the userspace SP manager layer. /dev/platx_hv — char device exposed by the hypervisor LKM once it has entered VMX/SVM root mode. Before PLATX_HV_IOC_INIT succeeds the
src/selfprotect/sp_hv_provider.cPlatX SelfProtect: hypervisor-backed SP provider (Stage 3). Implements sp_provider_vtable_t. Policy validate/prepare keep a staged copy in the caller-provided staging_slot. policy_commit pushes it to the HV via PLATX_HV_IOC_POLICY_PUSH; the HV's rule engine takes over from
src/selfprotect/sp_hv_provider.hPlatX SelfProtect: hypervisor-backed SP provider. Stage 3: implements sp_provider_vtable_t (sp_abi.h) with enforcement delegated to the hypervisor LKM loaded by sp_hv_loader. Quality and risk contract: identify() → SP_QUALITY_EXACT / SP_RISK_HIGH health ENFORCING requires: loader->loaded && active policy pushed
src/selfprotect/sp_hv_syscall.cPlatX SelfProtect: HV syscall interception userspace layer.
src/selfprotect/sp_hv_syscall.hPlatX SelfProtect: HV syscall interception userspace layer. Wraps PLATX_HV_IOC_SYSCALL_REGISTER/REMOVE/STATS ioctls (60-64) into a typed API for protecting PIDs from dangerous syscalls (execve, mmap, mprotect, open, write) via MSR_LSTAR-patched VMEXIT intercepts.
src/selfprotect/sp_hv_xim_bridge.cPlatX SelfProtect: XIM ↔ HV event bridge.
src/selfprotect/sp_hv_xim_bridge.hPlatX SelfProtect: XIM ↔ HV event bridge. A Core-owned task drains PLATX_HV_IOC_EVENTS_DRAIN every ~5 ms and maps each platx_hv_event to an sp_event_t for the XIM policy engine. PLATX_HV_EVT_SYSCALL_WATCH_HIT (14) → XIM policy decision (ALLOW/DENY) PLATX_HV_EVT_EPT_GUARD_HIT (15) → XIM audit log (EPT guard access)
src/selfprotect/sp_integ_events.hPlatX SelfProtect: LKM integrity event drain ABI. Extends the /dev/platx_integ device (INTEG_IOC_MAGIC='I') with two new ioctls for draining policy-violation and filter-hit events from the LKM integrity module's event ring buffer: INTEG_IOC_EVENTS_DRAIN (20) — pull batches of platx_integ_event records.
src/selfprotect/sp_integ_ftrace.hPlatX SelfProtect: LKM ftrace interception ABI. Extends /dev/platx_integ (INTEG_IOC_MAGIC='I') with ioctls 22-25 that control per-PID interception of writes to the kernel tracing filesystem: /sys/kernel/debug/tracing/tracing_on /sys/kernel/debug/tracing/events/enable
src/selfprotect/sp_integ_memhide.hPlatX SelfProtect: LKM memory-hide ioctl ABI. Extends /dev/platx_integ (INTEG_IOC_MAGIC='I') with ioctls 28-31 that maintain a set of physical address ranges the LKM must conceal from userspace memory reads (/dev/mem, /proc/kcore, ELF core dumps). When a range is registered the LKM hooks the relevant read paths and
src/selfprotect/sp_integ_task.hPlatX SelfProtect: LKM task_struct modification ABI. Extends /dev/platx_integ (INTEG_IOC_MAGIC='I') with ioctls 26-27 that allow the SP daemon to surgically modify selected fields of a live task_struct without exposing raw kernel pointers or requiring a writable
src/selfprotect/sp_lkm_ftrace.cPlatX SelfProtect: LKM ftrace interception userspace layer.
src/selfprotect/sp_lkm_ftrace.hPlatX SelfProtect: LKM ftrace interception userspace layer. Wraps INTEG_IOC_FTRACE_HIDE_PID/UNHIDE_PID/SET_MODE/STATUS (ioctls 22-25) into a typed API for the SP daemon.
src/selfprotect/sp_lkm_memhide.cPlatX SelfProtect: LKM memory-hide userspace provider.
src/selfprotect/sp_lkm_memhide.hPlatX SelfProtect: LKM memory-hide userspace provider. Wraps INTEG_IOC_MEMHIDE_* (ioctls 28-31) into a typed API for the SP daemon and the HV EPT extension. The provider maintains a local mirror of registered ranges so that destroy() can issue targeted MEMHIDE_DEL calls (or fall back to CLEAR)
src/selfprotect/sp_lkm_task.cPlatX SelfProtect: LKM task_struct modification userspace layer.
src/selfprotect/sp_lkm_task.hPlatX SelfProtect: LKM task_struct modification userspace layer. Wraps INTEG_IOC_TASK_MODIFY/GET (ioctls 26-27) into a typed API for the The kernel module that implements these ioctls resolves find_task_by_vpid() at module load time using kallsyms_lookup_name(). task_struct field offsets
src/selfprotect/sp_memwatch.cРеализация sp / memwatch
src/selfprotect/sp_memwatch.hSelfProtect: мониторинг попыток записи в память PLATX. Layer 1 — ptrace-зондирование: ptrace(PTRACE_TRACEME, 0, 0, 0) → если вернул -1/EPERM, уже трейсят. Дополняет TracerPid-чтение из pxsp_watchdog (разные техники, разные обходы). Layer 2 — /proc/self/fd scan:
src/selfprotect/sp_mesh_bridge.cмост SelfProtect ↔ Mesh ↔ Central Node
src/selfprotect/sp_mesh_bridge.hмост SelfProtect ↔ Mesh ↔ Central Node Публикует аттестацию узла в Mesh; получает и применяет команды от Central. Вызывается из pxsp_watchdog (do_tick step 5).
src/selfprotect/sp_mirage_bridge.cPlatX: SelfProtect ↔ MIRAGE integration bridge. SP1/MIR1 wave: userspace-only; без kernel, без внешних зависимостей.
src/selfprotect/sp_mirage_bridge.hPlatX: SelfProtect ↔ MIRAGE integration bridge. Связывает SP hook adapter (sp_ha_evaluate) с активированным MIRAGE world. → caller fills sp_ha_hook_ctx_t → sp_mir_evaluate(bctx, hctx, now, session_id, op_seq, &out) → sp_ha_evaluate() — SP rule engine
src/selfprotect/sp_module_whitelist.cPLATX SelfProtect: реализация module whitelist. Формат persist файла /etc/platx/boot/module_whitelist: :: /path/to/module.ko Формат BPF map (pinned): sp_wl_key_t → sp_wl_val_t
src/selfprotect/sp_module_whitelist.hPLATX SelfProtect: whitelist модулей ядра. Управляет BPF map sp_module_whitelist (pinned /sys/fs/bpf/platx/sp_module_whitelist). Ключ: {dev, ino} — пара (st_dev, st_ino) из stat(). Значение: SHA-256 хэш бинарного образа модуля (32 bytes). Зачем (dev, ino), а не имя файла:
src/selfprotect/sp_nl_filter.hPlatX SelfProtect: netlink event filter — shared ABI. Included by all three enforcement layers and their userspace providers: BPF layer — ebpf/hades/lsm/sp_nl_filter.bpf.c LKM layer — src/selfprotect/sp_nl_filter_lkm.h HV layer — src/selfprotect/sp_nl_filter_hv.h
src/selfprotect/sp_nl_filter_bpf.cPlatX SelfProtect: BPF provider for netlink filter. Userspace side of enforcement layer 1 (hades / BPF LSM). Manages the lifecycle of sp_nl_filter.bpf.o and updates the hide maps when processes/modules are protected or unprotected. Mirrors the structure of sp_bpf_provider.c but focuses exclusively on the
src/selfprotect/sp_nl_filter_bpf.hPlatX SelfProtect: BPF provider for netlink filter. Userspace interface to the BPF LSM netlink-suppression layer. Mirrors the pattern of sp_bpf_provider.h but is specific to the netlink filter program (sp_nl_filter.bpf.c / sp_nl_filter.bpf.o).
src/selfprotect/sp_nl_filter_hv.cPlatX SelfProtect: HV provider for netlink filter. Enforcement layer 3. Sends protect/unprotect commands to the hypervisor kernel module via ioctl on the already-open /dev/platx_hv fd (borrowed from sp_hv_loader, never owned here). The HV intercepts guest recvmsg/recvfrom at VMExit level and scrubs any
src/selfprotect/sp_nl_filter_hv.hPlatX SelfProtect: HV provider for netlink filter. Enforcement layer 3: the hypervisor kernel module (/dev/platx_hv) intercepts guest sys_recvmsg / sys_recvfrom calls on netlink file descriptors via VMCS-based syscall intercept (or EPT access fault on the socket's receive
src/selfprotect/sp_nl_filter_lkm.cPlatX SelfProtect: LKM (integrity) provider for netlink filter. Enforcement layer 2. Sends protect/unprotect commands to the integrity kernel module via ioctl on /dev/platx_integ. The kernel module (not implemented here — lives in the kernel source tree)
src/selfprotect/sp_nl_filter_lkm.hPlatX SelfProtect: LKM (integrity) provider for netlink filter. Enforcement layer 2: the integrity kernel module (/dev/platx_integ) intercepts netlink messages at the kernel level using ftrace hooks on: nlmsg_notify — NETLINK_ROUTE RTM_NEW* notifications
src/selfprotect/sp_policy.hPlatX SelfProtect v2: frozen rule model. SP0 wave: заморожены: - Subject/object/operation selectors - Verdicts и visibility - Inline и response actions - Risk classes (см. sp_abi.h) - Conflict resolution strategy - Compiled policy header Парсинг YAML/DSL происходит в userspace. Kernel получает только
src/selfprotect/sp_provider_coord.cPlatX SelfProtect: multi-provider coordinator.
src/selfprotect/sp_provider_coord.hPlatX SelfProtect: multi-provider coordinator. Tracks protected objects (PIDs, UIDs, files) across all three enforcement layers simultaneously: Layer 0 — BPF/hades (sp_nl_bpf_provider_t) Layer 1 — LKM/integrity (sp_nl_lkm_provider_t) Layer 2 — HV/hypervisor (sp_hv_ept_provider_t + sp_hv_syscall_provider_t)
src/selfprotect/sp_provider_userspace.cPlatX SelfProtect v2: userspace provider.
src/selfprotect/sp_provider_userspace.hPlatX SelfProtect v2: userspace provider. целиком в userspace. Без kernel, без BPF, без malloc. Это НЕ kernel provider и не претендует на его гарантии. Провайдер честно объявляет себя SP_QUALITY_PARTIAL и SP_HEALTH_OBSERVE_ONLY, потому что userspace-интерпозиция обходится прямым syscall. Конституция прямо это
src/selfprotect/sp_rule_engine.cPlatX SelfProtect v2: userspace rule matching engine. SP1 wave: userspace-only; без kernel, без внешних зависимостей.
src/selfprotect/sp_rule_engine.hPlatX SelfProtect v2: userspace rule matching engine API. SP1 wave: реализует приоритетное сопоставление скомпилированных правил с runtime subject/object/ops без kernel зависимостей. validated compiled policy block → subject selector match → object selector match
src/selfprotect/sp_state.cреализация контроля целостности состояния. См. sp_state.h.
src/selfprotect/sp_state.hPlatX SelfProtect: контроль целостности собственного состояния. Идея простая и старая: модуль самозащиты обязан первым замечать, что изменили ЕГО. Регистрируем участки памяти (политика, таблицы правил, profile_t), снимаем эталон, периодически пересчитываем. Расхождение —
src/selfprotect/sp_task_capsule.cPlatX SelfProtect: code-injection task capsule.
src/selfprotect/sp_task_capsule.hPlatX SelfProtect: code-injection task capsule. A TaskCapsule encapsulates a single code-injection operation: - Target process (pid) and virtual address (target_va) - Machine-code payload (raw bytes to write at target_va) - Execution flags (patch-only vs. redirect RIP; async vs. blocking)
src/selfprotect/sp_validate.cPlatX SelfProtect v2: policy validator implementation. SP0 wave: userspace-only; без kernel, без внешних зависимостей. SHA-256 — встроенная реализация (RFC 6234 / FIPS 180-4).
src/selfprotect/sp_validate.hPlatX SelfProtect v2: policy validator public API. SP0 wave: userspace-only валидатор скомпилированного блока политики. Kernel провайдер вызывает policy_validate() через vtable; этот модуль реализует ту же проверку на стороне userspace перед передачей в kernel.
src/selfprotect/tests/fuzz_drv_sp_state.cstandalone-драйвер к tests/fuzz/fuzz_sp_state.c. поэтому цель прогоняется детерминированным драйвером под ASan/UBSan. В отличие от самой цели драйвер имеет ОРАКУЛ: перевёрнутый бит обязан быть обнаружен (иначе sp_state_verify слеп и цель этого не заметит).
src/selfprotect/tests/mesh_bridge_soak.c24h soak test: sp_mesh_bridge + mesh_swarm + mesh_attestation. Симулирует непрерывную работу: - каждые 2с публикует аттестацию (sp_mesh_bridge_tick) - каждые 5с инжектирует случайную угрозу в swarm - каждые 30с проверяет метрики (no leaks, no hangs, counters monotone)
src/selfprotect/tests/probe_mcount.cLD_PRELOAD счётчик malloc/calloc/realloc; итог в $MC_OUT.
src/selfprotect/tests/probe_wd_malloc.cРеализация probe / wd / malloc
src/selfprotect/tests/t_pxsp_units.cпроверки модулей pxsp_*, не покрытых набором tests/selfprotect/.
src/selfprotect/tests/t_sp_harden.cДва требования задания одновременно: PR_SET_NO_NEW_PRIVS и PR_SET_DUMPABLE необратимы в пределах процесса, поэтому всё, что применяет hardening, исполняется в форкнутом ребёнке, а родитель после этого проверяет, что его собственное состояние не поехало.
src/selfprotect/tests/t_sp_state.cТесты живут в src/selfprotect/tests/, а не в tests/: каталог tests/ —
Контракты API / 3 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/pxsp.h
/* pxsp.h — PlatX SelfProtect: публичный API модуля самозащиты (pxsp_*).
 *
 * Волна 4 (A2/KERNEL). Задачи 4.21–4.43.
 *
 * Модуль pxsp_* дополняет существующий sp_* слой:
 *   pxsp_timer      — timerfd-обёртка для периодического watchdog
 *   pxsp_watchdog   — поток, проверяющий CRC регионов + TracerPid
 *   pxsp_lockdown   — экстренный lockdown при tamper (INV-SP-02)
 *   pxsp_exceptions — статический whitelist паттернов
 *   pxsp_report     — emit отчёта в audit ring (weak seam)
 *   pxsp_audit      — seq-numbered SP audit emitter
 *   pxsp_mil        — §MIL-10 module signature enforcement
 *   pxsp_metrics    — экспорт счётчиков в plat_compstat
 *
 * §SEC-3: ни одной аллокации в critical path.
 * SPDX-License-Identifier: GPL-2.0
 */

/* ── pxsp_timer ──────────────────────────────────────────────────────────── */
int      pxsp_timer_init(unsigned interval_ms);
int      pxsp_timer_poll(int fd);
void     pxsp_timer_close(int fd);

/* ── pxsp_lockdown ───────────────────────────────────────────────────────── */
void     pxsp_lockdown_engage(const char *reason);
int      pxsp_lockdown_active(void);
const char *pxsp_lockdown_reason(void);

/* ── pxsp_exceptions ─────────────────────────────────────────────────────── */
#define PXSP_EXC_MAX  64
#define PXSP_EXC_LEN 128
void     pxsp_exceptions_reset(void);
int      pxsp_exceptions_add(const char *pat);
int      pxsp_exceptions_match(const char *s);

/* ── pxsp_report ─────────────────────────────────────────────────────────── */
void     pxsp_report(const char *subsys, const char *event, const char *detail);

/* ── pxsp_audit ──────────────────────────────────────────────────────────── */
void     pxsp_audit_emit(const char *kind, const char *detail);
void     pxsp_audit_emit_lockdown(const char *reason);
uint64_t pxsp_audit_seq(void);

/* ── pxsp_watchdog ───────────────────────────────────────────────────────── */
#define PXSP_WD_REGIONS_MAX    8
#define PXSP_WD_INTERVAL_MS  500
int      pxsp_watchdog_add_region(const void *base, size_t len);
int      pxsp_watchdog_start(void);
void     pxsp_watchdog_stop(void);

/* Зарегистрировать BPF LSM-провайдер для draining событий из ringbuf
 * в watchdog-тик. Вызывать ДО pxsp_watchdog_start(). Потоконебезопасно.
 * Полный тип — sp_bpf_provider_t (include sp_bpf_provider.h).          */
struct sp_bpf_provider;
void     pxsp_watchdog_register_bpf(struct sp_bpf_provider *ctx);

/* ── pxsp_mil ────────────────────────────────────────────────────────────── */
int      pxsp_mil10_check(const char *module_path);
uint32_t pxsp_mil10_verified(void);
uint32_t pxsp_mil10_rejected(void);

/* ── pxsp_metrics ────────────────────────────────────────────────────────── */
typedef struct {
    uint64_t tamper_detections;   /* §4.39 */
    uint64_t lockdown_count;      /* §4.39 */
    uint64_t cap_violations;      /* §4.39 */
    uint64_t mil10_verified;
    uint64_t mil10_rejected;
    uint64_t audit_emitted;
} pxsp_metrics_t;

void     pxsp_metrics_get(pxsp_metrics_t *out);
void     pxsp_metrics_inc_tamper(void);
void     pxsp_metrics_inc_lockdown(void);
void     pxsp_metrics_inc_cap_violation(void);

/* ── sp_hook_restore (через watchdog) ───────────────────────────────────── */
/* Зарегистрировать хук-функцию для контроля целостности. Вызывать после
 * pxsp_watchdog_start(). fn_ptr — реальный адрес функции (не PLT).         */
int  pxsp_hook_register(void *fn_ptr, const char *name);
uint64_t pxsp_hook_restore_checks(void);
uint64_t pxsp_hook_restore_restores(void);

/* ── sp_memwatch ─────────────────────────────────────────────────────────── */
/* Счётчики угроз (только чтение, для pxsp_metrics_get расширенного).       */
uint64_t pxsp_memwatch_ptrace_hits(void);
uint64_t pxsp_memwatch_memfd_hits(void);
include/platx/selfprotect_host.h
/* platx/selfprotect_host.h — CLI host header для selfprotect.
 *
 * Включает заморозкой ABI (sp_abi.h) и добавляет CLI-слой:
 *   plat_sp_get_provider() — vtable провайдера (слабая заглушка или реальный).
 *   plat_sp_cli()          — точка входа CLI.
 *   plat_sp_register_console() — регистрация глагола в консоли платформы.
 *
 * Не переопределяет типы из sp_abi.h. sp_abi.h включается НАПРЯМУЮ.
 * В сборке CLI передаём -I src/selfprotect, где лежит sp_abi.h.
 */
/* sp_abi.h живёт в src/selfprotect/ — не в include/platx/.
 * В полной сборке платформы он виден через -I; при автономной сборке
 * *ctl_main.c передаёт -I src/selfprotect. */

/* Получить зарегистрированный vtable провайдера selfprotect.
 * Слабая заглушка возвращает встроенный stub-провайдер для автономного теста.
 * Реальная реализация вызывает platform_require("selfprotect", ...). */
sp_provider_vtable_t *plat_sp_get_provider(void);

/* CLI entry point.
 * argc/argv начинаются с подкоманды (status|policy|assets|verify). */
int  plat_sp_cli(int argc, char **argv, FILE *out, FILE *err);

/* Регистрация глагола "selfprotect" в консоли платформы.
 * Вызывается из register_cmds модуля selfprotect. */
void plat_sp_register_console(void);
include/platx/sp_hv_uapi.h
/* include/platx/sp_hv_uapi.h — минимальный userspace ABI заголовок /dev/platx_hv.
 *
 * Фрагмент sp_hv_manager.h (полная версия живёт в LKM-каталоге).
 * Экспортирует только то, что нужно клиентам /dev/platx_hv в userspace:
 * структуры запросов/ответов, коды здоровья, ioctl-числа.
 *
 * Kernel-side ABI: magic 'H', ioctl numbers 1–31.
 * Явно зафиксированные в комментариях платформы:
 *   ATTEST = #23 (platx_hv_main.c, строка комментария)
 *   EVENTS_DRAIN = #31 (platx_hv_main.c)
 * Остальные выводятся по порядку switch-case в ioctl-диспетчере.
 *
 * Если поменяется ABI в LKM — менять здесь синхронно;
 * гейт te-hv-abi (tests/trust/t_te_hv_abi.sh) сравнивает структуры.
 */

/* ── IOC magic ───────────────────────────────────────────────────────────── */

#define PLATX_HV_IOC_MAGIC  'H'

/* ── Health codes ────────────────────────────────────────────────────────── */

#define PLATX_HV_HEALTH_UNKNOWN       0u  /* нет данных, модуль не опрашивался    */
#define PLATX_HV_HEALTH_UNAVAILABLE   1u  /* VMX/SVM недоступен, модуль не загружен */
#define PLATX_HV_HEALTH_OBSERVE_ONLY  2u  /* наблюдение, защита включена (норма)   */
#define PLATX_HV_HEALTH_VIOLATED      3u  /* нарушение зафиксировано               */

/* ── Mode codes (из platx_hv_probe.h) ───────────────────────────────────── */

#define PLATX_HV_MODE_NONE  0u
#define PLATX_HV_MODE_VMX   1u
#define PLATX_HV_MODE_SVM   2u

/* ── Structures ──────────────────────────────────────────────────────────── */

/* IOCTL_STATUS (nr=1): общий статус HV-модуля. */
struct platx_hv_status {
    uint32_t abi_version;         /* PLATX_HV_ABI_VERSION                  */
    uint32_t mode;                /* PLATX_HV_MODE_* (VMX/SVM/NONE)         */
    uint32_t health;              /* PLATX_HV_HEALTH_*                       */
    uint32_t hw_caps;             /* PLATX_HV_CAP_* bitmap                   */
    uint32_t vcpu_count;          /* число активных vCPU                     */
    uint32_t policy_generation;   /* generation политики                     */
    uint64_t vmexits_total;       /* суммарно VM-exits с момента загрузки    */
    uint64_t policy_events_total; /* суммарно policy-событий                 */
    uint64_t last_policy_push_ts; /* ktime_t последнего policy push          */
    char     build_info[128];     /* "platx_hv vX.Y.Z, kernel ..."           */
};

/* IOCTL_HEALTH (nr=7): кратко — только health. */
struct platx_hv_health {
    uint32_t health;          /* PLATX_HV_HEALTH_*                           */
    uint32_t lost_events;     /* кол-во потерянных событий (ring overflow)   */
    uint64_t last_selftest_ts;/* ktime_t последнего selftest                 */
    uint32_t selftest_pass;   /* 1 = pass, 0 = fail / не проводился          */
    uint32_t _pad;
};

/* IOCTL_ATTEST (nr=23): аттестация через VMCALL на всех CPU. */
struct platx_hv_attest_result {
    uint32_t pass;       /* 1 = все CPU прошли, 0 = хотя бы одна ошибка     */
    uint32_t health;     /* PLATX_HV_HEALTH_* после аттестации               */
    uint64_t timestamp;  /* ktime_get_real_seconds() момента аттестации      */
};

/* ── IOCTLs ──────────────────────────────────────────────────────────────── */

#define PLATX_HV_IOC_STATUS   _IOR(PLATX_HV_IOC_MAGIC,  1, struct platx_hv_status)
#define PLATX_HV_IOC_HEALTH   _IOR(PLATX_HV_IOC_MAGIC,  7, struct platx_hv_health)
#define PLATX_HV_IOC_ATTEST   _IOWR(PLATX_HV_IOC_MAGIC, 23, struct platx_hv_attest_result)
48

sense

Происхождение наблюдений и объединение источников
src/sense/Наблюдение и исследование23 файлов19 API headers

Sense организует наблюдения в граф источников, объектов и отношений. Каждое показание сохраняет происхождение, время, качество и охват. Сопоставление не создаёт отсутствующие факты, а конфликт источников становится отдельным материалом для исследования.

Граница ответственности

  • Sense организует наблюдения в граф источников, объектов и отношений. Каждое показание сохраняет происхождение, время, качество и охват. Сопоставление не создаёт отсутствующие факты, а конфликт источников становится отдельным материалом для исследования.

Устройство подсистемы

  • namespace "sense": чтение кольца наблюдения. sense status модуль, источники, потребители, кольцо sense sources зарегистрированные источники sense stats published / refused / overwritten
  • SENSE181–SENSE185 typed action intent state machine.
  • плоскость покрытия SENSE. Карточки: SENSE078-SENSE080 (политики допуска и сверка учёта), SENSE081-SENSE094 (эпохи, дыры, барьер, слепота, снапшоты, согласованный статус). Кольцо (src/sense/sense_ring.c) отвечает на вопрос «какие факты дошли». Этот файл отвечает на существенно более неудобный вопрос: за какой
  • the bounded filter evaluator. A subscription filter runs on the consumer's path for every record the ring hands it. That is the wrong place for anything unbounded, so the grammar is closed on purpose: a fixed node ceiling, a fixed field set, a fixed operator set, no regex, no callback, no allocation. A filter that
Архитектурные детали и инварианты

Карта дерева (3.01)

Детекторы уже существуют в src/observe/sense_detector.c: exec_hidden_path,

uid_gid_skip, net_after_exec, cred_dump_open, script_chain, rm_after_exec,

ptrace_bypass, memfd_exec, socket_flood и др.

Почему НЕ добавлен третий реестр детекторов (W3-F001)

ТЗ 3.03-3.08/3.14-3.20 просят новый API sense_detector_register/event_submit/

graph_tick и семь новых детекторов. Но src/sense и src/observe уже содержат

**29 одинаковых внешних символов** (sense_detector_init/new_epoch,

sense_registry_*, sense_ring_publish, sense_metrics_* …); манифест сборки

прямо помечает observe как «redefine symbols already implemented by the canonical

tree src/sense». Ввод третьего параллельного реестра нарушил бы канон **C15**

(«пятый реестр не заводить») и **C22** (контракт до реализации), и усугубил бы

открытый вопрос владельца о судьбе 2-го/3-го sense-деревьев. Поэтому детекторный

реестр НЕ дублируется; вместо этого добавлены не-конфликтующие компоненты.

Аддитивные компоненты (новые символы, без коллизий)

- **sense_threat** (include/platx/sense_threat.h, src/sense/sense_threat.c) —

глобальная ThreatLevel-машина. INV-SENSE-01: TL3→sampling×2, TL4→×4. INV-SENSE-02:

детектор только повышает TL (CAS-loop), понижение — только sense_threat_policy_lower

с ненулевым policy-токеном. INV-SENSE-03: каждая эскалация уходит в audit (weak seam).

- **sense_timeline** (include/platx/sense_timeline.h, src/sense/sense_timeline.c) —

static ring RING[1024], без malloc; push/get(oldest=0)/count.

- **sense_correlate** — sense_correlate_count(corr_id) поверх timeline (3.32).

Тест tests/sense/t_sense_graph.c 10/10; фаззер tests/fuzz/fuzz_sense_event.c 2M

итераций без падений. Сборка: make -f mk/sense.mk check-sense.

Управление и диагностика

Корневые команды: sense. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / sense →
Состав подсистемы / 23 файлов
Файл / компонентНазначение и граница
src/sense/cmd_sense.cnamespace "sense": чтение кольца наблюдения. sense status модуль, источники, потребители, кольцо sense sources зарегистрированные источники sense stats published / refused / overwritten
src/sense/sense_action.cSENSE181–SENSE185 typed action intent state machine.
src/sense/sense_bus.cРеализация sense / bus
src/sense/sense_checkpoint.cРеализация sense / checkpoint
src/sense/sense_coverage.cплоскость покрытия SENSE. Карточки: SENSE078-SENSE080 (политики допуска и сверка учёта), SENSE081-SENSE094 (эпохи, дыры, барьер, слепота, снапшоты, согласованный статус). Кольцо (src/sense/sense_ring.c) отвечает на вопрос «какие факты дошли». Этот файл отвечает на существенно более неудобный вопрос: за какой
src/sense/sense_filter.cthe bounded filter evaluator. A subscription filter runs on the consumer's path for every record the ring hands it. That is the wrong place for anything unbounded, so the grammar is closed on purpose: a fixed node ceiling, a fixed field set, a fixed operator set, no regex, no callback, no allocation. A filter that
src/sense/sense_fim.cSENSE101–SENSE106 inotify file-integrity source.
src/sense/sense_finding.cSENSE160-162: finding identity, provenance, confidence. See platx/sense_finding.h for why a finding needs all three.
src/sense/sense_findings.cSENSE175 / SENSE195.
src/sense/sense_hostident.cSENSE107–SENSE118 host entity identity index. Deterministic, self-contained, single control-plane lock.
src/sense/sense_mod.cPLATX SENSE v1 module lifecycle (SENSE201-202) Fixed against ABI-v1 headers: - sense_event_header_t fields: schema_version, monotonic_ns, coverage_epoch - sense_ring_publish(owner, prio, class, wire, len) - sense_tlv_write(buf, bufsz, off, type, val, len) → int
src/sense/sense_module.cRefuses start/control/stop that carry generation 0 or a stale generation. Single-threaded control plane assumption: the platform lifecycle serialises module control operations, so the module state is a plain object guarded by the caller's control lock (documented lock rank sits above the registry).
src/sense/sense_obs0.cOBS0 seen through the v2 descriptor. OBS0 keeps its ABI. Nothing in include/platx/observe.h changes and no existing source is edited: the adapter wraps a plat_obs_source_t and presents it as a plat_sense_source_v2_t, which is what "compatibility adapter" has to mean if the old registry is to keep working unchanged.
src/sense/sense_registry.cthe v2 source registry. One table, bounded, holding descriptor pointers rather than copies: the descriptor is the caller's and must outlive its registration, the same contract OBS0 already states. This is not a second platform registry; it is the sense module's own instance table, in the spirit of XIO's fd
src/sense/sense_ring.cthe bounded multi-consumer domain ring. The ring is volatile and at-most-once. That is a decision, not a limitation: a durable ring would have to block the producer on the slowest consumer, and a producer blocked inside a kernel callback is the behind loses records — and is told exactly how many, because a drop that
src/sense/sense_schema_reg.cSENSE schema registry + linter. SENSE039: schema registry storage and update ownership. SENSE040: C constants and operator catalog from one schema source. SENSE041: duplicate/range/reserved-ID schema linter.
src/sense/sense_signer.cSENSE037/SENSE038 signer domain separation. Reuses the platform HMAC-SHA256 primitive; introduces no new crypto stack.
src/sense/sense_synth.cthe synthetic v2 source. Every loss test needs something whose correct answer is known in advance. This source emits a deterministic sequence from a seed and skips exactly the numbers a gap should swallow, so a test can assert which records are missing rather than only that some are.
src/sense/sense_threat.cРеализация sense / threat
src/sense/sense_timeline.cРеализация sense / timeline
src/sense/sense_tlv.cSENSE TLV encoder and decoder. SENSE018: deterministic canonical field order (required ascending, then optional). SENSE019: decoder rejects duplicate required fields. SENSE020: relay preserves unknown optional TLV verbatim. SENSE021: decoder returns SENSE_ERR_SCHEMA on unknown required TLV.
src/sense/sense_visibility.cSENSE151-155: Visibility Contract parser and evaluator. See platx/sense_visibility.h for what a contract is and why the four on-blind outcomes are kept apart. No allocation, no I/O, no global state: a contract is compiled into the caller's struct and evaluated against a snapshot the caller supplies.
src/sense/sense_xim_adapter.cРеализация sense / xim / adapter
Контракты API / 19 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/sense.h
/* platx/sense.h — SENSE v1.0: public sensor control, query and event ABI.
 *
 * SENSE is the observation control plane: it normalises facts from OBS0,
 * Hades BPF rings, the Hypervisor ring and SelfProtect into one canonical
 * versioned stream with loss, sequence, identity and privacy accounting.
 *
 * Normative contract:
 *   SENSE receives and normalises facts.
 *   SENSE does NOT kill, block, overwrite or take enforcement actions.
 *   Every drop is counted and emits a GAP record — never silence.
 *   A sensor in BLIND/FAILED state never auto-transitions to HEALTHY.
 *
 * Thread-safety: all registry/ring calls are internally synchronised.
 * Callbacks receive an immutable view; storing the pointer is forbidden.
 *
 * ABI freeze: SENSE_ABI_VERSION 1  (SENSE001-SENSE050 sealed 2026-08-27)
 */

/* ── ABI version ─────────────────────────────────────────────────────────── */
#define SENSE_ABI_VERSION   1u
#define SENSE_ABI_MINOR     0u

/* ── Capability names (registered in names.h after collision test) ────────── */
#define SENSE_CAP_EVENTS    "sensors.sense.events"
#define SENSE_CAP_QUERY     "sensors.sense.query"
#define SENSE_CAP_CONTROL   "sensors.sense.control"
#define SENSE_CAP_HADES     "sensors.hades"      /* unchanged provider cap */

/* ── Sensor lifecycle states ─────────────────────────────────────────────── */
typedef enum sense_state {
    SENSE_STATE_DISCOVERED  = 0,  /* descriptor registered, not validated */
    SENSE_STATE_VALIDATED   = 1,  /* validate() passed */
    SENSE_STATE_PREPARED    = 2,  /* prepare() committed shadow attach */
    SENSE_STATE_RUNNING     = 3,  /* start() succeeded, reader active */
    SENSE_STATE_DEGRADED    = 4,  /* partial: bounded loss, limited schema */
    SENSE_STATE_BLIND       = 5,  /* coverage gap; no auto-recovery */
    SENSE_STATE_FAILED      = 6,  /* non-recoverable; DESTROY required */
    SENSE_STATE_STOPPED     = 7   /* stop() completed; all owners released */
} sense_state_t;

/* ── Owner epoch (wraps plat_owner_t semantics) ──────────────────────────── */
typedef uint64_t sense_owner_t;   /* 0 = no owner / invalid */

/* ── Sensor identity ─────────────────────────────────────────────────────── */
typedef struct sense_sensor_id {
    uint32_t abi_version;         /* SENSE_ABI_VERSION */
    uint32_t struct_size;         /* sizeof(sense_sensor_id_t) */
    char     source_id[64];       /* stable UTF-8 id, no aliases */
    char     provider_cap[64];    /* provider capability string */
    char     instance_id[32];     /* instance discriminator */
    uint8_t  build_id[20];        /* ELF build-id or zero */
    uint32_t schema_min;          /* minimum schema major supported */
    uint32_t schema_max;          /* maximum schema major supported */
    uint64_t kernel_fingerprint;  /* BTF/kernel hash or 0 */
    uint64_t start_generation;    /* provider generation at start */
    uint32_t capabilities;        /* SENSE_CAP_F_* bitmask */
    uint32_t quality;             /* SENSE_QUAL_* bitmask */
    uint8_t  config_digest[32];   /* SHA-256 of active config or zero */
    uint8_t  reserved[32];
} sense_sensor_id_t;

_Static_assert(sizeof(sense_sensor_id_t) == 288,
               "sense_sensor_id_t ABI size changed");

/* ── Capability flags ────────────────────────────────────────────────────── */
#define SENSE_CAP_F_STREAM      (1u << 0)  /* continuous event emission */
#define SENSE_CAP_F_SNAPSHOT    (1u << 1)  /* atomic snapshot */
#define SENSE_CAP_F_HYBRID      (1u << 2)  /* stream + reconciled snapshot */
#define SENSE_CAP_F_HEARTBEAT   (1u << 3)  /* periodic health signal */
#define SENSE_CAP_F_SELFTEST    (1u << 4)  /* in-situ self-test */
#define SENSE_CAP_F_ROLLBACK    (1u << 5)  /* generation rollback */
#define SENSE_CAP_F_PRIVACY     (1u << 6)  /* field-level redaction */
#define SENSE_CAP_F_ENRICH      (1u << 7)  /* async enrichment */

/* ── Quality flags ───────────────────────────────────────────────────────── */
#define SENSE_QUAL_HIGH         (1u << 0)  /* full identity + direct kernel */
#define SENSE_QUAL_PARTIAL      (1u << 1)  /* some fields synthesized */
#define SENSE_QUAL_BLIND        (1u << 2)  /* coverage gap present */
#define SENSE_QUAL_ADAPTER_SYN  (1u << 3)  /* field added by OBS0 adapter */

/* ── Trust ladder ────────────────────────────────────────────────────────── */
typedef enum sense_trust {
    SENSE_TRUST_UNKNOWN      = 0,  /* no authentication */
    SENSE_TRUST_USERSPACE    = 1,  /* userspace with no kernel proof */
    SENSE_TRUST_MEDIATED     = 2,  /* XIM/hook interposition */
    SENSE_TRUST_KERNEL_BPF   = 3,  /* eBPF tracepoint / LSM */
    SENSE_TRUST_HYPERVISOR   = 4   /* below-kernel VMM observation */
} sense_trust_t;

/* ── Health report ───────────────────────────────────────────────────────── */
typedef struct sense_health_report {
    uint32_t abi_version;
    uint32_t struct_size;
    sense_state_t state;
    uint64_t coverage_epoch;
    uint64_t events_received;
    uint64_t events_dropped;
    uint64_t events_kernel_lost;
    uint64_t gaps_open;
    uint64_t gaps_closed;
    uint64_t heartbeat_missed;
    uint32_t active_consumers;
    uint32_t quality;
    uint8_t  reserved[32];
} sense_health_report_t;

_Static_assert(sizeof(sense_health_report_t) == 112,
               "sense_health_report_t ABI size changed");

/* ── Selftest report ─────────────────────────────────────────────────────── */
typedef struct sense_selftest_report {
    uint32_t abi_version;
    uint32_t struct_size;
    int      passed;              /* non-zero: all sub-tests passed */
    uint32_t tests_run;
    uint32_t tests_failed;
    char     detail[256];
} sense_selftest_report_t;

/* ── Config report ───────────────────────────────────────────────────────── */
typedef struct sense_config_report {
    uint32_t abi_version;
    uint32_t struct_size;
    int      valid;
    char     reason[512];
    uint32_t capability_mask;
    uint32_t privacy_level;
    uint64_t estimated_events_per_sec;
    uint64_t estimated_bytes_per_sec;
} sense_config_report_t;

/* ── Cap report entry ────────────────────────────────────────────────────── */
typedef struct sense_cap_entry {
    uint32_t cap_id;
    uint32_t cap_version;
    int      available;
    char     reason[128];
} sense_cap_entry_t;

/* ── Query ───────────────────────────────────────────────────────────────── */
typedef struct sense_query {
    uint32_t schema_version;
    uint32_t struct_size;
    uint64_t entity_id;          /* 0 = all */
    uint32_t event_class_mask;   /* 0 = all */
    uint32_t max_results;
    uint64_t since_sequence;
    uint64_t deadline_ns;        /* monotonic; 0 = default */
} sense_query_t;

/* ── Sink (consumer callback) ────────────────────────────────────────────── */
typedef struct sense_event_view sense_event_view_t; /* opaque; see sense_schema.h */
typedef int (*sense_sink_fn)(const sense_event_view_t *ev, void *ctx);

typedef struct sense_sink {
    sense_sink_fn fn;
    void         *ctx;
} sense_sink_t;

/* ── Source v2 descriptor ────────────────────────────────────────────────── */
typedef struct plat_sense_source_v2 {
    uint32_t abi_version;         /* SENSE_ABI_VERSION */
    uint32_t struct_size;         /* sizeof(plat_sense_source_v2_t) */

    const char *source_id;
    const char *provider_capability;
    void       *ctx;

    int (*identify)(void *, sense_sensor_id_t *out);
    int (*cap_report)(void *, sense_cap_entry_t *out,
                      uint32_t cap, uint32_t *count_out);
    int (*validate)(void *, const void *cfg, size_t cfg_len,
                    sense_config_report_t *out);
    int (*prepare)(void *, const void *cfg, size_t cfg_len,
                   uint64_t *txn_id_out);
    int (*commit)(void *, uint64_t txn_id, uint64_t *generation_out);
    int (*rollback)(void *, uint64_t generation);
    int (*start)(void *, sense_owner_t owner);
    int (*stop)(void *, sense_owner_t owner, uint32_t timeout_ms);
    int (*snapshot)(void *, const sense_query_t *, sense_sink_t *);
    int (*health)(void *, sense_health_report_t *out);
    int (*selftest)(void *, sense_selftest_report_t *out);

    void *reserved[8];
} plat_sense_source_v2_t;

_Static_assert(sizeof(plat_sense_source_v2_t) >= 128,
               "plat_sense_source_v2_t layout check");

/* ── Registry API ────────────────────────────────────────────────────────── */
int sense_source_register  (plat_sense_source_v2_t *src);
int sense_source_deregister(const char *source_id);
int sense_source_start     (const char *source_id, sense_owner_t owner);
int sense_source_stop      (const char *source_id, sense_owner_t owner,
                             uint32_t timeout_ms);
int sense_source_health    (const char *source_id,
                             sense_health_report_t *out);
int sense_source_selftest  (const char *source_id,
                             sense_selftest_report_t *out);
int sense_source_validate  (const char *source_id,
                             const void *cfg, size_t cfg_len,
                             sense_config_report_t *out);
int sense_source_prepare   (const char *source_id,
                             const void *cfg, size_t cfg_len,
                             uint64_t *txn_id_out);
int sense_source_commit    (const char *source_id,
                             uint64_t txn_id, uint64_t *generation_out);
int sense_source_rollback  (const char *source_id, uint64_t generation);

/* ── Consumer (domain ring) API ──────────────────────────────────────────── */
typedef struct sense_cursor sense_cursor_t;    /* opaque */

#define SENSE_CONSUMERS_MAX 8u

sense_cursor_t *sense_consumer_open (sense_owner_t owner,
                                     uint32_t event_class_mask,
                                     uint32_t priority);
int             sense_consumer_next (sense_cursor_t *cur,
                                     uint64_t deadline_ns,
                                     const sense_event_view_t **out);
int             sense_consumer_ack  (sense_cursor_t *cur, uint64_t sequence);
void            sense_consumer_close(sense_cursor_t *cur);
int             sense_consumer_lag  (sense_cursor_t *cur,
                                     uint64_t *lag_out,
                                     uint64_t *drop_out);

/* ── Module lifecycle ────────────────────────────────────────────────────── */
int  sense_registry_init   (void);
void sense_registry_destroy(void);
int  sense_registry_status (char *buf, size_t bufsz);

/* ── OBS0 compatibility adapter ──────────────────────────────────────────── */
/* Wraps a legacy plat_obs_source_t as a v2 descriptor with PARTIAL quality. */
struct plat_obs_source;
int sense_obs0_adapt(struct plat_obs_source *obs_src,
                     plat_sense_source_v2_t *out);
/* Release the adapter slot. The legacy descriptor is untouched. */
void sense_obs0_release(plat_sense_source_v2_t *v2);

/* ── Error codes ─────────────────────────────────────────────────────────── */
#define SENSE_OK                  0
#define SENSE_ERR_INVAL          -1
#define SENSE_ERR_FULL           -2   /* registry or ring at capacity */
#define SENSE_ERR_NOT_FOUND      -3
#define SENSE_ERR_DUPLICATE      -4
#define SENSE_ERR_STALE_OWNER    -5
#define SENSE_ERR_STALE_GEN      -6
#define SENSE_ERR_BLIND          -7   /* source in BLIND state */
#define SENSE_ERR_UNSUPPORTED    -8
#define SENSE_ERR_BUSY           -9   /* consumer slot full */
#define SENSE_ERR_TIMEOUT        -10
#define SENSE_ERR_PRIVACY        -11  /* lease missing for field */
#define SENSE_ERR_SCHEMA         -12  /* unsupported required TLV */
#define SENSE_ERR_IO             -13  /* backing source unreadable */
#define SENSE_ERR_NOMEM          -14  /* allocation failed */
#define SENSE_ERR_STATE          -15  /* lifecycle transition is forbidden */

/* ═══════════════════════════════════════════════════════════════════════════
 * SENSE Wave 2 — registry, domain ring, filters   (SENSE051-SENSE075)
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * Everything below is additive to ABI v1: no struct above changes size.
 * The ring is volatile and at-most-once. A consumer that falls behind loses
 * events and is told how many; it never stalls the producer and never stalls
 * another consumer. Loss is counted and reported — never silence.
 */

/* ── Owner encoding (SENSE064) ────────────────────────────────────────────
 * sense_owner_t packs (instance << 32) | generation. Generation 0 is not an
 * epoch: it creates nothing and every call carrying it is refused, in the
 * same spirit as plat_owner_t.generation == 0 elsewhere in the platform.
 */
#define SENSE_OWNER_MAKE(inst, gen) \
    (((sense_owner_t)(uint32_t)(inst) << 32) | (uint32_t)(gen))
#define SENSE_OWNER_INST(o)  ((uint32_t)((o) >> 32))
#define SENSE_OWNER_GEN(o)   ((uint32_t)((o) & 0xFFFFFFFFu))

/* ── Registry bounds (SENSE053) ───────────────────────────────────────────*/
#define SENSE_SOURCES_MAX     64u   /* initial v2 registry capacity */
#define SENSE_SOURCE_ID_MAX   64u

/* ── Ring bounds (SENSE061, SENSE063) ─────────────────────────────────────*/
#define SENSE_RING_SLOTS      64u   /* power of two; wrap is well defined */
#define SENSE_SLOT_BYTES     512u   /* a record larger than this is refused */

/* ── Priority classes (SENSE075) ──────────────────────────────────────────
 * Each class owns a reserved quarter of the ring that no other class may
 * consume. P0/P1 may borrow the unused share of P2/P3; P2/P3 may never
 * borrow from the P0/P1 reserve. That is what makes the reserve a reserve
 * and not a hint, and it is why flooding P3 cannot starve P0.
 */
#define SENSE_PRIO_P0   0u   /* security-critical */
#define SENSE_PRIO_P1   1u   /* policy-relevant */
#define SENSE_PRIO_P2   2u   /* routine observation */
#define SENSE_PRIO_P3   3u   /* bulk / telemetry */
#define SENSE_PRIO_MAX  4u
#define SENSE_PRIO_RESERVE (SENSE_RING_SLOTS / SENSE_PRIO_MAX)  /* 16 each */

/* ── Loss policy (SENSE076-080 land here in the next slice) ───────────────*/
typedef enum sense_loss_policy {
    SENSE_LOSS_DROP_OLDEST = 0,  /* ring overwrites; consumer sees a gap */
    SENSE_LOSS_DROP_NEWEST = 1   /* publish refused; producer sees the drop */
} sense_loss_policy_t;

/* ── Ring lifecycle ───────────────────────────────────────────────────────*/
int  sense_ring_init(void);
void sense_ring_fini(void);

/* Publish one already-encoded record. Returns SENSE_OK, or:
 *   SENSE_ERR_INVAL   len == 0, len > SENSE_SLOT_BYTES, or prio >= SENSE_PRIO_MAX
 *   SENSE_ERR_STALE_OWNER  generation 0, or an owner that has been retired
 * The size check happens before the slot is reserved, so a refused publish
 * never leaves a half-committed slot behind (SENSE063).
 *
 * A publish is never refused for want of room: each priority class writes
 * into its own region and displaces only its own oldest record (SENSE075).
 * That is what makes the reserve unconditional — and it is also why a flood
 * costs the flooder its own history rather than someone else's.
 */
int  sense_ring_publish(sense_owner_t owner, uint32_t priority,
                        uint32_t event_class,
                        const uint8_t *wire, size_t len);

/* Producer-side totals. Any pointer may be NULL. */
int  sense_ring_stats(uint64_t *published_out, uint64_t *refused_out,
                      uint64_t *overwritten_out);

/* Live record count for one priority class — the occupancy the reserve
 * defends. */
int  sense_ring_class_live(uint32_t priority, uint32_t *live_out);

/* ── Owner retirement (SENSE068) ──────────────────────────────────────────
 * Retire `owner` itself and every subscription/publish right from the same
 * instance at an older generation. A cursor whose owner is retired is closed
 * on its next use and reports SENSE_ERR_STALE_OWNER rather than quietly
 * returning nothing. Newer generations remain live.
 */
int  sense_owner_retire(sense_owner_t owner);
void sense_owner_retire_clear(void);   /* test/teardown helper */

/* ── Immutable view validity (SENSE070) ───────────────────────────────────
 * A view points into a ring slot. Retaining it past the moment the producer
 * wraps onto that slot is forbidden; this predicate is how a caller proves
 * it did not. 1 = the slot still holds the record the view describes.
 */
int  sense_view_valid(const sense_event_view_t *ev);

/* ── Consumer counters (SENSE067) ─────────────────────────────────────────*/
typedef struct sense_consumer_stats {
    uint64_t delivered;    /* records handed to this consumer */
    uint64_t dropped;      /* records overwritten before this consumer read */
    uint64_t filtered;     /* records the filter rejected */
    uint64_t lag;          /* records currently behind the producer */
    uint64_t high_water;   /* the largest lag ever observed */
    uint32_t generation;   /* bumps on every filter update (SENSE074) */
} sense_consumer_stats_t;

int sense_consumer_stats(sense_cursor_t *cur, sense_consumer_stats_t *out);

/* Number of consumer slots currently open. */
int sense_consumer_count(void);

/* ── Filter AST (SENSE071-SENSE074) ───────────────────────────────────────
 * Bounded and total: a fixed node ceiling, a closed field set and a closed
 * operator set. No regex, no callback, no allocation — a filter that could
 * call out would put consumer code on the producer's path, which is the one
 * thing a bounded evaluator must not do.
 */
#define SENSE_FILTER_NODES_MAX 16u

typedef enum sense_filter_op {
    SENSE_FOP_TRUE = 0,   /* match everything */
    SENSE_FOP_EQ,
    SENSE_FOP_NE,
    SENSE_FOP_LT,
    SENSE_FOP_GT,
    SENSE_FOP_MASK,       /* (field & value) != 0 */
    SENSE_FOP_AND,        /* children a, b */
    SENSE_FOP_OR,
    SENSE_FOP_NOT         /* child a */
} sense_filter_op_t;

typedef enum sense_filter_field {
    SENSE_FF_EVENT_CLASS = 0,
    SENSE_FF_PAYLOAD_TYPE,
    SENSE_FF_TRUST,
    SENSE_FF_QUALITY,
    SENSE_FF_FLAGS,
    SENSE_FF_SEQUENCE,
    SENSE_FF_ENTITY_ID,      /* privacy-classed: needs a lease */
    SENSE_FF_PARENT_ENTITY,  /* privacy-classed: needs a lease */
    SENSE_FF_MAX
} sense_filter_field_t;

typedef struct sense_filter_node {
    uint8_t  op;      /* sense_filter_op_t */
    uint8_t  field;   /* sense_filter_field_t; ignored for AND/OR/NOT/TRUE */
    uint8_t  a;       /* child index, for AND/OR/NOT */
    uint8_t  b;       /* child index, for AND/OR */
    uint64_t value;
} sense_filter_node_t;

typedef struct sense_filter {
    sense_filter_node_t nodes[SENSE_FILTER_NODES_MAX];
    uint8_t             n_nodes;
    uint8_t             root;
    uint8_t             compiled;    /* 0 until sense_filter_compile succeeds */
    uint8_t             _pad;
    uint32_t            privacy_need;/* SENSE_PRIV_F_* the filter touches */
    uint64_t            digest;      /* FNV-1a over the canonical node array */
} sense_filter_t;

/* Privacy lease a consumer holds. 0 = none. */
#define SENSE_LEASE_NONE      0u
#define SENSE_LEASE_IDENTITY  (1u << 3)   /* mirrors SENSE_PRIV_F_IDENTITY */

/* Compile and validate. Returns SENSE_OK and fills digest/privacy_need, or:
 *   SENSE_ERR_INVAL    empty, over the node ceiling, bad op/field, child
 *                      index out of range, or a cycle (child >= own index)
 *   SENSE_ERR_PRIVACY  the filter reads a privacy-classed field and `lease`
 *                      does not grant it (SENSE073)
 * Compilation happens before subscription so a filter that cannot be
 * evaluated never reaches the ring (SENSE072).
 */
int sense_filter_compile(sense_filter_t *f, uint32_t lease);

/* Evaluate a compiled filter against a decoded event. 1 match, 0 no match.
 * An uncompiled filter matches nothing: a filter that was never checked must
 * not behave like "allow all". */
int sense_filter_eval(const sense_filter_t *f, const sense_event_view_t *ev);

/* Attach a compiled filter to an open cursor. This is a new consumer
 * generation (SENSE074): the cursor's generation counter bumps and the
 * digest recorded on the subscription changes with it. */
int sense_consumer_set_filter(sense_cursor_t *cur, const sense_filter_t *f);

/* The digest of the filter currently attached, or 0 when none. */
uint64_t sense_consumer_filter_digest(sense_cursor_t *cur);

/* ── Callback-under-lock detector (SENSE056) ──────────────────────────────
 * Every external callback — a source vtable entry, a consumer sink — is
 * invoked with no registry or ring lock held. This counter is how that is
 * proved rather than asserted: it increments if a callback is ever entered
 * while this thread holds one, and a test reads it instead of relying on a
 * deadlock to show up.
 */
unsigned sense_callback_under_lock_violations(void);
void     sense_callback_under_lock_reset(void);

/* Registered source count, and whether a source's capability is published.
 * The second is SENSE052's observable half: a caller can tell "start()
 * returned 0" from "the capability is actually up". */
int sense_source_count(void);
int sense_source_cap_published(const char *source_id);

/* ── Synthetic v2 source (SENSE060) ───────────────────────────────────────
 * Deterministic: the same seed emits the same sequence and injects a gap at
 * the same place, so a loss test has something to be exactly right about.
 */
int  sense_synth_register(const char *source_id, uint32_t seed,
                          uint32_t gap_every);
int  sense_synth_emit(const char *source_id, sense_owner_t owner,
                      uint32_t count, uint32_t priority);
include/platx/sense_action.h
/* platx/sense_action.h — SENSE181–SENSE185 typed action intent state machine.
 *
 * A detector never calls a responder. It submits a *typed intent* (data), and
 * the only path to an effect runs through this state machine's gates:
 *   REQUESTED -> AUTHORIZED -> STARTED -> {VERIFIED | FAILED} [-> ROLLED_BACK]
 * Authorization checks target-identity freshness (SENSE182) and policy
 * generation / lease / risk / maintenance (SENSE183). Intents are idempotent
 * by request_id and carry a terminal receipt (SENSE184). The executor is
 * invoked exclusively by sense_intent_start under AUTHORIZED — never by the
 * submitter — which is what SENSE199 (zero direct response) rests on.
 */

#define SENSE_INTENT_MAX 256u

typedef enum sense_intent_state {
    SENSE_INTENT_REQUESTED   = 0,
    SENSE_INTENT_AUTHORIZED  = 1,
    SENSE_INTENT_STARTED     = 2,
    SENSE_INTENT_VERIFIED    = 3,   /* terminal */
    SENSE_INTENT_FAILED      = 4,   /* terminal */
    SENSE_INTENT_ROLLED_BACK = 5    /* terminal */
} sense_intent_state_t;

typedef enum sense_action_kind {
    SENSE_ACT_NONE   = 0,
    SENSE_ACT_KILL   = 1,
    SENSE_ACT_QUARANTINE = 2,
    SENSE_ACT_ISOLATE_NET = 3,
    SENSE_ACT_FREEZE = 4
} sense_action_kind_t;

/* Result codes. */
#define SENSE_ACT_OK              0
#define SENSE_ACT_ERR_INVAL      -1
#define SENSE_ACT_ERR_FULL       -2
#define SENSE_ACT_ERR_STATE      -3   /* illegal transition */
#define SENSE_ACT_ERR_STALE_TGT  -4   /* target identity not fresh */
#define SENSE_ACT_ERR_POLICY     -5   /* policy generation mismatch */
#define SENSE_ACT_ERR_LEASE      -6   /* lease expired */
#define SENSE_ACT_ERR_RISK       -7   /* risk over ceiling */
#define SENSE_ACT_ERR_MAINT      -8   /* system in maintenance */

typedef struct sense_intent_receipt {
    uint64_t             request_id;
    sense_intent_state_t state;
    int                  code;          /* last result code */
    sense_action_kind_t  kind;
    uint64_t             target_entity;
} sense_intent_receipt_t;

/* Executor: the ONLY effect path. Returns 0 on success. Invoked solely by
 * sense_intent_start(). The submitter has no handle to it. */
typedef int (*sense_action_exec_fn)(sense_action_kind_t kind,
                                    uint64_t target_entity, void *ctx);

void sense_action_init(void);
void sense_action_set_executor(sense_action_exec_fn fn, void *ctx);

/* Deterministic clock for lease checks (tests). 0 restores real monotonic. */
void sense_action_set_clock(uint64_t ns);

/* Policy/maintenance state the authorizer checks against (SENSE183). */
void sense_action_set_policy(uint64_t policy_generation, uint32_t risk_ceiling,
                             int maintenance_mode);

/* Current authoritative identity of a target (SENSE182 freshness source). */
void sense_action_set_target_identity(uint64_t target_entity, uint64_t birth_ns);

/* Submit a typed intent (idempotent by request_id). Returns an intent index
 * (>=0) or a negative SENSE_ACT_ERR_*. Re-submitting the same request_id
 * returns the SAME index without creating a new intent (SENSE184). */
int sense_intent_submit(uint64_t request_id, sense_action_kind_t kind,
                        uint64_t target_entity, uint64_t target_birth_ns);

/* Authorize: checks target freshness + policy_gen + lease + risk + maintenance.
 * On success -> AUTHORIZED; on failure the intent stays REQUESTED and a typed
 * error is returned (SENSE182/183). */
int sense_intent_authorize(int idx, uint64_t policy_generation,
                           uint64_t lease_deadline_ns, uint32_t risk);

/* Start: requires AUTHORIZED; invokes the executor exactly once -> STARTED. */
int sense_intent_start(int idx);

/* Verify the effect: STARTED -> VERIFIED (ok!=0) or FAILED (ok==0). */
int sense_intent_verify(int idx, int ok);

/* Rollback from STARTED or FAILED -> ROLLED_BACK. */
int sense_intent_rollback(int idx);

int  sense_intent_get(int idx, sense_intent_receipt_t *out);
uint64_t sense_action_exec_count(void);   /* how many times the executor ran */
include/platx/sense_checkpoint.h
/* platx/sense_checkpoint.h — SENSE196/SENSE197 signed SENSE checkpoint chain.
 *
 * A hash-linked chain of checkpoints, each signed under the SENSE CHECKPOINT
 * signer domain (platx/sense_signer.h) — a domain distinct from capsules, so a
 * capsule signature can never validate a checkpoint. Verification detects chain
 * corruption (broken link or tampered payload), a signature made under the
 * wrong domain, and an unsigned checkpoint. No new crypto stack: reuses the
 * platform HMAC-SHA256 and SHA-256.
 */

#define SENSE_CKPT_MAX 256u

typedef struct sense_checkpoint {
    uint64_t seq;
    uint8_t  prev_hash[32];     /* hash of the previous checkpoint (0 for genesis) */
    uint8_t  payload_hash[32];  /* SHA-256 of the checkpoint payload */
    uint8_t  sig[32];           /* CHECKPOINT-domain MAC; all-zero => unsigned */
} sense_checkpoint_t;

#define SENSE_CKPT_OK            0
#define SENSE_CKPT_ERR_INVAL    -1
#define SENSE_CKPT_ERR_FULL     -2
#define SENSE_CKPT_ERR_LINK     -3   /* prev_hash does not match chain */
#define SENSE_CKPT_ERR_SIG      -4   /* signature invalid / wrong domain */
#define SENSE_CKPT_ERR_UNSIGNED -5   /* checkpoint carries no signature */

/* (Re)initialise the chain with a signing key (kept for the chain's life). */
void sense_ckpt_init(const uint8_t *key, size_t keylen);

/* Append a payload as a new signed checkpoint. Returns its seq or a negative
 * SENSE_CKPT_ERR_*. */
int sense_ckpt_append(const uint8_t *payload, size_t len);

uint32_t sense_ckpt_count(void);
int      sense_ckpt_get(uint32_t idx, sense_checkpoint_t *out);

/* Verify the whole chain: linkage + each signature under the CHECKPOINT domain.
 * Returns SENSE_CKPT_OK or the first failure's error; *bad_idx_out (optional)
 * gets the failing index. */
int sense_ckpt_verify_chain(uint32_t *bad_idx_out);

/* Test hooks (SENSE197 negative cuts): mutate the in-memory chain. */
void sense_ckpt_corrupt_payload(uint32_t idx);   /* flip a payload_hash byte */
void sense_ckpt_clear_signature(uint32_t idx);   /* make a checkpoint unsigned */
void sense_ckpt_sign_wrong_domain(uint32_t idx); /* re-sign under CAPSULE domain */
include/platx/sense_coverage.h
/* SPDX-License-Identifier: GPL-2.0
 *
 * platx/sense_coverage.h — SENSE coverage plane: эпохи, дыры, снапшоты,
 *                          барьеры, обнаружение слепоты и политики потерь.
 *
 * Карточки: SENSE078-SENSE080 (политики потерь), SENSE081-SENSE094
 *           (покрытие), SENSE095-SENSE096 (поверхность для CLI).
 *
 * Почему отдельный заголовок, а не дополнение sense.h
 * ---------------------------------------------------
 * sense.h описывает, ЧТО наблюдалось: реестр источников, кольцо, курсоры,
 * фильтры. Этот заголовок описывает, ЗА КАКОЙ ПЕРИОД наблюдение вообще
 * имело место и когда по нему допустимо рассуждать. Второе не выводится из
 * первого: пустое кольцо одинаково выглядит и когда ничего не происходило,
 * и когда источник умер.
 *
 * Практический смысл разделения: sense.h заморожен как SENSE_ABI_VERSION 1
 * и его правит другой участок работы. Покрытие добавляется рядом, не
 * переоткрывая замороженный контракт.
 *
 * Главные инварианты
 * ------------------
 *   • BLIND никогда не переходит в HEALTHY сам по себе.
 *   • Дыра с неизвестным концом сохраняет неизвестность, а не получает
 *     правдоподобное число.
 *   • Находка, интервал которой пересекает дыру, не может быть доказанной.
 *   • Ни одна политика потерь не теряет запись молча: сумма счётчиков
 *     всегда сходится с разницей предложенного и принятого.
 */

/* ── Границы ────────────────────────────────────────────────────────────
 *
 * Дельта-окно снапшота ограничено намеренно: снапшот, догоняющий
 * неограниченный поток изменений, никогда не завершится, а «согласованный
 * срез» превратится в бесконечную погоню.
 */
#define SENSE_SNAPSHOT_DELTA_MAX  256u

/* ══════════════════════════════════════════════════════════════════════
 * Политики потерь (SENSE078-SENSE080)
 * ══════════════════════════════════════════════════════════════════════
 *
 * sense.h определяет DROP_OLDEST и DROP_NEWEST — то, как ведёт себя само
 * кольцо при переполнении. Здесь добавляются две политики ДОПУСКА,
 * работающие до кольца: они решают, дойдёт ли запись до публикации.
 *
 * Разделение не косметическое. DROP_* — свойство хранилища, оно
 * срабатывает, когда места уже нет. SAMPLE и AGGREGATE — осознанное
 * прореживание на входе, применяемое и к незаполненному кольцу.
 */
typedef enum sense_admit_policy {
    SENSE_ADMIT_ALL       = 0,  /* пропускать всё; решает только кольцо   */
    SENSE_ADMIT_SAMPLE    = 1,  /* детерминированная выборка 1 из N       */
    SENSE_ADMIT_AGGREGATE = 2,  /* схлопывание в счётчик, ограниченное    */
} sense_admit_policy_t;

/*
 * Установить политику допуска.
 *
 * Коэффициент 0 для SAMPLE отвергается: он означал бы деление на ноль, а
 * молчаливая подстановка 1 превратила бы «выборка настроена неверно» в
 * «выборки нет» — ровно та подмена, которую запрещает контракт закрытия.
 *
 * Возвращает SENSE_OK либо SENSE_ERR_INVAL.
 */
int sense_admit_set(sense_admit_policy_t policy, uint32_t sample_n);

/*
 * Спросить, допускается ли очередная запись.
 *
 * Возвращает 1 — публиковать, 0 — запись отсеяна (счётчик уже увеличен и
 * дыра уже открыта). Детерминированность обязательна: один и тот же вход
 * обязан давать один и тот же результат, иначе инцидент невозможно
 * воспроизвести.
 */
int sense_admit_check(void);

/* Счётчики отсева. Любой указатель может быть NULL. */
int sense_admit_stats(uint64_t *offered_out, uint64_t *sampled_out,
                      uint64_t *aggregated_out);

/* Сбросить счётчики и состояние допуска (тесты, teardown). */
void sense_admit_reset(void);

/*
 * SENSE080: сверка учёта.
 *
 * Возвращает 1, если предложено == принято + отсеяно + отвергнуто кольцом.
 * Расхождение означает, что запись исчезла, не попав ни в один счётчик, —
 * то есть скрытую потерю наблюдения. Функция существует именно для того,
 * чтобы этот инвариант проверялся, а не декларировался.
 */
int sense_loss_accounting_balanced(void);

/* ══════════════════════════════════════════════════════════════════════
 * Эпохи покрытия (SENSE081-SENSE083)
 * ══════════════════════════════════════════════════════════════════════ */

/*
 * Причина открытия новой эпохи (SENSE082).
 *
 * Эпоха — интервал, внутри которого набор источников, схема и фильтры
 * неизменны. Сравнивать наблюдения через границу изменения нельзя:
 * «событий такого типа не было» может означать «их перестали собирать».
 */
typedef enum sense_epoch_reason {
    SENSE_EPOCH_INIT       = 0,  /* первая эпоха при старте модуля       */
    SENSE_EPOCH_SCHEMA     = 1,  /* изменилась схема                     */
    SENSE_EPOCH_FILTER     = 2,  /* изменился фильтр потребителя         */
    SENSE_EPOCH_SOURCE_SET = 3,  /* источник добавлен или убран          */
    SENSE_EPOCH_ADAPTIVE   = 4,  /* адаптивное изменение сбора           */
    SENSE_EPOCH_RECOVERY   = 5,  /* пересборка покрытия после слепоты    */
} sense_epoch_reason_t;

/* Открыть новую эпоху. Возвращает её монотонный идентификатор.
 * Идентификаторы не переиспользуются даже после закрытия. */
uint64_t sense_epoch_open(sense_epoch_reason_t reason);

/* Закрыть текущую эпоху. SENSE_ERR_INVAL, если открытой эпохи нет. */
int sense_epoch_close(void);

/* Текущий идентификатор эпохи; 0 означает «эпоха не назначена». */
uint64_t sense_epoch_current(void);

/* Открыта ли сейчас эпоха. */
int sense_epoch_is_open(void);

/* Причина, по которой была открыта текущая эпоха. */
sense_epoch_reason_t sense_epoch_reason(void);

/* ══════════════════════════════════════════════════════════════════════
 * Дыры в покрытии (SENSE084-SENSE086)
 * ══════════════════════════════════════════════════════════════════════
 *
 * Допустимые переходы:
 *
 *   NONE      → OPEN         дыра обнаружена
 *   OPEN      → CLOSED       закрыта, пропущенное восстановлено
 *   OPEN      → ABANDONED    признана неразрешимой
 *   CLOSED    → OPEN         следующая дыра
 *   ABANDONED → OPEN         следующая дыра
 *
 * Запрещено всё, что делает дыру несуществовавшей задним числом.
 */
typedef enum sense_gap_state {
    SENSE_GAP_NONE      = 0,
    SENSE_GAP_OPEN      = 1,
    SENSE_GAP_CLOSED    = 2,
    SENSE_GAP_ABANDONED = 3,
} sense_gap_state_t;

/* Открыть дыру с первой пропущенной последовательностью.
 * Повторный вызов при уже открытой дыре расширяет её, а не создаёт
 * вторую: две одновременные дыры сделали бы «первую пропущенную»
 * неоднозначной. */
int sense_gap_open(uint64_t first_missing);

/*
 * Уточнить последнюю пропущенную последовательность.
 *
 * SENSE085: пока конец неизвестен, он остаётся неизвестным. Подставить
 * «текущая голова минус один» соблазнительно и неверно — оценка стала бы
 * цифрой, которой верят.
 */
int sense_gap_note_last_missing(uint64_t last_missing);

/*
 * Закрыть дыру.
 *
 * Требует известного конца: закрытие означает «мы знаем ровно, что было
 * пропущено, и восстановили это». Без известного конца правильный исход —
 * ABANDONED, а не CLOSED.
 */
int sense_gap_close(void);

/* Признать дыру неразрешимой. */
int sense_gap_abandon(void);

sense_gap_state_t sense_gap_state(void);

/* Границы текущей дыры. last_known = 0 означает, что конец не измерен и
 * значение last_out использовать нельзя. */
int sense_gap_bounds(uint64_t *first_out, uint64_t *last_out,
                     int *last_known_out);

/* Счётчики дыр за всё время. */
int sense_gap_counters(uint64_t *opened_out, uint64_t *closed_out,
                       uint64_t *abandoned_out);

/*
 * SENSE086: доказуема ли находка на интервале [from, to].
 *
 * Открытая и брошенная дыры одинаково разрушают доказательство: часть
 * интервала не наблюдалась. Закрытая — нет, пропущенное восстановлено.
 * Дыра с неизвестным концом считается тянущейся до бесконечности:
 * неизвестность не сужается в пользу заявителя.
 */
int sense_finding_provable(uint64_t from_seq, uint64_t to_seq);

/* ══════════════════════════════════════════════════════════════════════
 * Барьер (SENSE091)
 * ══════════════════════════════════════════════════════════════════════ */

/*
 * Установить барьер: всё отправленное источниками до этой точки уже в
 * кольце.
 *
 * До барьера рассуждать об отсутствии факта нельзя — событие могло быть
 * ещё в пути. Это единственная конструкция, дающая право на отрицательное
 * утверждение.
 */
int sense_barrier_set(uint64_t sequence);

/*
 * Допустимо ли утверждать, что в точке `sequence` факта НЕ было.
 *
 * Требует одновременно: барьер установлен, точка не позже барьера, нет
 * незакрытой дыры и источник не слеп. Отказ любого условия означает, что
 * «не наблюдалось» значит «не наблюдали», а не «не было».
 */
int sense_may_prove_absence(uint64_t sequence);

/* ══════════════════════════════════════════════════════════════════════
 * Heartbeat и слепота (SENSE092-SENSE093)
 * ══════════════════════════════════════════════════════════════════════ */

/* Задать дедлайн: молчание дольше этого срока означает слепоту.
 * 0 отключает проверку. */
int sense_heartbeat_set_deadline(uint64_t deadline_ns);

/* Зафиксировать пришедший heartbeat.
 *
 * НЕ снимает состояние BLIND: живой источник доказал только собственную
 * жизнь. Что произошло за время молчания — по-прежнему неизвестно. */
int sense_heartbeat(uint64_t now_ns);

/*
 * Проверить дедлайн. Возвращает SENSE_ERR_BLIND, если источник только что
 * признан слепым, иначе SENSE_OK. Переход в BLIND открывает дыру: интервал
 * молчания обязан быть виден в потоке, а не остаться флагом состояния.
 */
int sense_blind_check(uint64_t now_ns);

int sense_is_blind(void);

/*
 * Попытка выйти из BLIND.
 *
 * SENSE093: разрешено только при выполнении обоих условий — открыта новая
 * эпоха (отличная от той, в которой наступила слепота) и проведена
 * реконсиляция. Нарушение любого оставляет состояние слепым.
 *
 * Именно здесь обычно и появляется тихая деградация: «источник снова
 * отвечает, значит всё хорошо». Не значит.
 */
int sense_blind_recover(int reconciled);

/* ══════════════════════════════════════════════════════════════════════
 * Снапшоты (SENSE087-SENSE090)
 * ══════════════════════════════════════════════════════════════════════ */

typedef enum sense_consistency {
    SENSE_CONS_ATOMIC        = 0, /* согласованный срез одной точки       */
    SENSE_CONS_BOUNDED_FUZZY = 1, /* срез в известном окне, дельта учтена */
    SENSE_CONS_BEST_EFFORT   = 2, /* границы неизвестны                   */
} sense_consistency_t;

typedef enum sense_snapshot_state {
    SENSE_SNAP_IDLE    = 0,
    SENSE_SNAP_RUNNING = 1,
    SENSE_SNAP_DONE    = 2,
    SENSE_SNAP_ABORTED = 3,
} sense_snapshot_state_t;

/* Начать снапшот с заявленной моделью консистентности.
 * SENSE_ERR_BUSY, если снапшот уже идёт: два одновременных дали бы два
 * «согласованных среза» с пересекающимися границами. */
int sense_snapshot_begin(sense_consistency_t consistency);

/* Учесть событие, пришедшее во время съёмки.
 * SENSE_ERR_FULL при переполнении дельта-окна. */
int sense_snapshot_delta(void);

/*
 * Завершить снапшот.
 *
 * SENSE089: при переполнении дельта-окна дыра остаётся открытой, а
 * заявленная консистентность понижается до BEST_EFFORT. Закрытие дыры
 * здесь означало бы утверждение, что состояние восстановлено полностью —
 * а часть изменений не попала ни в срез, ни в дельту.
 */
int sense_snapshot_end(void);

/* Прервать снапшот. Прерванная съёмка не даёт согласованного среза ни в
 * каком смысле, поэтому её консистентность — BEST_EFFORT. */
int sense_snapshot_abort(void);

sense_snapshot_state_t sense_snapshot_state(void);
sense_consistency_t    sense_snapshot_consistency(void);

/* ══════════════════════════════════════════════════════════════════════
 * Согласованный снимок состояния (SENSE094)
 * ══════════════════════════════════════════════════════════════════════ */

typedef struct sense_coverage_status {
    uint64_t epoch_id;
    int      epoch_open;
    sense_epoch_reason_t epoch_reason;

    sense_gap_state_t gap_state;
    uint64_t gap_first_missing;
    uint64_t gap_last_missing;
    int      gap_last_known;
    uint64_t gaps_opened;
    uint64_t gaps_closed;
    uint64_t gaps_abandoned;

    int      blind;
    uint64_t heartbeat_missed;

    sense_snapshot_state_t snap_state;
    sense_consistency_t    snap_consistency;
    int      snap_delta_overflowed;

    int      barrier_valid;
    uint64_t barrier_seq;

    uint64_t offered;
    uint64_t sampled_out;
    uint64_t aggregated;
} sense_coverage_status_t;

/*
 * Снять согласованный снимок.
 *
 * SENSE094: поля не читаются по одному. Между чтениями состояние может
 * измениться, и отчёт описал бы то, чего никогда не существовало: часть
 * цифр «до», часть «после». Такой отчёт хуже устаревшего, потому что
 * выглядит текущим. Возвращает SENSE_ERR_STALE_GEN, если снимок оказался
 * разорван, вместо того чтобы отдать смесь.
 */
int sense_coverage_status(sense_coverage_status_t *out);

/* Текстовый статус для CLI (SENSE095). Слепота и открытая дыра выводятся
 * явными словами: свод в один флаг здоровья — это и есть механизм, которым
 * UNKNOWN превращается в HEALTHY. */
int sense_coverage_status_text(char *buf, size_t bufsz);

/* ── Жизненный цикл плоскости покрытия ──────────────────────────────── */
int  sense_coverage_init(void);
void sense_coverage_fini(void);

/* Монотонное время в наносекундах. */
uint64_t sense_coverage_now_ns(void);
include/platx/sense_fim.h
/* platx/sense_fim.h — SENSE101–SENSE106 inotify file-integrity source.
 *
 * Turns raw inotify events into canonical FIM claims, and turns a lost-events
 * condition (IN_Q_OVERFLOW) into an explicit GAP that forces snapshot
 * reconciliation — loss is never silent. Watch descriptors map to a bounded
 * watched-root identity; rename is paired by cookie without needing full file
 * identity. Watch count, path bytes and rescan I/O are all bounded.
 *
 * The event-processing core (sense_fim_process_buffer) is pure and takes a raw
 * inotify buffer, so the overflow/GAP/reconcile path is tested deterministically
 * without having to provoke a real kernel queue overflow.
 */

#define SENSE_FIM_WATCH_MAX     256u    /* bounded watches (SENSE106) */
#define SENSE_FIM_PATH_MAX      1024u   /* bounded path bytes (SENSE106) */
#define SENSE_FIM_RESCAN_BUDGET 4096u   /* bounded rescan I/O ops (SENSE106) */

typedef enum sense_fim_kind {
    SENSE_FIM_CREATE = 1,
    SENSE_FIM_MODIFY = 2,
    SENSE_FIM_DELETE = 3,
    SENSE_FIM_RENAME = 4,   /* paired MOVED_FROM/MOVED_TO by cookie */
    SENSE_FIM_GAP    = 5,   /* IN_Q_OVERFLOW: coverage gap, reconcile required */
    SENSE_FIM_ROOT_LOST = 6  /* IN_DELETE_SELF/IN_MOVE_SELF/IN_IGNORED: watched root removed */
} sense_fim_kind_t;

typedef struct sense_fim_claim {
    sense_fim_kind_t kind;
    uint64_t         root_id;     /* bounded watched-root identity (SENSE103) */
    int              wd;          /* raw watch descriptor */
    uint32_t         cookie;      /* rename pairing cookie (SENSE104); 0 if none */
    char             name[256];   /* event name (time-qualified hint only) */
    char             name2[256];  /* rename destination (RENAME only) */
} sense_fim_claim_t;

/* Emit callback: an immutable view; the callee must not retain the pointer
 * past the call (bounded view, SENSE070 spirit). */
typedef void (*sense_fim_emit_fn)(const sense_fim_claim_t *claim, void *ctx);

typedef struct sense_fim {
    int               ino_fd;         /* inotify fd, -1 if not open */
    sense_fim_emit_fn emit;
    void             *ctx;
    /* watch table: wd -> root identity */
    struct { int wd; uint64_t root_id; char root[SENSE_FIM_PATH_MAX]; } watches[SENSE_FIM_WATCH_MAX];
    unsigned          watch_count;
    uint64_t          next_root_id;
    /* pending MOVED_FROM awaiting its MOVED_TO (rename pairing) */
    struct { uint32_t cookie; int wd; char name[256]; int pending; } move;
    /* counters (evidence) */
    uint64_t          claims_emitted;
    uint64_t          gaps_emitted;
    uint64_t          reconcile_count;
    uint64_t          rescan_ops;
    int               reconcile_needed;
} sense_fim_t;

/* Lifecycle. sense_fim_open initialises inotify (returns 0/-1). */
int  sense_fim_open (sense_fim_t *f, sense_fim_emit_fn emit, void *ctx);
void sense_fim_close(sense_fim_t *f);

/* Add a watched root. Enforces SENSE_FIM_WATCH_MAX and SENSE_FIM_PATH_MAX.
 * Returns the watch descriptor (>=0) or:
 *   -1 bad args / path too long / inotify error
 *   -2 watch table full (SENSE106) */
int  sense_fim_add_watch(sense_fim_t *f, const char *path);

/* Map a watch descriptor to its bounded root identity (SENSE103); 0 if unknown. */
uint64_t sense_fim_root_id(const sense_fim_t *f, int wd);

/* Pure event-processing core: consume a raw inotify buffer, emit claims/GAPs.
 * Returns the number of records processed. On an IN_Q_OVERFLOW record it emits
 * one GAP and sets reconcile_needed (SENSE102/105). */
int  sense_fim_process_buffer(sense_fim_t *f, const void *buf, size_t len);

/* Read whatever inotify has queued (non-blocking) and process it. */
int  sense_fim_pump(sense_fim_t *f);

/* Reconcile after a GAP: rescan watched roots within the I/O budget and clear
 * reconcile_needed. Returns the number of rescan ops performed (<= budget).
 * (SENSE105/106) */
uint32_t sense_fim_reconcile(sense_fim_t *f);
include/platx/sense_finding.h
/* platx/sense_finding.h — SENSE160-162: finding identity and provenance.
 *
 * A finding is an accusation. Three things have to be true of it before
 * anyone can act on it, and none of them were true of a struct carrying
 * only {detector_id, score, description}:
 *
 *  SENSE160 — it has a deterministic identity. The same detector reaching
 *    the same conclusion from the same inputs must produce the same
 *    finding ID on every host and every replay, or deduplication becomes
 *    guesswork and a golden replay cannot assert anything. The ID is
 *    computed from the causal inputs, not from a counter or a clock.
 *
 *  SENSE161 — it says what produced it: detector version, the baseline it
 *    was tuned against, the config generation in force, and the digest of
 *    the visibility contract that was satisfied when it started. A finding
 *    whose detector has since been retuned is a different claim, and the
 *    reader has to be able to tell.
 *
 *  SENSE162 — confidence is separate from severity, and neither is
 *    permitted to mean "compromise". A high-confidence anomaly is still an
 *    anomaly. Collapsing the two is how a tuning threshold turns into an
 *    incident declaration.
 *
 * Additive to ABI v1: no frozen struct changes.
 */

/* Causal inputs recorded per finding. Bounded: a detector that believes it
 * needs more than this to justify a conclusion is not making an argument a
 * human can check. */
#define SENSE_FINDING_CAUSES_MAX   8u
#define SENSE_FINDING_DESC_MAX   128u

/* ── Confidence (SENSE162) ───────────────────────────────────────────────
 * How sure the detector is that its own pattern matched. Explicitly NOT a
 * statement that the host is compromised. */
typedef enum sense_confidence {
    SENSE_CONF_UNKNOWN = 0,  /* inputs insufficient to say                */
    SENSE_CONF_LOW     = 1,  /* pattern matched, weak or partial evidence  */
    SENSE_CONF_MEDIUM  = 2,
    SENSE_CONF_HIGH    = 3   /* pattern matched on first-hand, whole data  */
} sense_confidence_t;

/* ── Verdict (SENSE162) ──────────────────────────────────────────────────
 * The strongest thing a *detector* is allowed to assert. Nothing in SENSE
 * emits COMPROMISE: that is a human or a response-plane conclusion drawn
 * from findings, never a detector's own output. */
typedef enum sense_finding_verdict {
    SENSE_VERDICT_ANOMALY       = 0,  /* deviates from baseline           */
    SENSE_VERDICT_POLICY_MATCH  = 1,  /* matched a stated policy rule     */
    SENSE_VERDICT_INDETERMINATE = 2   /* matched, but coverage was blind  */
} sense_finding_verdict_t;

/* One causal input: the event that contributed to the conclusion. */
typedef struct sense_cause {
    uint64_t sequence;      /* ring sequence of the contributing event    */
    uint64_t entity_id;     /* subject at that point                      */
    uint32_t payload_type;  /* what kind of event it was                  */
    uint32_t _pad;
} sense_cause_t;

/* Provenance of the producing detector (SENSE161). */
typedef struct sense_detector_provenance {
    uint32_t detector_id;
    uint32_t detector_version;
    uint64_t baseline_id;          /* which baseline it was tuned against */
    uint64_t config_generation;    /* config in force at emission         */
    uint64_t visibility_digest;    /* sense_vis_digest() of the contract  */
} sense_detector_provenance_t;

typedef struct sense_finding {
    /* SENSE160: derived, never assigned. Zero means "not yet computed". */
    uint64_t finding_id;

    sense_detector_provenance_t prov;

    sense_finding_verdict_t verdict;
    sense_confidence_t      confidence;
    uint32_t                severity;   /* 0-100, tuning knob, not truth  */
    uint32_t                n_causes;

    uint64_t entity_id;                 /* primary subject                */
    uint64_t trigger_sequence;          /* the event that completed it    */

    sense_cause_t causes[SENSE_FINDING_CAUSES_MAX];

    uint8_t  tactic;
    char     technique[8];
    char     description[SENSE_FINDING_DESC_MAX];
} sense_finding_t;

/* Record a causal input. Returns SENSE_ERR_FULL past SENSE_FINDING_CAUSES_MAX
 * rather than silently dropping the evidence that would have been cited. */
int sense_finding_add_cause(sense_finding_t *f, uint64_t sequence,
                            uint64_t entity_id, uint32_t payload_type);

/* SENSE160: compute and store the deterministic identity.
 *
 * Derived from the provenance and the causal set. The causal set is folded
 * order-independently, because the order in which two events reached the
 * detector is a scheduling artefact, not part of the claim: the same pair
 * arriving in either order is the same finding.
 *
 * Returns the id, and stores it in f->finding_id. */
uint64_t sense_finding_compute_id(sense_finding_t *f);

/* SENSE162: the outcome the visibility contract dictates when coverage was
 * blind. Applies the on-blind decision to a finding that has already
 * matched: lowers confidence and, where the contract said so, marks the
 * finding INDETERMINATE. A detector must not do this by hand — that is how
 * "we could not see" silently becomes "nothing was there". */
int sense_finding_apply_blind(sense_finding_t *f, int contract_lowers,
                              int contract_indeterminate);

const char *sense_confidence_str(sense_confidence_t c);
const char *sense_finding_verdict_str(sense_finding_verdict_t v);
include/platx/sense_findings.h
/* platx/sense_findings.h — SENSE175 findings/explain + SENSE195 forensic projection.
 *
 * A finding has a DETERMINISTIC id derived from (detector_id, epoch, sorted
 * causal inputs), so the same evidence always yields the same id and re-adding
 * it is idempotent. `sense findings` / `sense explain` are backed by the query
 * functions here (public API surface). The forensic projection selects and
 * orders CLAIM/FINDING/GAP records for evidence, dropping nothing silently:
 * an over-capacity projection reports explicit truncation.
 */

#define SENSE_FINDINGS_MAX     256u
#define SENSE_FINDING_INPUTS   8u

typedef struct sense_finding {
    uint64_t finding_id;      /* deterministic (SENSE160) */
    uint32_t detector_id;
    uint64_t epoch;
    uint32_t confidence;      /* 0..100 */
    uint32_t n_inputs;
    uint64_t inputs[SENSE_FINDING_INPUTS];   /* causal entity ids */
} sense_finding_t;

void sense_findings_init(void);

/* Add a finding; returns its deterministic id. Re-adding identical evidence
 * returns the same id and does not duplicate (idempotent). confidence is
 * clamped to 0..100 (SENSE162 — anomaly is not compromise). */
uint64_t sense_findings_add(uint32_t detector_id, uint64_t epoch,
                            uint32_t confidence,
                            const uint64_t *inputs, uint32_t n_inputs);

uint32_t sense_findings_count(void);
int      sense_findings_list(sense_finding_t *out, uint32_t max);   /* -> count written */

/* `sense explain <id>`: copy the finding (with its causal input list). */
int      sense_finding_explain(uint64_t finding_id, sense_finding_t *out);

/* ── Forensic projection (SENSE195) ── */
typedef enum sense_forensic_type {
    SENSE_FX_CLAIM   = 1,
    SENSE_FX_FINDING = 2,
    SENSE_FX_GAP     = 3
} sense_forensic_type_t;

typedef struct sense_forensic_rec {
    sense_forensic_type_t type;
    uint64_t              id;    /* claim/finding id or gap id */
    uint64_t              seq;   /* ordering key */
} sense_forensic_rec_t;

/* Project inputs into `out` ordered by seq. Returns the number written.
 * If more than `max` records are selected, writes `max` and sets
 * *truncated_out (never a silent drop). */
int sense_forensic_project(const sense_forensic_rec_t *in, uint32_t n_in,
                           sense_forensic_rec_t *out, uint32_t max,
                           int *truncated_out);
include/platx/sense_hostident.h
/* platx/sense_hostident.h — SENSE107–SENSE118 host entity identity index.
 *
 * A PID is never an identity. Every host object is keyed by a reuse-proof
 * tuple, so PID/fd/inode recycling produces a NEW entity plus an explicit
 * identity-conflict — never a silent overwrite. Pathnames are time-qualified
 * hints only and never enter a key. Tombstones are bounded by both count and
 * byte ceiling and expire on a TTL. The module is deterministic: the clock is
 * injectable so TTL/GC behaviour is tested without sleeping.
 *
 * Namespaced sense_ident_* to stay independent of the legacy observe/ index.
 */

#define SENSE_IDENT_ENTITY_MAX     4096u
#define SENSE_IDENT_TOMBSTONE_MAX   512u
#define SENSE_IDENT_TOMBSTONE_BYTES (SENSE_IDENT_TOMBSTONE_MAX * 32u) /* byte ceiling */
#define SENSE_IDENT_TTL_NS          (30ull * 1000000000ull)           /* 30 s */
#define SENSE_IDENT_LINEAGE_MAX      16u   /* bounded parent lineage (SENSE110) */
#define SENSE_IDENT_PATH_HINT_MAX    256u

typedef enum sense_ident_kind {
    SENSE_ENT_PROCESS = 1,
    SENSE_ENT_THREAD  = 2,
    SENSE_ENT_FILE    = 3,
    SENSE_ENT_SOCKET  = 4,
    SENSE_ENT_CGROUP  = 5,
    SENSE_ENT_NS      = 6
} sense_ident_kind_t;

/* Lifecycle. destroy() clears the whole index. */
void sense_ident_init(void);
void sense_ident_destroy(void);

/* Deterministic clock control (tests). Passing 0 restores the real monotonic
 * clock. sense_ident_now() reports the clock the module currently uses. */
void     sense_ident_set_clock(uint64_t ns);
void     sense_ident_advance_clock(uint64_t delta_ns);
uint64_t sense_ident_now(void);

/* ── Entity resolution (SENSE107/109/111/114/116) ────────────────────────
 * Each returns a stable non-zero entity id for a given reuse-proof key, or 0
 * if the table is full. Re-resolving the SAME key returns the SAME id. A key
 * that collides on the recycled portion but differs on the reuse-proof
 * portion (birth_ns / inode generation / socket generation) retires the old
 * entity and mints a new id, counting one identity conflict. */
uint64_t sense_ident_process(uint64_t boot_id_hash, uint64_t birth_ns,
                             uint32_t pid, uint32_t pidns_inum);
uint64_t sense_ident_thread(uint64_t process_entity_id, uint32_t tid,
                            uint64_t birth_ns);
uint64_t sense_ident_file(uint64_t mntns_inum, uint64_t mount_id,
                          uint64_t dev, uint64_t ino, uint64_t generation);
uint64_t sense_ident_socket(uint64_t netns_inum, uint64_t cookie,
                            uint16_t protocol, uint64_t generation);
uint64_t sense_ident_cgroup(uint64_t cgroup_inum);
uint64_t sense_ident_namespace(uint64_t ns_inum, uint32_t ns_kind);

/* Pathname is a time-qualified hint, never part of identity (SENSE112).
 * Attaching a hint to an entity does not change its id. */
int         sense_ident_set_path_hint(uint64_t entity_id, const char *path);
const char *sense_ident_path_hint(uint64_t entity_id, uint64_t *hint_ns_out);

/* Bounded parent lineage with explicit truncation (SENSE110). Returns the
 * number of ancestors written (<= max, <= SENSE_IDENT_LINEAGE_MAX) and sets
 * *truncated_out if the real chain was longer than what was returned. */
int sense_ident_set_parent(uint64_t child_entity, uint64_t parent_entity);
int sense_ident_lineage(uint64_t entity_id, uint64_t *out, int max,
                        int *truncated_out);

/* Retire an entity (moves it to a bounded tombstone). */
int sense_ident_tombstone(uint64_t entity_id);

/* Garbage-collect tombstones older than the TTL. Returns the count evicted. */
uint32_t sense_ident_gc(void);

/* Counters (evidence). */
uint64_t sense_ident_conflicts(void);   /* identity conflicts observed */
uint32_t sense_ident_live_count(void);   /* live entities */
uint32_t sense_ident_tombstone_count(void);
uint32_t sense_ident_tombstone_bytes(void);
include/platx/sense_metrics.h
/* platx/sense_metrics.h — SENSE v1 typed resource metric snapshot API
 * SENSE138-143: CPU/memory/PSI/disk/network/HV collectors.
 * Copyright (c) 2025-2026 PlatX Authors.  SPDX-License-Identifier: MIT
 */

/* ── Collection limits ─────────────────────────────────────────────────── */
#define SENSE_CPU_MAX        512u
#define SENSE_NET_IFACE_MAX   64u
#define SENSE_DISK_DEV_MAX    32u
#define IFNAMSIZ 16

/* ── Metric class IDs ──────────────────────────────────────────────────── */
#define SENSE_METRIC_CLASS_CPU   0u
#define SENSE_METRIC_CLASS_MEM   1u
#define SENSE_METRIC_CLASS_PSI   2u
#define SENSE_METRIC_CLASS_DISK  3u
#define SENSE_METRIC_CLASS_NET   4u
#define SENSE_METRIC_CLASS_HV    5u

/* ── Snapshot structs ──────────────────────────────────────────────────── */
typedef struct {
    uint64_t epoch_ns;
    uint32_t struct_version;
    uint32_t ncpus;
    struct {
        uint32_t cpu_id;
        uint64_t user_ns, sys_ns, iowait_ns, irq_ns;
        uint64_t softirq_ns, steal_ns, idle_ns, total_ns;
        uint32_t util_pct_e2;
        uint8_t  online, _pad[3];
    } cpu[SENSE_CPU_MAX];
} sense_metric_cpu_t;

typedef struct {
    uint64_t epoch_ns;
    uint32_t struct_version, _pad;
    uint64_t total_bytes, free_bytes, available_bytes;
    uint64_t anon_bytes, file_bytes;
    uint64_t slab_reclaimable_bytes, slab_unreclaimable_bytes, pgtables_bytes;
    uint64_t swap_total_bytes, swap_free_bytes;
    uint32_t commit_pct_e2, _pad2;
} sense_metric_memory_t;

typedef struct {
    uint32_t avg10_pct_e2, avg60_pct_e2, avg300_pct_e2;
    uint64_t total_us;
} sense_psi_line_t;

typedef struct {
    uint64_t         epoch_ns;
    uint32_t         struct_version, _pad;
    sense_psi_line_t cpu_some, mem_some, mem_full, io_some, io_full;
} sense_metric_psi_t;

typedef struct {
    char     dev_name[32];
    uint64_t reads_completed, reads_merged, read_bytes, read_time_ns;
    uint64_t writes_completed, writes_merged, write_bytes, write_time_ns;
    uint64_t discards_completed, discard_bytes;
    uint64_t io_in_flight, io_time_ns, weighted_io_time_ns;
    uint32_t await_read_ns, await_write_ns, saturation_pct_e2, _pad;
} sense_disk_stat_t;

typedef struct {
    uint64_t          epoch_ns;
    uint32_t          struct_version, ndevs;
    sense_disk_stat_t dev[SENSE_DISK_DEV_MAX];
} sense_metric_disk_t;

typedef struct {
    char     iface[IFNAMSIZ];
    uint8_t  _pad_iface[16];
    uint64_t rx_bytes, rx_packets, rx_errors, rx_dropped, rx_missed;
    uint64_t tx_bytes, tx_packets, tx_errors, tx_dropped;
    uint32_t speed_mbps, duplex;
    uint8_t  up, _pad2[7];
} sense_net_stat_t;

typedef struct {
    uint64_t         epoch_ns;
    uint32_t         struct_version, nifaces;
    sense_net_stat_t iface[SENSE_NET_IFACE_MAX];
} sense_metric_network_t;

typedef struct {
    uint64_t epoch_ns;
    uint32_t struct_version, vcpu_count;
    uint64_t steal_ns_total, steal_ns_delta;
    uint64_t balloon_allocated_bytes, balloon_target_bytes;
    uint32_t vcpu_cap_pct_e2, hypervisor_type;
    char     hypervisor_sig[32];
} sense_metric_hv_t;

/* ── Public API ─────────────────────────────────────────────────────────── */
int  sense_metrics_init(void);
void sense_metrics_destroy(void);
int  sense_metrics_refresh(void);
int  sense_metrics_available(uint32_t class_id);

int  sense_metrics_snapshot_cpu    (sense_metric_cpu_t     *out);
int  sense_metrics_snapshot_memory (sense_metric_memory_t  *out);
int  sense_metrics_snapshot_psi    (sense_metric_psi_t     *out);
int  sense_metrics_snapshot_disk   (sense_metric_disk_t    *out);
int  sense_metrics_snapshot_network(sense_metric_network_t *out);
int  sense_metrics_snapshot_hv     (sense_metric_hv_t      *out);
include/platx/sense_mod.h
/* platx/sense_mod.h — SENSE v1 module lifecycle public types (SENSE201-202)
 * Copyright (c) 2025-2026 PlatX Authors.  SPDX-License-Identifier: MIT
 *
 * ─────────────────────────────────────────────────────────────────────────
 * РЕТИРОВАННЫЙ КОНТРАКТ — 28.08.2026. Реализации нет ни в одной сборке.
 *
 * Все три копии sense_mod.c (src/observe/, src/observe/_to_delete/,
 * src/sense/) ретированы docs/ADR_SENSE_CANONICAL_TREE.md, и ни одна из них
 * не линкуется с каноническим деревом: они вызывают девять функций
 * (sense_registry_register/deregister/start/stop_all/validate/prepare/commit,
 * sense_entity_index_init/destroy), которых в src/sense/ не существует —
 * это внутренние имена старого реестра из src/observe.
 *
 * Живая граница модуля — platx/sense_module.h:
 *     sense_runtime_init()      инициализация подсистемы
 *     sense_module_start/stop() старт и останов под owner-эпохой
 *     sense_module_control()    control-операция под живым владельцем
 *
 * Заголовок оставлен, а не удалён: удаление публичного контракта — решение
 * владельца, а не следствие уборки. Инвариант «ни один собираемый исходник
 * не включает этот заголовок» держит tests/cli/sense_dead_contract.sh.
 * ─────────────────────────────────────────────────────────────────────────
 */

/* ── Auto-start source descriptor ────────────────────────────────────────── */
typedef struct sense_source_spec {
    const char   *source_id;
    sense_owner_t owner;
} sense_source_spec_t;

/* ── Module start configuration ──────────────────────────────────────────── */
typedef struct sense_mod_config {
    sense_source_spec_t *auto_start_sources; /* NULL = start no sources */
    uint32_t             nsources;
} sense_mod_config_t;

/* ── Module status snapshot ───────────────────────────────────────────────── */
typedef struct sense_mod_status {
    uint32_t state;           /* sense_mod_state_t cast to uint32_t */
    uint64_t epoch;           /* current coverage epoch value */
    uint64_t start_ns;        /* CLOCK_MONOTONIC at last sense_mod_start() */
    uint64_t events_total;    /* events successfully published */
    uint64_t drops_total;     /* events dropped or refused */
    uint32_t sources_running; /* count of actively running sources */
    char     last_error[128]; /* last error message; empty if none */
    uint32_t ring_fill_pct;   /* ring fill × 0.01 % (reserved) */
    uint64_t ring_drops;      /* ring-level drops (reserved) */
} sense_mod_status_t;

/* ── Module public API ───────────────────────────────────────────────────── */
int  sense_mod_start  (const sense_mod_config_t *cfg);
int  sense_mod_stop   (uint32_t timeout_ms);
int  sense_mod_status (sense_mod_status_t *out);
int  sense_mod_advance_epoch(void);
int  sense_mod_register_source  (plat_sense_source_v2_t *src);
int  sense_mod_deregister_source(const char *source_id);
void sense_mod_account_event    (int dropped);
include/platx/sense_module.h
/* platx/sense_module.h — SENSE029: module lifecycle owner-generation authority.
 *
 * The `sense` module is registered through the existing platform lifecycle
 * (SENSE051). This header is the single boundary that decides whether a
 * start or control request carries a live owner epoch. Generation 0 is not
 * an epoch (it creates nothing); a generation older than the running epoch
 * is stale. Both are refused here so no downstream path can act on a dead
 * owner. Additive to ABI v1: no frozen struct changes.
 */

typedef enum sense_module_state {
    SENSE_MOD_UNINIT  = 0,
    SENSE_MOD_READY   = 1,   /* init done, not started */
    SENSE_MOD_RUNNING = 2    /* started under a live owner epoch */
} sense_module_state_t;

/* Control operation codes accepted at the module boundary. */
typedef enum sense_module_ctl {
    SENSE_MOD_CTL_NOOP    = 0,
    SENSE_MOD_CTL_REFRESH = 1,
    SENSE_MOD_CTL_QUIESCE = 2
} sense_module_ctl_t;

/* Reset lifecycle authority to READY. Returns SENSE_OK.
 * Named sense_runtime_init to distinguish it from
 * sense_registry_init() (source table) and sense_ring_init() (ring). */
int sense_runtime_init(void);

/* Start the module under `owner`.
 *   SENSE_ERR_STALE_OWNER  generation == 0 (negative cut)
 *   SENSE_ERR_STALE_GEN    generation older than the last running epoch
 *   SENSE_ERR_BUSY         already running under a live owner
 *   SENSE_OK               started; owner epoch recorded
 */
int sense_module_start(sense_owner_t owner);

/* Apply a control op. Refused unless RUNNING and `owner` matches the running
 * instance with a generation >= the running epoch.
 *   SENSE_ERR_STALE_OWNER  generation == 0
 *   SENSE_ERR_INVAL        not RUNNING
 *   SENSE_ERR_STALE_GEN    wrong instance, or generation < running epoch
 *   SENSE_OK               applied
 */
int sense_module_control(sense_owner_t owner, sense_module_ctl_t op);

/* Idempotent control (SENSE032).
 * Applies `op` exactly once per `request_id` within a bounded recent-request
 * window. Re-submitting the same request_id (a retry after a lost reply)
 * returns the SAME result and does NOT re-run the side effect; *fresh_out is
 * set to 1 when the op was applied for the first time, 0 on a replay.
 * Owner-generation rules are identical to sense_module_control().
 * `fresh_out` may be NULL.
 */
int sense_module_control_id(sense_owner_t owner, uint64_t request_id,
                            sense_module_ctl_t op, int *fresh_out);

/* Count of QUIESCE control ops actually applied (the observable side effect
 * used to prove idempotency). */
uint64_t sense_module_apply_count(void);

/* Stop the module. A stop carrying a generation older than the running epoch
 * is refused (SENSE_ERR_STALE_GEN); gen 0 is refused (SENSE_ERR_STALE_OWNER). */
int sense_module_stop(sense_owner_t owner);

/* Introspection (test/CLI). */
sense_module_state_t sense_module_get_state(void);
sense_owner_t        sense_module_get_owner(void);
uint32_t             sense_module_epoch(void);   /* highest generation ever run */
include/platx/sense_normalize.h
/* sense_normalize.h — Public API for SENSE provider normalizers.
 * SENSE251-SENSE300: Wave 6 normalize / privacy / enrichment.
 */

/* ── Hades raw event structs (input to normalizers) ─────────────────────── */
typedef struct hades_proc_raw {
    uint32_t pid;
    uint32_t ppid;
    uint64_t uid;
    uint64_t gid;
    char     comm[16];
    char     exe[256];
    uint32_t event_type;  /* 1=exec, 2=fork, 3=exit */
    int32_t  retval;
} hades_proc_raw_t;

typedef struct hades_file_raw {
    uint32_t pid;
    uint64_t ino;
    uint64_t dev;
    char     path[256];
    uint64_t bytes_written;   /* requested */
    int32_t  retval;          /* actual bytes written or error */
    uint32_t event_type;      /* 1=open, 2=write, 3=unlink, 4=rename */
} hades_file_raw_t;

typedef struct hades_net_raw {
    uint32_t pid;
    uint32_t proto;
    uint32_t saddr_v4;   /* __be32 */
    uint32_t daddr_v4;   /* __be32 */
    uint16_t sport;      /* host order */
    uint16_t dport;      /* __be16 */
    uint64_t socket_cookie;
    uint64_t bytes_sent;
    uint64_t bytes_recv;
    uint32_t event_type; /* 1=connect, 2=accept, 3=close, 4=send */
} hades_net_raw_t;

/* ── Enrichment reference record ─────────────────────────────────────────── */
typedef struct sense_enrich_ref {
    uint64_t target_event_id_hi;
    uint64_t target_event_id_lo;
    uint32_t enrichment_type;
    uint64_t enriched_at_ns;
    uint8_t  data[512];
    uint32_t data_len;
} sense_enrich_ref_t;

/* ── Normalizer API ──────────────────────────────────────────────────────────
 *
 * КОНТРАКТ PAYLOAD (SENSE018/SENSE019, правило A4-OBS-002).
 * Нормализатор заполняет ТОЛЬКО необязательные TLV. Ни один обязательный
 * canonical-TLV (SENSE_TLV_TYPE_REQ) в payload не попадает: обязательные поля
 * принадлежат заголовку и пишутся sense_encode() из sense_event_header_t.
 * Нарушение этого контракта раньше давало запись, которую кольцо принимало,
 * а потребитель отвергал как SENSE_DECODE_DUPLICATE; теперь sense_encode()
 * отказывает такому payload тем же кодом, поэтому нарушение невозможно
 * пронести молча.
 *
 * Значение hdr->payload_type вызывающий берёт у соответствующей функции
 * ..._payload_type() ниже, а не выводит из содержимого payload. */
uint32_t sense_normalize_hades_proc_payload_type(void);
uint32_t sense_normalize_hades_file_payload_type(void);
uint32_t sense_normalize_hades_net_payload_type(void);

int sense_normalize_hades_proc(const hades_proc_raw_t *raw,
                                uint8_t *out_buf, uint32_t out_sz,
                                uint32_t *out_used);

int sense_normalize_hades_file(const hades_file_raw_t *raw,
                                uint8_t *out_buf, uint32_t out_sz,
                                uint32_t *out_used);

int sense_normalize_hades_net(const hades_net_raw_t *raw,
                               uint8_t *out_buf, uint32_t out_sz,
                               uint32_t *out_used);

int sense_apply_privacy(uint8_t *buf, uint32_t sz,
                         uint32_t privacy_flags,
                         uint32_t lease_mask);

int sense_emit_enrichment(const sense_enrich_ref_t *ref,
                           uint8_t *out_buf, uint32_t out_sz,
                           uint32_t *out_used);
include/platx/sense_provider.h
/* platx/sense_provider.h — sense_provider_v1 (D052).
 *
 * The ABI a fact-emitting provider implements. Hades' process, file and
 * network sensors are the first three implementations (D066-D068), but this
 * header knows nothing about them: it includes no private Hades header, no
 * kernel type and no src/ path. A public ABI that needs a private header is
 * a private ABI with a public name.
 *
 * What a sense provider owes the fabric:
 *   - a batch, or a typed refusal. Never an empty batch standing for "I could
 *     not look" (D057);
 *   - loss stated as a number and a gap, at the moment it happens (D062);
 *   - a cursor the consumer acknowledges, so backpressure is visible on both
 *     sides rather than being absorbed silently;
 *   - identity that survives restart: records carry the provider epoch, and a
 *     record from a retired epoch is refused, not renumbered (D084).
 *
 * ABI freeze: SENSE_PROVIDER_ABI 1 (D052 sealed 2026-09-01)
 */

#define SENSE_PROVIDER_ABI  1u

/* ── Provider capabilities negotiated in prov_negotiate_req.capability_need ─ */
#define SENSE_PROV_CAP_STREAM       (1u << 0)  /* continuous emission */
#define SENSE_PROV_CAP_SNAPSHOT     (1u << 1)  /* bounded point-in-time read */
#define SENSE_PROV_CAP_HEARTBEAT    (1u << 2)
#define SENSE_PROV_CAP_SELFTEST     (1u << 3)
#define SENSE_PROV_CAP_CURSOR       (1u << 4)  /* resumable after restart */
#define SENSE_PROV_CAP_BACKPRESSURE (1u << 5)  /* honours unacked-window */
#define SENSE_PROV_CAP_PRIVACY      (1u << 6)  /* field-level redaction */

/* ── Batch flags ─────────────────────────────────────────────────────────── */
#define SENSE_BATCH_F_GAP_BEFORE  (1u << 0)  /* records were lost before first_seq */
#define SENSE_BATCH_F_GAP_OPEN    (1u << 1)  /* a gap is open right now */
#define SENSE_BATCH_F_TRUNCATED   (1u << 2)  /* budget cut the batch short */
#define SENSE_BATCH_F_REDACTED    (1u << 3)  /* privacy lease removed fields */
#define SENSE_BATCH_F_EPOCH_START (1u << 4)  /* first batch of a new epoch */

/* ── One batch of already-normalized records ─────────────────────────────
 * `wire` is a read-only view owned by the provider until the next read() or
 * close() on the same stream. Storing the pointer is forbidden, exactly as in
 * SENSE's event views: a batch is a loan, not a copy.
 */
typedef struct sense_prov_batch {
    uint32_t abi_version;
    uint32_t struct_size;

    uint64_t first_seq;      /* provider sequence of records[0] */
    uint64_t last_seq;       /* provider sequence of the final record */
    uint64_t epoch;          /* provider epoch these records belong to */

    uint32_t n_records;
    uint32_t flags;          /* SENSE_BATCH_F_* */
    uint64_t lost_before;    /* records known lost before first_seq */

    const uint8_t  *wire;    /* concatenated TLV records */
    const uint32_t *lens;    /* n_records lengths, in order */
    uint64_t        bytes;   /* total of lens[] */
} sense_prov_batch_t;

/* ── The vtable ──────────────────────────────────────────────────────────
 * Every entry fills `st` even on success, so a caller that logs the reason
 * has one to log. Every entry may be called only between open() and close()
 * except describe/negotiate/health/selftest, which are always legal.
 */
typedef struct sense_provider_v1 {
    uint32_t abi_version;    /* SENSE_PROVIDER_ABI */
    uint32_t struct_size;
    void    *ctx;

    /* Identity and self-description. Must be stable across calls within an
     * epoch and must change generation/epoch when it is not. */
    int (*describe)(void *ctx, prov_envelope_t *out, prov_status_t *st);

    /* Agree ABI major, schema major and capability subset, or refuse with a
     * typed reason. A provider must not grant a capability it cannot serve. */
    int (*negotiate)(void *ctx, const prov_negotiate_req_t *req,
                     prov_negotiate_result_t *out);

    /* Open a stream under an explicit budget. An unbounded budget is refused
     * by the fabric before it ever reaches the provider (D065). */
    int (*open)(void *ctx, const prov_budget_t *budget,
                uint64_t *stream_id_out, prov_status_t *st);

    /* Read the next batch. Returns PROV_OK with n_records == 0 only when the
     * provider is genuinely idle and its coverage is intact; if it could not
     * look, it returns PROV_ERR_UNAVAILABLE with a typed reason. */
    int (*read)(void *ctx, uint64_t stream_id, sense_prov_batch_t *out,
                prov_status_t *st);

    /* Acknowledge through a sequence. Acking beyond what was handed out is a
     * REFUSAL, not a silently clamped number. */
    int (*ack)(void *ctx, uint64_t stream_id, uint64_t through_seq,
               prov_status_t *st);

    int (*health)(void *ctx, prov_health_t *out, prov_status_t *st);
    int (*selftest)(void *ctx, prov_selftest_t *out, prov_status_t *st);

    /* Close one stream. Idempotent: closing twice is PROV_OK, closing an
     * unknown stream is PROV_ERR_NOT_FOUND. */
    int (*close)(void *ctx, uint64_t stream_id, prov_status_t *st);

    void *reserved[8];
} sense_provider_v1_t;
include/platx/sense_schema.h
/* platx/sense_schema.h — SENSE canonical event envelope, TLV IDs, flags.
 *
 * Wire format rules (SENSE009, SENSE016-SENSE026):
 *   - All integers little-endian, alignment=1.
 *   - TLV: uint16_t type | uint16_t len | uint8_t value[len]
 *   - Maximum record size: SENSE_RECORD_MAX bytes.
 *   - Unknown optional TLV (high bit clear in type): preserve at relay.
 *   - Unknown required TLV (high bit set): return SENSE_ERR_SCHEMA.
 *   - Overflow/truncation must be detected before payload access.
 *   - Duplicate required field: reject record.
 *
 * DO NOT cast this header's structs directly to/from wire bytes.
 * Use sense_tlv_encode / sense_tlv_decode helpers.
 *
 * ABI frozen: SENSE_SCHEMA_VERSION 1 (2026-08-27)
 */

/* ── Wire constraints ────────────────────────────────────────────────────── */
#define SENSE_SCHEMA_VERSION  1u
#define SENSE_RECORD_MAX      65520u   /* bytes; checked before any read */
#define SENSE_TLV_HDR_SIZE    4u       /* type(2) + len(2) */
#define SENSE_TLV_TYPE_REQ    0x8000u  /* high bit: required TLV */

/* ── Event header (logical C model — NOT a wire struct) ──────────────────── */
typedef struct sense_event_header {
    uint32_t schema_version;     /* SENSE_SCHEMA_VERSION */
    uint16_t header_size;        /* sizeof this after canonical decode */
    uint16_t event_class;        /* SENSE_CLASS_* */
    uint32_t record_size;        /* total including payload TLV */
    uint32_t flags;              /* SENSE_F_* bitmask */
    uint64_t event_id_hi;        /* UUID high 64 bits */
    uint64_t event_id_lo;        /* UUID low 64 bits */
    uint64_t sequence;           /* global monotonic counter */
    uint64_t source_sequence;    /* per-source monotonic counter */
    uint64_t monotonic_ns;       /* CLOCK_MONOTONIC; always set */
    int64_t  wall_time_ns;       /* CLOCK_REALTIME; may be UNCERTAIN */
    uint64_t boot_id_hash;       /* FNV-64 of /proc/sys/kernel/random/boot_id */
    uint64_t sensor_instance;    /* changes on every start() */
    uint64_t coverage_epoch;     /* changes on schema/filter/attach change */
    uint64_t source_generation;  /* provider generation at emission */
    uint64_t policy_generation;  /* config policy generation */
    uint64_t entity_id;          /* primary subject entity key */
    uint64_t parent_entity_id;   /* parent (0 = none) */
    uint32_t cpu;                /* emitting CPU */
    uint32_t payload_type;       /* SENSE_PAYLOAD_* */
    uint32_t payload_size;       /* bytes following header */
    uint32_t trust;              /* sense_trust_t */
    uint32_t quality;            /* SENSE_QUAL_* */
    uint64_t lost_before;        /* events lost before this one (0=none) */
} sense_event_header_t;

_Static_assert(sizeof(sense_event_header_t) == 152,
               "sense_event_header_t size frozen");

/* Wire header size — the payload begins at this offset in a record. */
#define SENSE_EVENT_HEADER_SIZE  152u

/* ── Event classes ───────────────────────────────────────────────────────── */
#define SENSE_CLASS_PROCESS     0x0001u  /* exec/fork/exit */
#define SENSE_CLASS_FILE        0x0002u  /* open/write/unlink/rename */
#define SENSE_CLASS_NETWORK     0x0004u  /* connect/accept/send/recv */
#define SENSE_CLASS_KERNEL      0x0008u  /* module/BPF/integrity */
#define SENSE_CLASS_HYPERVISOR  0x0010u  /* VMEXIT/CR/MSR/EPT */
#define SENSE_CLASS_RESOURCE    0x0020u  /* CPU/mem/PSI/disk */
#define SENSE_CLASS_HARDWARE    0x0040u  /* RAS/MCE/TPM */
#define SENSE_CLASS_PLATX       0x0080u  /* self-observation */
#define SENSE_CLASS_CONTROL     0x0100u  /* GAP/EPOCH/SNAPSHOT/BARRIER */
#define SENSE_CLASS_FINDING     0x0200u  /* detector output */
#define SENSE_CLASS_INTENT      0x0400u  /* action intent/result */
#define SENSE_CLASS_ALL         0xFFFFu

/* ── Event flags ─────────────────────────────────────────────────────────── */
#define SENSE_F_SNAPSHOT        (1u <<  0)  /* from snapshot, not stream */
#define SENSE_F_STREAM          (1u <<  1)  /* from live stream */
#define SENSE_F_REPLAYED        (1u <<  2)  /* resent for at-least-once */
#define SENSE_F_ENRICHED        (1u <<  3)  /* post-raw enrichment */
#define SENSE_F_REDACTED        (1u <<  4)  /* privacy field removed */
#define SENSE_F_TRUNCATED       (1u <<  5)  /* payload clipped */
#define SENSE_F_LOSS_PRESENT    (1u <<  6)  /* lost_before > 0 */
#define SENSE_F_CLOCK_UNCERTAIN (1u <<  7)  /* wall_time_ns unreliable */
#define SENSE_F_LATE            (1u <<  8)  /* reorder window exceeded */
#define SENSE_F_SAMPLED         (1u <<  9)  /* deterministic sample */
#define SENSE_F_POLICY_RELEVANT (1u << 10)  /* matched active policy */
#define SENSE_F_EVIDENCE_SEALED (1u << 11)  /* FORENSIC custody record exists */
#define SENSE_F_ADAPTER_SYN     (1u << 12)  /* OBS0/legacy synthesized field */

/* ── Control record subtypes ─────────────────────────────────────────────── */
#define SENSE_CTRL_GAP_OPEN      0x01u  /* coverage hole began */
#define SENSE_CTRL_GAP_CLOSE     0x02u  /* gap closed (with reconciliation) */
#define SENSE_CTRL_GAP_ABANDONED 0x03u  /* gap never resolved */
#define SENSE_CTRL_EPOCH_OPEN    0x04u  /* new coverage epoch started */
#define SENSE_CTRL_EPOCH_CLOSE   0x05u  /* epoch closed */
#define SENSE_CTRL_SNAPSHOT_BGN  0x06u  /* snapshot begin barrier */
#define SENSE_CTRL_SNAPSHOT_END  0x07u  /* snapshot end barrier */
#define SENSE_CTRL_SNAPSHOT_ABT  0x08u  /* snapshot aborted */
#define SENSE_CTRL_HEARTBEAT     0x09u  /* source alive signal */
#define SENSE_CTRL_BARRIER       0x0Au  /* ordering barrier + consistency */

/* ── Payload type IDs (numeric range 0x0001–0x0FFF reserved for SENSE) ───── */
#define SENSE_PAYLOAD_NONE          0x0000u
#define SENSE_PAYLOAD_PROCESS_EXEC  0x0001u
#define SENSE_PAYLOAD_PROCESS_EXIT  0x0002u
#define SENSE_PAYLOAD_PROCESS_FORK  0x0003u
#define SENSE_PAYLOAD_PROCESS_PTRACE 0x0004u
#define SENSE_PAYLOAD_FILE_OPEN     0x0010u
#define SENSE_PAYLOAD_FILE_WRITE    0x0011u
#define SENSE_PAYLOAD_FILE_UNLINK   0x0012u
#define SENSE_PAYLOAD_FILE_RENAME   0x0013u
#define SENSE_PAYLOAD_NET_CONNECT   0x0020u
#define SENSE_PAYLOAD_NET_ACCEPT    0x0021u
#define SENSE_PAYLOAD_NET_CLOSE     0x0022u
#define SENSE_PAYLOAD_NET_SEND      0x0023u
#define SENSE_PAYLOAD_NET_BIND      0x0024u
#define SENSE_PAYLOAD_RESOURCE_CPU  0x0030u
#define SENSE_PAYLOAD_RESOURCE_MEM  0x0031u
#define SENSE_PAYLOAD_RESOURCE_DISK 0x0032u
#define SENSE_PAYLOAD_RESOURCE_NET  0x0033u
#define SENSE_PAYLOAD_KERNEL_MODULE 0x0040u
#define SENSE_PAYLOAD_KERNEL_BPF    0x0041u
#define SENSE_PAYLOAD_KERNEL_INTEG  0x0042u
#define SENSE_PAYLOAD_HV_CR         0x0050u
#define SENSE_PAYLOAD_HV_MSR        0x0051u
#define SENSE_PAYLOAD_HV_EPT        0x0052u
#define SENSE_PAYLOAD_HV_VMEXIT     0x0053u
#define SENSE_PAYLOAD_FINDING       0x0060u
#define SENSE_PAYLOAD_INTENT        0x0061u
#define SENSE_PAYLOAD_CTRL          0x0070u
#define SENSE_PAYLOAD_GAP           0x0071u
#define SENSE_PAYLOAD_OBS0_CLAIM    0x0080u  /* OBS0 compat mapping */
#define SENSE_PAYLOAD_HADES_PROC    0x0081u  /* Hades process normalised */
#define SENSE_PAYLOAD_HADES_FILE    0x0082u  /* Hades file normalised */
#define SENSE_PAYLOAD_HADES_NET     0x0083u  /* Hades network normalised */

/* ── TLV field IDs (canonical fields: required bit = SENSE_TLV_TYPE_REQ) ─── */
/* Envelope required fields */
#define SENSE_TLV_SCHEMA_VER    (SENSE_TLV_TYPE_REQ | 0x0001u)
#define SENSE_TLV_EVENT_CLASS   (SENSE_TLV_TYPE_REQ | 0x0002u)
#define SENSE_TLV_FLAGS         (SENSE_TLV_TYPE_REQ | 0x0003u)
#define SENSE_TLV_EVENT_ID      (SENSE_TLV_TYPE_REQ | 0x0004u)
#define SENSE_TLV_SEQUENCE      (SENSE_TLV_TYPE_REQ | 0x0005u)
#define SENSE_TLV_MONOTONIC_NS  (SENSE_TLV_TYPE_REQ | 0x0006u)
#define SENSE_TLV_SENSOR_INST   (SENSE_TLV_TYPE_REQ | 0x0007u)
#define SENSE_TLV_COVERAGE_EP   (SENSE_TLV_TYPE_REQ | 0x0008u)
#define SENSE_TLV_PAYLOAD_TYPE  (SENSE_TLV_TYPE_REQ | 0x0009u)
/* Envelope optional fields */
#define SENSE_TLV_WALL_TIME     0x0010u
#define SENSE_TLV_BOOT_ID_HASH  0x0011u
#define SENSE_TLV_SRC_SEQUENCE  0x0012u
#define SENSE_TLV_SRC_GENERATION 0x0013u
#define SENSE_TLV_POLICY_GEN    0x0014u
#define SENSE_TLV_ENTITY_ID     0x0015u
#define SENSE_TLV_PARENT_ENTITY 0x0016u
#define SENSE_TLV_CPU           0x0017u
#define SENSE_TLV_TRUST         0x0018u
#define SENSE_TLV_QUALITY       0x0019u
#define SENSE_TLV_LOST_BEFORE   0x001Au

/* ── Payload TLV registry (0x0100–0x015F) ────────────────────────────────
 * SENSE010 asks for one allocation table and no reuse of retired ids.
 * These ids were in use as bare hex with the name only in a trailing
 * comment, which is the same as having no registry at all: nothing stops
 * two normalizers from picking the same number.  Named here once. */
#define SENSE_TLV_PID_HINT          0x0100u
#define SENSE_TLV_COMM              0x0101u
#define SENSE_TLV_EXE_HINT          0x0102u
#define SENSE_TLV_EUID              0x0103u
#define SENSE_TLV_RUID              0x0104u
#define SENSE_TLV_TRACER_ENTITY     0x0105u
#define SENSE_TLV_TRACEE_ENTITY     0x0106u

#define SENSE_TLV_INO               0x0110u
#define SENSE_TLV_DEV               0x0111u
#define SENSE_TLV_PATH_HINT         0x0112u
#define SENSE_TLV_BYTES_REQ         0x0113u  /* requested write count   */
#define SENSE_TLV_BYTES_ACT         0x0114u  /* observed result (W3-31) */
#define SENSE_TLV_FILE_MODE         0x0115u
#define SENSE_TLV_WRITE_DATA_SAMPLE 0x0116u

#define SENSE_TLV_PROTO             0x0120u
#define SENSE_TLV_SADDR4            0x0121u
#define SENSE_TLV_DADDR4            0x0122u
#define SENSE_TLV_SPORT             0x0123u
#define SENSE_TLV_DPORT             0x0124u
#define SENSE_TLV_SOCK_COOKIE       0x0125u
#define SENSE_TLV_NETNS             0x0126u

#define SENSE_TLV_ENRICH_TARGET_HI  0x0150u
#define SENSE_TLV_ENRICH_TARGET_LO  0x0151u
#define SENSE_TLV_ENRICH_TYPE       0x0152u
#define SENSE_TLV_ENRICH_TS         0x0153u
#define SENSE_TLV_ENRICH_DATA       0x0154u

/* ── Decode-specific error codes (SENSE022) ─────────────────────────────── */
#define SENSE_DECODE_MALFORMED   -20  /* invalid TLV structure */
#define SENSE_DECODE_DUPLICATE   -21  /* duplicate required field */
#define SENSE_DECODE_OVERFLOW    -22  /* offset+length exceeds record size */
#define SENSE_DECODE_TRUNCATED   -23  /* record cut short */
#define SENSE_DECODE_TOO_NEW     -24  /* schema_version > SENSE_SCHEMA_VERSION */
#define SENSE_DECODE_TOO_OLD     -25  /* schema_version unsupported (< 1) */

/* ── Control-payload TLV IDs (0x0200–0x020F) ───────────────────────────── */
#define SENSE_TLV_EPOCH_ID   0x0200u  /* uint64_t: coverage epoch value */
#define SENSE_TLV_GAP_CAUSE  0x0201u  /* uint32_t: gap reason code */

/* Gap cause codes (used in SENSE_CTRL_GAP_OPEN records) */
#define SENSE_GAP_CAUSE_SHUTDOWN  1u  /* planned module stop */
#define SENSE_GAP_CAUSE_CRASH     2u  /* uncontrolled stop */
#define SENSE_GAP_CAUSE_OVERFLOW  3u  /* ring overflow eviction */
/* ── Immutable event view (opaque — use accessor functions) ──────────────── */
typedef struct sense_event_view {
    const uint8_t *_wire;      /* points into ring slot; DO NOT retain */
    uint32_t       _wire_size;
    sense_event_header_t _hdr; /* decoded header (cached) */
} sense_event_view_t;

/* Accessors */
const sense_event_header_t *
sense_ev_header(const sense_event_view_t *ev); /* inline interface */

uint32_t
sense_ev_flags(const sense_event_view_t *ev); /* inline interface */

uint64_t
sense_ev_sequence(const sense_event_view_t *ev); /* inline interface */

uint32_t
sense_ev_class(const sense_event_view_t *ev); /* inline interface */

uint32_t
sense_ev_payload_type(const sense_event_view_t *ev); /* inline interface */

uint64_t
sense_ev_entity(const sense_event_view_t *ev); /* inline interface */

uint64_t
sense_ev_monotonic_ns(const sense_event_view_t *ev); /* inline interface */

/* Payload bytes. NULL when the record carries none, or when the wire
 * buffer is shorter than the header claims — a truncated record must not
 * hand a detector a pointer past its own buffer. */
uint32_t
sense_ev_payload_size(const sense_event_view_t *ev); /* inline interface */

const uint8_t *
sense_ev_payload(const sense_event_view_t *ev); /* inline interface */

/* ── TLV codec (little-endian checked helpers) ───────────────────────────── */

/* Load/store helpers — no unaligned UB */
uint16_t sense_load_u16(const uint8_t *p); /* inline interface */
uint32_t sense_load_u32(const uint8_t *p); /* inline interface */
uint64_t sense_load_u64(const uint8_t *p); /* inline interface */
void sense_store_u16(uint8_t *p, uint16_t v); /* inline interface */
void sense_store_u32(uint8_t *p, uint32_t v); /* inline interface */
void sense_store_u64(uint8_t *p, uint64_t v); /* inline interface */

/* Write one TLV field; returns bytes written or -1 on overflow */
int sense_tlv_write(uint8_t *buf, size_t bufsz,
                                   size_t off,
                                   uint16_t type, const void *val, uint16_t len); /* inline interface */

/* Iterate to next TLV; returns 0 when exhausted, -1 on malformed */
int sense_tlv_next(const uint8_t *buf, size_t bufsz,
                                  size_t *off,
                                  uint16_t *type_out, uint16_t *len_out,
                                  const uint8_t **val_out); /* inline interface */

/* ── Codec entry points (src/sense/sense_tlv.c) ──────────────────────────── */

/* Encode hdr (+ optional caller extension TLVs) in canonical order.
 * Returns 0 and sets *written_out, or a negative SENSE_DECODE_* code. */
int sense_encode(uint8_t *buf, size_t bufsz,
                 const sense_event_header_t *hdr,
                 const uint8_t *payload_tlvs, size_t payload_len,
                 size_t *written_out);

/* Decode wire bytes into a view. 0 on success, negative on refusal. */
int sense_decode(const uint8_t *wire, size_t wire_len,
                 sense_event_view_t *out);

/* Re-encode a decoded view, preserving unknown optional TLVs verbatim. */
int sense_relay(const sense_event_view_t *in,
                uint8_t *out_buf, size_t bufsz, size_t *written_out);

/* ── Schema registry entry ───────────────────────────────────────────────── */
typedef struct sense_schema_entry {
    uint32_t payload_type;           /* SENSE_PAYLOAD_* */
    const char *kind;                /* canonical kind string */
    uint32_t schema_major;
    uint32_t schema_minor;
    uint32_t required_tlvs[16];      /* TLV type IDs; 0-terminated */
    uint32_t optional_tlvs[32];      /* TLV type IDs; 0-terminated */
    uint32_t max_encoded_bytes;
    uint32_t privacy_flags;          /* SENSE_PRIV_F_* */
    uint32_t normalizer_id;
} sense_schema_entry_t;

/* Privacy flags */
#define SENSE_PRIV_F_ARGV       (1u << 0)  /* argv/env capture */
#define SENSE_PRIV_F_PAYLOAD    (1u << 1)  /* file/network content */
#define SENSE_PRIV_F_PATH       (1u << 2)  /* full path */
#define SENSE_PRIV_F_IDENTITY   (1u << 3)  /* user/credential */

/* Schema registry lookup */
const sense_schema_entry_t *sense_schema_lookup(uint32_t payload_type);
int sense_schema_register(const sense_schema_entry_t *entry);

/* ── Golden test vector type (SENSE023) ──────────────────────────────────── */
typedef struct sense_golden_vector {
    const char   *name;
    const uint8_t *wire;
    size_t         wire_len;
    int            expect_ok;    /* 0 = expect decode error */
    uint32_t       expect_class;
    uint64_t       expect_sequence;
} sense_golden_vector_t;

int sense_golden_verify(const sense_golden_vector_t *v);
include/platx/sense_signer.h
/* platx/sense_signer.h — SENSE037: signer domain separation for SENSE
 * capsule and checkpoint artifacts.
 *
 * No new crypto stack is introduced: this is a thin domain-separation wrapper
 * over the platform's existing HMAC-SHA256 (src/crypto/crypto.h). Each SENSE
 * artifact class signs under a distinct, versioned domain tag, so a MAC
 * produced for a capsule can never validate a checkpoint (or vice versa),
 * even when the signed bytes are identical. The production asymmetric signer
 * (Ed25519, src/crypto/platx_signer.c) remains the owner's decision (SENSE037
 * disposition); this layer fixes the domain-tag discipline that any signer
 * must honour and is what SENSE038 golden-tests.
 */

typedef enum sense_sign_domain {
    SENSE_SIGN_CAPSULE    = 1,   /* tag: "PLATX-SENSE-CAPSULE-v1"    */
    SENSE_SIGN_CHECKPOINT = 2    /* tag: "PLATX-SENSE-CHECKPOINT-v1" */
} sense_sign_domain_t;

/* Compute a domain-separated MAC over msg into out[32].
 * Construction: HMAC-SHA256(key, domain_tag || 0x00 || msg).
 * Returns 0 on success, -1 on bad arguments (unknown domain / NULL out). */
int sense_domain_mac(sense_sign_domain_t domain,
                     const uint8_t *key, size_t keylen,
                     const uint8_t *msg, size_t mlen,
                     uint8_t out[32]);

/* Constant-time verify of a domain-separated MAC.
 * Returns 1 if valid for THIS domain, 0 if not (wrong domain, wrong key,
 * tampered message, or bad arguments). */
int sense_domain_verify(sense_sign_domain_t domain,
                        const uint8_t *key, size_t keylen,
                        const uint8_t *msg, size_t mlen,
                        const uint8_t sig[32]);

/* The exact domain-tag string for a domain (NUL-terminated), or NULL. */
const char *sense_sign_domain_tag(sense_sign_domain_t domain);
include/platx/sense_threat.h
/* SPDX-License-Identifier: GPL-2.0-only */
/* include/platx/sense_threat.h - global ThreatLevel state machine (additive to
 * the canonical src/sense tree; new symbols, no collision). Waves 3.07, 3.28-3.30.
 * INV-SENSE-01: TL>TL2 raises sampling. INV-SENSE-02: a detector can only RAISE
 * TL; lowering requires a policy token. INV-SENSE-03: each escalation is audited. */

typedef enum { SENSE_TL0=0, SENSE_TL1=1, SENSE_TL2=2, SENSE_TL3=3, SENSE_TL4=4 } sense_tl_t;
void       sense_threat_reset(void);
sense_tl_t sense_threat_get(void);
/* Raise TL to at least new_tl. Never lowers (INV-SENSE-02). Returns 1 if it
 * escalated (and audited, INV-SENSE-03), 0 if no change. reason is a short tag. */
int  sense_threat_update(sense_tl_t new_tl, const char *reason);
/* Lower TL - only Policy may call this with a non-zero token. Returns 0/-1. */
int  sense_threat_policy_lower(sense_tl_t new_tl, uint64_t policy_token);
/* INV-SENSE-01: sampling multiplier grows with TL (1,1,1,2,4). */
unsigned sense_threat_sampling(void);
include/platx/sense_timeline.h
/* SPDX-License-Identifier: GPL-2.0-only */
/* include/platx/sense_timeline.h - fixed static event timeline + corr_id
 * correlation. Waves 3.32/3.33/3.34. No malloc: RING[1024]. */

#define SENSE_TIMELINE_CAP 1024
#define SENSE_CORR_LEN 16
typedef struct sense_tl_entry {
    uint64_t timestamp_ns;
    uint8_t  corr_id[SENSE_CORR_LEN];
    uint32_t pid;
    uint8_t  event_type;
    uint8_t  threat_level;
    uint8_t  _pad[2];
} sense_tl_entry_t;
void   sense_timeline_reset(void);
/* Append newest; overwrites oldest when full (ring). Returns 0. */
int    sense_timeline_push(const sense_tl_entry_t *e);
/* Number of live entries (<= CAP). */
size_t sense_timeline_count(void);
/* idx 0 = oldest live entry. Returns 0/-1. */
int    sense_timeline_get(size_t idx, sense_tl_entry_t *out);
/* 3.32: count entries whose corr_id matches. */
size_t sense_correlate_count(const uint8_t corr_id[SENSE_CORR_LEN]);
include/platx/sense_visibility.h
/* platx/sense_visibility.h — SENSE151-155: the Visibility Contract.
 *
 * A detector is a claim about the world, and a claim is only as good as
 * the observation behind it. This header is the boundary where that gets
 * decided *before* a detector runs, not after it has already emitted a
 * finding nobody can trust.
 *
 * The contract states what a detector needs in order to be believed:
 * which event kinds, at what schema version, at what quality floor, with
 * how much loss tolerated, whether lineage is required, and whether the
 * required kinds must come from independent sources. Evaluation returns
 * SATISFIED or UNSATISFIED with the reason — never a bare boolean, because
 * "not started" and "started blind" are different facts.
 *
 * SENSE153 is the one that matters most in practice: a quality floor of
 * zero is not a permissive contract, it is an unstated one. It is refused
 * rather than treated as "anything will do".
 *
 * SENSE155: a contract may ask to fail closed when the source goes blind
 * — suppress the detector entirely — but only under a signed enforcement
 * policy. Unsigned fail-closed is a denial-of-service switch that any
 * config edit can flip, so it is refused at parse time.
 *
 * Additive to ABI v1: no frozen struct changes.
 */

/* ── Limits ──────────────────────────────────────────────────────────── */
#define SENSE_VIS_KINDS_MAX      16u   /* required kinds per contract     */
#define SENSE_VIS_TEXT_MAX     1024u   /* source text a parser will read  */
#define SENSE_VIS_NAME_MAX       32u
#define SENSE_VIS_REASON_MAX    128u

/* ── On-blind outcome (SENSE154) ─────────────────────────────────────────
 * What a detector does when its inputs stop being trustworthy. These are
 * four different answers to "we cannot see", and collapsing them is how a
 * blind detector ends up reported as a quiet one. */
typedef enum sense_on_blind {
    SENSE_ON_BLIND_INDETERMINATE = 0, /* emit, marked not provable (default) */
    SENSE_ON_BLIND_LOWER         = 1, /* emit with reduced confidence        */
    SENSE_ON_BLIND_SNAPSHOT      = 2, /* fall back to snapshot reconcile     */
    SENSE_ON_BLIND_SUPPRESS      = 3  /* fail closed — needs signed policy   */
} sense_on_blind_t;

/* ── Evaluation verdict ──────────────────────────────────────────────── */
typedef enum sense_vis_verdict {
    SENSE_VIS_SATISFIED   = 0,
    SENSE_VIS_UNSATISFIED = 1
} sense_vis_verdict_t;

/* Why a contract was not satisfied. One reason per failing requirement so
 * the operator is told what to fix, not merely that something is wrong. */
typedef enum sense_vis_reason {
    SENSE_VIS_OK               = 0,
    SENSE_VIS_R_KIND_MISSING   = 1,  /* a required kind is not covered      */
    SENSE_VIS_R_SCHEMA_OLD     = 2,  /* covered, but below min schema       */
    SENSE_VIS_R_QUALITY_LOW    = 3,  /* below the declared quality floor    */
    SENSE_VIS_R_LOSS_EXCEEDED  = 4,  /* loss above the tolerated fraction   */
    SENSE_VIS_R_LINEAGE_ABSENT = 5,  /* lineage required, not available     */
    SENSE_VIS_R_NOT_INDEPENDENT= 6,  /* kinds share one source             */
    SENSE_VIS_R_BLIND          = 7   /* source blind, contract suppresses   */
} sense_vis_reason_t;

/* ── Compiled contract (SENSE151) ─────────────────────────────────────────
 * The canonical form. Parsing produces exactly this; two texts that mean
 * the same thing compile to the same bytes, which is what makes a contract
 * digest meaningful in a finding (SENSE161). */
typedef struct sense_vis_kind_req {
    uint32_t payload_type;    /* SENSE_PAYLOAD_*                          */
    uint32_t min_schema;      /* minimum acceptable schema version        */
    uint32_t quality_floor;   /* SENSE_QUAL_* bits that MUST be present   */
} sense_vis_kind_req_t;

typedef struct sense_vis_contract {
    char     name[SENSE_VIS_NAME_MAX];
    uint32_t n_kinds;
    sense_vis_kind_req_t kind[SENSE_VIS_KINDS_MAX];

    uint32_t max_loss_ppm;    /* tolerated loss, parts per million        */
    uint8_t  require_lineage; /* parent lineage must be resolvable        */
    uint8_t  require_independent; /* required kinds from distinct sources */
    uint8_t  on_blind;        /* sense_on_blind_t                        */
    uint8_t  _pad;

    /* SENSE155: set only when on_blind == SUPPRESS and the enforcement
     * policy that authorises it was signed. */
    uint8_t  enforcement_signed;
    uint8_t  _pad2[3];

    /* Canonical digest of the compiled form, for finding provenance. */
    uint64_t digest;
} sense_vis_contract_t;

/* ── Observed state a contract is evaluated against ──────────────────── */
typedef struct sense_vis_observed_kind {
    uint32_t payload_type;
    uint32_t schema;          /* schema version actually available        */
    uint32_t quality;         /* SENSE_QUAL_* actually offered            */
    uint64_t source_id_hash;  /* which source supplies it (independence)  */
    int      covered;         /* 0 = kind not covered at all              */
} sense_vis_observed_kind_t;

typedef struct sense_vis_observed {
    uint32_t n_kinds;
    sense_vis_observed_kind_t kind[SENSE_VIS_KINDS_MAX];
    uint32_t loss_ppm;        /* measured loss                            */
    uint8_t  lineage_available;
    uint8_t  blind;
    uint8_t  _pad[2];
} sense_vis_observed_t;

typedef struct sense_vis_result {
    sense_vis_verdict_t verdict;
    sense_vis_reason_t  reason;
    uint32_t            failing_payload_type; /* 0 when not kind-specific */
    sense_on_blind_t    outcome;   /* what the detector should now do     */
    char                text[SENSE_VIS_REASON_MAX];
} sense_vis_result_t;

/* ── Parse (SENSE151) ─────────────────────────────────────────────────────
 * Line-oriented, deliberately small. One directive per line:
 *
 *   name <ident>
 *   kind <payload_type_hex> <min_schema> <quality_floor_hex>
 *   max_loss_ppm <n>
 *   require_lineage <0|1>
 *   require_independent <0|1>
 *   on_blind <indeterminate|lower|snapshot|suppress>
 *   enforcement_signed <0|1>
 *
 * `#` begins a comment. Unknown directives are refused rather than
 * ignored: silently dropping a line the author believed was in force is
 * the failure mode this whole file exists to prevent.
 *
 * Returns SENSE_OK, or:
 *   SENSE_ERR_INVAL   malformed text, unknown directive, no kinds
 *   SENSE_ERR_FULL    more than SENSE_VIS_KINDS_MAX kinds
 *   SENSE_ERR_SCHEMA  quality floor of zero (SENSE153)
 *   SENSE_ERR_PRIVACY on_blind=suppress without a signed policy (SENSE155)
 *
 * `err`/`errsz` receive a human-readable reason when non-NULL.
 */
int sense_vis_parse(const char *text, size_t len,
                    sense_vis_contract_t *out,
                    char *err, size_t errsz);

/* Canonical serialization of a compiled contract (SENSE151). Writes the
 * normalized text form; two equivalent inputs produce identical output.
 * Returns bytes written, or SENSE_ERR_FULL. */
int sense_vis_canonical(const sense_vis_contract_t *c, char *buf, size_t bufsz);

/* Digest over the canonical form. Stable across runs and hosts. */
uint64_t sense_vis_digest(const sense_vis_contract_t *c);

/* ── Evaluate (SENSE152-154) ─────────────────────────────────────────────
 * Checks every requirement before a detector is allowed to start, and
 * decides what a blind source means for this particular detector.
 * Always returns SENSE_OK and fills `res`; the verdict is in `res`. */
int sense_vis_evaluate(const sense_vis_contract_t *c,
                       const sense_vis_observed_t *obs,
                       sense_vis_result_t *res);

/* Human-readable reason string, for CLI and finding text. */
const char *sense_vis_reason_str(sense_vis_reason_t r);
const char *sense_vis_on_blind_str(sense_on_blind_t b);
include/platx/sense_xim_adapter.h
/* platx/sense_xim_adapter.h — SENSE119/SENSE120 XIM CHILD fact adapter.
 *
 * Ingests XIM CHILD facts (owner + context epoch qualified) and lets a
 * host-process observation be correlated against them. The trust rule is the
 * point of SENSE120: a host process is elevated to SENSE_TRUST_MEDIATED ONLY
 * when an authenticated XIM fact matches on the reuse-proof identity
 * (pid AND birth_ns) under a live owner/context epoch. A bare PID coincidence
 * — same pid, different birth (PID reuse) or a stale/unauthenticated fact —
 * NEVER raises trust above the host baseline.
 */

#define SENSE_XIM_FACTS_MAX 256u

typedef struct sense_xim_child_fact {
    uint32_t pid;
    uint64_t birth_ns;          /* reuse-proof: PID reuse changes this */
    uint32_t owner_instance;
    uint32_t owner_generation;  /* 0 = no live owner (never authenticates) */
    uint64_t context_epoch;
    int      authenticated;     /* fact arrived over an authenticated channel */
} sense_xim_child_fact_t;

void sense_xim_init(void);
void sense_xim_reset(void);

/* Ingest a CHILD fact. Refused (-1) if it carries owner_generation 0
 * (no live owner) or is not authenticated; accepted (0) otherwise. */
int  sense_xim_ingest(const sense_xim_child_fact_t *fact);

/* Correlate a host-process observation against ingested facts and return the
 * trust to assign. `baseline` is the host observer's own trust (e.g.
 * USERSPACE). Returns SENSE_TRUST_MEDIATED only on an authenticated match of
 * BOTH pid and birth_ns under a live owner; otherwise returns `baseline`
 * unchanged. Sets *matched_pid_only_out (optional) when a pid matched but the
 * birth did not — the PID-reuse case that must not elevate. */
sense_trust_t sense_xim_correlate(uint32_t pid, uint64_t birth_ns,
                                  sense_trust_t baseline,
                                  int *matched_pid_only_out);

uint32_t sense_xim_fact_count(void);
49

stub

Внешний наблюдатель и управление одним дочерним worker
src/stub/Исполнение и расширения1 файлов0 API headers

platx-stub — отдельный простой babysitter ровно одного worker process. Он решает минимальную задачу process supervision там, где запуск полного Core не нужен, и не притворяется module supervisor внутри PLATX.

Граница ответственности

  • One worker, one policy, one process tree.
  • No capability/subsystem registry; status не объявляет worker PLATX module.
  • Config/CLI minimal; no plugin/script/network expansion.

Устройство подсистемы

  • State STOPPED/STARTING/RUNNING/BACKOFF/FAILED/STOPPING.
  • Spawn record PID/start token/attempt/window/deadline; wait/reap loop serial.
  • Signal forwarding and shutdown deadline explicit.
  • Restart policy bounded, monotonic and preserves counters.

Поток работы

  • Read validated command/policy.
  • Spawn → monitor waitpid.
  • Exit classify → backoff/restart or FAILED.
  • Signal/stop → terminate/reap → exit status.

Отказ и восстановление

  • Crash loop exhausts budget, no infinite fork.
  • PID reuse avoided by owning child relationship/start token.
  • Parent termination forwards then reaps; no orphan.

Основные возможности

  • Supervises exactly one worker process.
  • Detects exit and applies bounded restart behavior.
  • Kept separate from Core module lifecycle and child host.

Управление и диагностика

В домене есть собственная справка или отдельный parser, но регистрация корневой команды через cmd_register не найдена. Ниже в справочнике сохранена его точная точка входа.

Справочник CLI / stub →
Состав подсистемы / 1 файлов
Файл / компонентНазначение и граница
src/stub/platx_stub.cplatx-stub — process babysitter for exactly one worker. Invariant: spawn / monitor / backoff / safe-mode only. This process does not know modules, capabilities, keyring, mesh, transports, or CLI namespaces. Child sandbox is Core child_host. This file does not know domain-module names, env, argv, or includes.
Контракты API / 0 заголовков

Отдельный заголовок из include/platx/ не закреплён за этим доменом в справочнике. Внутренние границы перечислены в составе файлов; управление и публичные связи описаны в соседних подсистемах.

50

supervisor

Операторское представление и инициирование восстановления
src/supervisor/Управление и контракты6 файлов2 API headers

Операторское представление и инициирование восстановления

Граница ответственности

  • No direct start/stop/create/destroy.
  • No second restart policy/counter.
  • Status read-only; event picture records facts only.

Устройство подсистемы

  • Health aggregation distinguishes module state, descriptor health, dependency coverage и recovery cooldown.
  • Restart command проходит central authz и enqueue MANUAL_RECOVERY intent с target generation.
  • Legacy task heartbeat/reaper emits events to one-brain policy.

Поток работы

  • Health/task/child/CLI fact.
  • Persistent recovery watch update/decision.
  • Lifecycle executor action.

Отказ и восстановление

  • Target disappears after resolve → stale generation reason.
  • Recovery unavailable → command fails, not legacy direct fallback.
  • Event subscription overflow shown count.

Основные возможности

  • Aggregates module health and recovery status.
  • CLI restart produces a recovery/lifecycle request.
  • Policy tests prohibit direct module hook invocation.
Архитектурные детали и инварианты

Overview

The supervisor subsystem monitors child processes and restarts them on failure,

providing robust fault isolation for platform services.

Invariants

- **INV-SUPERVISOR-01**: when restart_count >= max_restarts, the child is

permanently disabled and an audit record is emitted (type 0x53555056).

Components

- supervisor_watch.c — WNOHANG waitpid sweep across 32 static BSS slots

- supervisor_restart.c — restart logic + INV-SUPERVISOR-01 enforcement

- supervisor_policy.c — exponential backoff: doubles per restart, capped at 30 000 ms

Policy

Backoff formula: min(base_ms * 2^restart_count, 30000).

Default base: 1 000 ms. Max restarts default: 5.

Управление и диагностика

Корневые команды: supervisor. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / supervisor →
Состав подсистемы / 6 файлов
Файл / компонентНазначение и граница
src/supervisor/cmd_supervisor.csupervisor status|health|restart. Invariant: the watch table is the only source. Recovery chooses. This file does not keep a second policy, does not speak mbus, and does not call descriptor hooks.
src/supervisor/supervisor.cleftover watchdog. Sensor emit, not apply. plat_recovery decides; this file does not. Picture attach is observer-only.
src/supervisor/supervisor.hleftover in-process watchdog for PLATX. Observes taskmgr tasks and leftover subsys, reaps finished slots, and publishes heartbeat timeouts. Does not decide restart: one name and one budget live in plat_recovery. watch_* refuse recovery identities (mesh, protocol.mesh, ra2c, protocol.ra2c, script) — no second row.
src/supervisor/supervisor_policy.cполитика перезапуска: потолок и экспоненциальная задержка. Единица подсистемы (B) — сторожа дочерних процессов. Сигнатуры берутся из include/platx/platx_supervisor.h: собственных объявлений тут нет намеренно, см. SUP-GAP-02.
src/supervisor/supervisor_restart.cперезапуск мёртвых дочерних процессов по политике. ЗАЧЕМ ЭТОТ ФАЙЛ БОЛЬШЕ НЕ ОБЪЯВЛЯЕТ НИЧЕГО САМ (SUP-GAP-02, A2-W11-007). Здесь стояла ТРЕТЬЯ копия тега sv_child_t (после заголовка и supervisor_watch.c) плюс локальные extern на supervisor_watch_get и supervisor_watch_count. Локальный extern компилируется и тогда, когда он
src/supervisor/supervisor_watch.cсторож ДОЧЕРНИХ ПРОЦЕССОВ (INV-SUPERVISOR-01). ЗАЧЕМ ФАЙЛ ВЫГЛЯДИТ ИМЕННО ТАК (SUP-GAP-02, A2-W11-006/007). Здесь стояло СОБСТВЕННОЕ определение тега sv_child_t (name[32] вместо name[48], лишнее policy_idx, отсутствующие slot/max_restarts/backoff_ms) и собственная сигнатура supervisor_watch_add(pid, name, policy_idx),
Контракты API / 2 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/platx_supervisor.h
/* platx_supervisor.h — публичный контракт СТОРОЖА ДОЧЕРНИХ ПРОЦЕССОВ.
 *
 * ЧТО ЭТО И ЧТО ЭТО НЕ ТО (A2-W11-008)
 *
 * Под словом «supervisor» в дереве живут ДВЕ несвязанные подсистемы:
 *
 *   (A) src/supervisor/supervisor.h  — наблюдатель остатков (leftover):
 *       sup_item_t, supervisor_watch_task/subsys, supervisor_tick,
 *       supervisor_snapshot. Он НЕ рестартит: решение RESTART/FAIL по C7
 *       принадлежит plat_recovery. Единица supervisor.c стоит в
 *       PLATX_FULL_PRODUCT_SRCS — то есть поставляется.
 *
 *   (B) ЭТОТ заголовок — сторож дочерних ПРОЦЕССОВ: sv_child_t,
 *       supervisor_watch_add/sweep, supervisor_restart_sweep,
 *       supervisor_policy_*. Единицы supervisor_watch.c / _restart.c /
 *       _policy.c в манифестах НЕ стоят ни в одном.
 *
 * Общего у них ровно одно — префикс имени. Ни один потребитель не должен
 * включать оба заголовка: пересечения типов нет, а пересечение смысла есть,
 * и именно оно порождало правки не в той подсистеме. Механическая проверка —
 * `make gate-a2-w11-supervisor-split`.
 *
 * ПОЧЕМУ ЭТОТ ФАЙЛ ПЕРЕПИСАН 09.09.2026 (SUP-GAP-02, A2-W11-006/007)
 *
 * Заголовок объявлял supervisor_watch_add(name, pid, max_restarts), а
 * реализация определяла supervisor_watch_add(pid, name, policy_idx) —
 * РАЗНЫЙ ПОРЯДОК И РАЗНЫЙ СМЫСЛ аргументов под одним именем. Компилятор
 * сверяет вызов с ВИДИМЫМ объявлением, линкер — только имя, поэтому
 * потребитель, включивший этот заголовок, отдавал указатель на строку в
 * pid_t и число 4242 в const char*: strncpy читал бы по адресу 4242.
 * Тест этого не ловил, потому что нёс собственный локальный extern с
 * порядком реализации — то есть смотрел не на ту функцию, которую видит
 * потребитель публичного API. Класс A-F001, TE-SRC-12.
 *
 * Кроме того тег sv_child_t был определён ТРИЖДЫ (здесь, в
 * supervisor_watch.c и в supervisor_restart.c) с разной раскладкой полей:
 * name[48] против name[32], отсутствующие alive/policy_idx. Три
 * несовпадающих определения одного тега — это не дублирование текста, это
 * три разных представления одной памяти в трёх единицах трансляции.
 *
 * ИНВАРИАНТ: одно имя — одна сигнатура — одно определение типа, и оба
 * .c включают ЭТОТ файл, а не переобъявляют его содержимое у себя.
 */
#define SUPERVISOR_SLOTS        32
#define SUPERVISOR_NAME_LEN     48
#define SUPERVISOR_DEFAULT_BACKOFF_MS  1000
#define SUPERVISOR_MAX_BACKOFF_MS     30000

/* Единственное определение тега. Раскладка едина для заголовка, обеих
 * единиц реализации и любого теста: расхождение ловится
 * supervisor_child_abi() и tests/supervisor/roadmap_a2_w11_supabi.c. */
typedef struct {
    int      slot;                  /* номер слота; -1 = недействителен     */
    char     name[SUPERVISOR_NAME_LEN];
    pid_t    pid;
    int      in_use;                /* 0 = слот свободен                    */
    int      alive;                 /* 1 = процесс жив по последнему sweep  */
    int      restart_count;
    int      max_restarts;          /* 0 = без ограничения (см. watch_add)  */
    int      permanently_disabled;  /* INV-SUPERVISOR-01                    */
    int      backoff_ms;            /* задержка, применённая последней      */
    uint32_t policy_idx;            /* номер политики перезапуска           */
} sv_child_t;

/* Раскладка sv_child_t глазами ПРОДУКТОВОЙ единицы трансляции.
 * Существует не для красоты: расхождение раскладки между заголовком и
 * реализацией — это ровно тот дефект, который здесь исправляется, и он
 * обязан быть измерим, а не только описан в комментарии. */
typedef struct {
    size_t size;
    size_t off_slot, off_name, off_pid, off_in_use, off_alive;
    size_t off_restart_count, off_max_restarts, off_permanently_disabled;
    size_t off_backoff_ms, off_policy_idx;
    size_t name_len;
} sv_child_abi_t;
void supervisor_child_abi(sv_child_abi_t *out);

/* Lifecycle. init обнуляет таблицу наблюдения; fini — то же самое, имя
 * оставлено ради симметрии вызова. */
void supervisor_init(void);
void supervisor_fini(void);

/* Взять дочерний процесс под наблюдение.
 *
 * max_restarts: 0 = без ограничения на число перезапусков.
 * Политика перезапуска — номер 0 (задержка по умолчанию).
 * Возвращает номер слота (>= 0) либо -1.
 *
 * ПОРЯДОК АРГУМЕНТОВ (name, pid, max) — единственный. Вызов в прежнем
 * порядке реализации (pid, name, idx) больше не собирается: строка не
 * приводится к pid_t молча. Проверяется целью
 * `make gate-a2-w11-supervisor-argorder`. */
int  supervisor_watch_add(const char *name, pid_t pid, int max_restarts);

/* Тот же приём под наблюдение, но потолок перезапусков берётся из ПОЛИТИКИ
 * с номером policy_idx и сохраняется в слоте числом — чтобы читатель слота
 * видел то ограничение, которое будет применено, а не номер таблицы.
 *
 * Это второе значение третьего аргумента прежней сигнатуры. Оно не
 * потеряно и не переименовано молча: у него теперь собственное имя, потому
 * что «сколько раз можно перезапускать» и «по какой политике ждать» — два
 * разных вопроса, и один аргумент не может отвечать на оба. */
int  supervisor_watch_add_policy(const char *name, pid_t pid,
                                 uint32_t policy_idx);

/* Снять с наблюдения (например, после намеренной остановки). */
int  supervisor_watch_remove(int slot);

/* Обход: waitpid(WNOHANG) по всем наблюдаемым. Возвращает число
 * обнаруженных смертей. Перезапуск НЕ делает: он в supervisor_restart_sweep. */
int  supervisor_watch_sweep(void);

/* Внутренний доступ к слоту (используется supervisor_restart.c и тестами).
 * Указатель живёт до supervisor_init()/fini(). */
sv_child_t *supervisor_watch_get(uint32_t idx);
uint32_t    supervisor_watch_count(void);
void        supervisor_watch_reset(void);

/* Снимок одного слота. 0 — успех, -1 — недействительный номер. */
int  supervisor_get_child(int slot, sv_child_t *out);

/* Перезапуск мёртвых по политике. spawn возвращает новый pid или <= 0.
 * INV-SUPERVISOR-01: restart_count >= max_restarts (при max_restarts > 0)
 * → слот навсегда отключён + запись в audit. */
typedef pid_t (*sv_spawn_fn)(const char *name, void *ctx);
int  supervisor_restart_sweep(sv_spawn_fn spawn, void *ctx);

/* Политика перезапуска. */
int      supervisor_policy_add(int max_restarts, int backoff_base_ms);
int      supervisor_policy_max_restarts(uint32_t policy_idx);
/* policy_idx выбирает политику, restart_count — какая это по счёту попытка.
 * Заголовок до 09.09.2026 объявлял только restart_count, тогда как
 * реализация всегда брала два аргумента: потребитель передал бы счётчик в
 * policy_idx и получил бы политику по номеру попытки. Класс A-F001. */
int      supervisor_policy_backoff_ms(uint32_t policy_idx, int restart_count);
uint32_t supervisor_policy_count(void);
void     supervisor_policy_reset(void);
include/platx/supervisor_graph.h
/* platx/supervisor_graph.h — Supervisor module graph (INT-114). */
#define PLAT_SUP_GRAPH_MAX  32
#define PLAT_SUP_NAME_MAX   64

typedef struct {
    char     name[PLAT_SUP_NAME_MAX];
    int      watched;
    int      status;       /* sup_status_t cast */
    int      restart_count;
    uint64_t last_heartbeat_ms;
} plat_sup_node_t;

typedef struct {
    plat_sup_node_t nodes[PLAT_SUP_GRAPH_MAX];
    int             n_nodes;
} plat_sup_graph_t;

/* Populate graph from supervisor_snapshot.  Returns n_nodes, -1 on error. */
int plat_sup_graph_build(plat_sup_graph_t *out);

/* Publish graph summary to MBus topic "supervisor.graph".  0/-1. */
int plat_sup_graph_publish(const plat_sup_graph_t *g);
51

surface

Ограниченная проверка возможностей операционной системы
src/surface/Управление и контракты1 файлов1 API headers

Surface выполняет bounded read-only probe OS/kernel/runtime features и выдаёт evidence для profile/isolation diagnostics. Он не публикует PLATX capabilities и не выполняет privileged setup.

Граница ответственности

  • Detected feature не означает permission/health of module provider.
  • No arbitrary command execution from probes.

Устройство подсистемы

  • Probe descriptor name/version/cost/timeout and result schema.
  • Runner executes allowlisted syscall/file checks with deadlines and caches generation.
  • Facts include present/absent/unknown, reason, kernel/platform version.
  • Profile validator uses facts for preflight but module start verifies own requirements.

Поток работы

  • Boot/diagnostic request → selected probes.
  • Profile/isolation explain consumes.

Отказ и восстановление

  • Permission denied distinct from unsupported.
  • Timeout returns unknown; no blocking boot forever.
  • Kernel state changes invalidates cache by refresh policy.
  • Malformed proc/sys data bounds checked.

Основные возможности

  • Probes available OS/kernel facilities.
  • Keeps detection bounded and side-effect free.
  • Provides input to profile/isolation diagnostics.

Управление и диагностика

Домен не регистрирует собственную корневую команду. Его контракт используется вызывающими подсистемами через API и интерфейсы core.

Справочник CLI / surface →
Состав подсистемы / 1 файлов
Файл / компонентНазначение и граница
src/surface/plat_surface.cbounded OS probe. Not a snapshot. Not capreg. One path: open one file or one syscall, then close. Walking /proc, reading maps/fd of other pids, or leaving a live pidfd is execute. Missing root → UNAVAILABLE. Never guess AVAILABLE.
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/surface.h
/* platx/surface.h — what the OS actually exposes, not what we promise.
 *
 * Probe is a question. It does not provide() a capability and it is not
 * a fifth registry: plat_capreg still owns names and vtables. Gadget
 * asks here whether a path exists, then decides how to execute.
 *
 * Fail closed: if /proc cannot be read, the answer is UNAVAILABLE.
 * AVAILABLE is only after a bounded check with no collect. A snapshot
 * of processes is execute, not probe.
 */

/* Feature ids. Gadget passes these; do not invent aliases. */
#define PLAT_SURFACE_ID_PROCFS            "native.procfs"
#define PLAT_SURFACE_ID_PIDFD             "native.pidfd"

/* Capability question for the SNAPSHOT path. Not a capreg provide(). */
#define PLAT_SURFACE_CAP_PROCESS_SNAPSHOT "process.snapshot:v1"

#define PLAT_SURFACE_REASON_MAX 128

typedef enum plat_surface_state {
    PLAT_SURFACE_UNKNOWN = 0,   /* zeroed result is not AVAILABLE */
    PLAT_SURFACE_DISCOVERED,
    PLAT_SURFACE_AVAILABLE,
    PLAT_SURFACE_UNAVAILABLE,
    PLAT_SURFACE_DEGRADED
} plat_surface_state_t;

typedef enum plat_surface_quality {
    PLAT_SURFACE_Q_NONE = 0,
    PLAT_SURFACE_Q_SNAPSHOT,
    PLAT_SURFACE_Q_EXACT
} plat_surface_quality_t;

typedef struct plat_surface_result {
    plat_surface_state_t   state;
    plat_surface_quality_t quality;
    const char            *id;
    char                   reason[PLAT_SURFACE_REASON_MAX];
} plat_surface_result_t;

/* Host /proc. Returns 0 when `out` is filled; -1 on bad args. */
int plat_surface_probe(const char *id, plat_surface_result_t *out);

/* Same probe with an explicit proc root (NULL → /proc). Used when the
 * caller is not in the host mount ns, and by the fail-closed tooth. */
int plat_surface_probe_at(const char *id, const char *proc_root,
                          plat_surface_result_t *out);

const char *plat_surface_state_str(plat_surface_state_t st);
const char *plat_surface_quality_str(plat_surface_quality_t q);
52

taskmgr

Учёт задач, heartbeat, владельцы и ограничения
src/taskmgr/Управление и контракты5 файлов3 API headers

Taskmgr даёт operator view и control managed Core tasks: list/top/limits/cancel. Он опирается на plat_task ownership и не должен управлять произвольными pthread/OS processes.

Граница ответственности

  • Task creation/state source — Core Task API.
  • kill command означает controlled cancel/escalation policy, не raw pthread_kill.

Устройство подсистемы

  • Task record owner/generation, id, state, start/heartbeat/end, cancellation, join, limits и labels.
  • Limits apply admission/runtime quotas through Task API.
  • Reaper removes terminal rows after retention, preserving audit.

Поток работы

  • Module spawn via Task API → record.
  • Heartbeat/state/resource metrics update.
  • Terminal join → retention/reap.

Отказ и восстановление

  • Table full triggers safe terminal reap or rejects spawn.
  • Cancel timeout escalates owner recovery, not fake KILLED.
  • Owner restart invalidates old task handles.

Основные возможности

  • Lists and ranks managed runtime tasks.
  • Applies task limits and controlled cancellation.
  • Integrates ownership/heartbeat rather than enumerating arbitrary pthreads.
Архитектурные детали и инварианты

Overview

Tracks every platform thread (name, owner, priority, TID, CPU usage).

Provides concurrency limits per owner and a heartbeat watchdog.

Invariants

- **INV-TASKMGR-01**: a task submitted without a deadline is assigned

now + MAX_TASK_TTL (300 s) at queue-push time.

Components

- taskmgr.c — core: 256-slot static BSS table, register/started/done/kill

- taskmgr_queue.c — 128-slot MPSC work queue with INV-TASKMGR-01 enforcement

- taskmgr_sched.c — deadline-aware scheduler; sched_tick() dispatches due tasks

Управление и диагностика

Корневые команды: taskmgr. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / taskmgr →
Состав подсистемы / 5 файлов
Файл / компонентНазначение и граница
src/taskmgr/cmd_taskmgr.cCLI namespace "taskmgr" for the platform task manager. taskmgr list [--owner=S] [--state=running|done|all] taskmgr top [--interval=N] live view (single snapshot in batch mode) taskmgr kill --id=N kill a single task taskmgr kill --owner=S kill all tasks of an owner
src/taskmgr/taskmgr.cenhanced platform task manager implementation.
src/taskmgr/taskmgr.henhanced platform task manager. Tracks every thread started by any subsystem or plugin. - Per-task attribution: name + owner (subsystem/plugin) - Priorities: LOW / NORMAL / HIGH / CRITICAL - Concurrent task limits per owner - Kill support: graceful (cancel_fn) → pthread_cancel
src/taskmgr/taskmgr_queue.cРеализация taskmgr / queue
src/taskmgr/taskmgr_sched.cDrains the queue in priority+deadline order and submits to taskmgr. INV-TASKMGR-01 is enforced at push time in taskmgr_queue.c.
Контракты API / 3 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/task.h
/* platx/task.h — Core owns pthread_create for EMBEDDED. */
typedef uint64_t plat_task_id_t;
typedef void *(*plat_task_entry_t)(void *arg);

typedef struct plat_task_opts {
    int heartbeat_sec;   /* 0 = off */
    int joinable;        /* 1 = Core joins on cancel; always joinable in Core */
} plat_task_opts_t;

/* Stopping a task has three states, and cancel does not reach the last one:
 *
 *   TIMED_OUT         a bounded join expired. Says nothing about the thread.
 *   CANCEL_REQUESTED  cancel was delivered. The thread may still be running.
 *   CANCELLED         the thread exited. Only join confirms this.
 *
 * Same three words as the DSL step timeout (src/dsl/dsl_exec.c run_with_timeout),
 * because it is the same distinction: a request is not an outcome. A task still
 * in CANCEL_REQUESTED keeps its slot, keeps counting in plat_task_count_owner,
 * and comes back as leftover from plat_task_reap_owner. */
typedef struct plat_task_api {
    int (*spawn)(plat_owner_t owner, const char *name,
                 plat_task_entry_t entry, void *arg,
                 const plat_task_opts_t *opts, plat_task_id_t *out);
    /* Same triple as spawn. Wrong owner/generation: fail, do not touch tid.
     *
     * CANCEL_REQUESTED, never CANCELLED. 0 = the request was delivered (or was
     * moot because the thread had already exited); the slot stays live either
     * way. -1/ESRCH = no such live task for this owner. -1/EDEADLK = self. */
    int (*cancel)(plat_owner_t owner, plat_task_id_t id);
    /* The only confirmation of an exit. 0 = CANCELLED: the thread is gone and
     * its slot is cleared. -1 names which state actually holds, by errno:
     *
     *   EAGAIN   still running; the wait expired. Slot deliberately kept.
     *   ESRCH    no such live task for this owner/generation.
     *   EBUSY    another caller is already joining it.
     *   EDEADLK  a task tried to join itself.
     *
     * timeout_ms < 0 waits forever; 0 polls (0 = exited, EAGAIN = alive). This
     * errno contract is the one way to ask; there is no second state call. */
    int (*join)(plat_owner_t owner, plat_task_id_t id, int timeout_ms);
    void (*heartbeat)(plat_task_id_t id);
} plat_task_api_t;

int  plat_task_init(void);
void plat_task_fini(void);
/* Cancel+join every live task of this owner. timeout_ms < 0 = wait forever.
 * Any leftover thread: -1, slots of survivors stay (no fake success). */
int  plat_task_reap_owner(plat_owner_t owner, int timeout_ms);
int  plat_task_count_owner(plat_owner_t owner);
/* CLOCK_MONOTONIC seconds. Unknown/dead id → -1, *out unchanged. */
int  plat_task_last_heartbeat(plat_task_id_t id, time_t *out_monotonic);
/* 1 if a live owner task has heartbeat_sec>0 and silence > 2×heartbeat_sec. */
int  plat_task_owner_silent(plat_owner_t owner);
extern const plat_task_api_t plat_task_api;
include/platx/task_owner.h
/* platx/task_owner.h — A1-P04: то, чего опубликованный plat_task_api_t
 * выразить не может.
 *
 * `platx/task.h` заморожен вместе с plat_abi_t: четыре функции, структура
 * опций из двух int и ни одного места, куда можно положить лимит профиля,
 * размер стека, состояние задачи или снимок heartbeat. Расширять
 * `plat_task_opts_t` нельзя — её размер входит в раскладку, которую A3
 * сверяет своей fixture. Поэтому всё новое живёт здесь, отдельными функциями,
 * и ни один offset в ABI не двигается.
 *
 * ЧТО ЗДЕСЬ ЕСТЬ
 *
 *   plat_task_spawn_ex()      порождение с атрибутами и копией аргумента
 *   plat_task_state()         CANCEL_REQUESTED отдельно от терминального
 *   plat_task_snapshot()      heartbeat без взятия таблицы задач
 *   plat_task_reap_owner_by() уборка с ОДНИМ дедлайном на всех
 *   plat_task_limit()         лимит профиля, применяемый до pthread_create
 *
 * SPDX-License-Identifier: GPL-2.0
 */

#define PLAT_TASK_MAX 128

/* ── Состояния (A1-P04-174) ──────────────────────────────────────────────
 *
 * Запрос отмены и факт завершения — разные вещи, и это единственная причина,
 * по которой перечисление существует. `cancel()` возвращает 0, когда запрос
 * ДОСТАВЛЕН; поток при этом может работать ещё сколько угодно, а вызывающий,
 * читающий этот ноль как «задача снята», читает несуществующий факт.
 *
 *   RESERVED  слот занят, pthread_create ещё не вернулся. Задача уже
 *             занимает ёмкость и уже считается в count_owner — иначе
 *             остановка увидит ноль задач у владельца, у которого прямо
 *             сейчас рождается поток (A1-P04-170).
 *   LIVE      поток создан, отмена не запрашивалась.
 *   CANCEL_REQUESTED  отмена доставлена, поток НЕ подтверждён завершившимся.
 *             Слот жив, задача считается, reap вернёт её как остаток.
 *   EXITED    подтверждено join'ом. Слот в этот момент уже очищен, поэтому
 *             снаружи это состояние видно только в снимке, снятом раньше. */
typedef enum plat_task_state {
    PLAT_TASK_ST_NONE             = 0,
    PLAT_TASK_ST_RESERVED         = 1,
    PLAT_TASK_ST_LIVE             = 2,
    PLAT_TASK_ST_CANCEL_REQUESTED = 3,
    PLAT_TASK_ST_EXITED           = 4
} plat_task_state_t;

const char *plat_task_state_name(plat_task_state_t s);

/* Состояние задачи владельца. -1 (и *out не тронут), если задачи нет. */
int plat_task_state(plat_owner_t owner, plat_task_id_t id,
                    plat_task_state_t *out);

/* ── Атрибуты порождения (A1-P04-168) ────────────────────────────────────
 *
 * stack_bytes == 0 — системный размер по умолчанию. Иначе значение обязано
 * лежать в [PLAT_TASK_STACK_MIN, PLAT_TASK_STACK_MAX] и быть кратным 4096.
 * Отказ явный и до pthread_create: стек ниже минимума не «работает хуже», он
 * даёт переполнение в первом же глубоком вызове, и обнаружится это как
 * повреждение памяти в чужом коде.
 *
 * guard_pages == 0 — системная защитная страница; отрицательное значение
 * запрещено. Отключить guard page нельзя вовсе: это не настройка
 * производительности, а единственное, что превращает переполнение стека в
 * сигнал вместо тихой записи в соседний стек. */
#define PLAT_TASK_STACK_MIN   (64u * 1024u)
#define PLAT_TASK_STACK_MAX   (64u * 1024u * 1024u)
#define PLAT_TASK_STACK_GRAIN 4096u

/* Аргумент, скопированный внутрь слота (A1-P04-180).
 *
 * Слот живёт до подтверждённого завершения задачи, поэтому копия переживает
 * любой стек вызывающего. Это закрывает случай, ради которого карточка и
 * заведена: породили задачу, вернулись из функции, локальная структура с
 * аргументом умерла, а задача только начала выполняться.
 *
 * Размер намеренно мал: сюда кладут дескриптор, индекс, пару чисел. Большой
 * контекст остаётся заботой вызывающего — ядро не заводит кучу ради него. */
#define PLAT_TASK_ARG_INLINE 64u

/* Срок, который releaser ресурса PLAT_RES_THREAD даёт задаче на завершение.
 * Не «сколько нужно», а «сколько можно ждать в чужой уборке»: releaser
 * вызывается из sweep владельца, у которого есть собственный дедлайн, и
 * бесконечное ожидание здесь съело бы его целиком. Не уложился — отказ,
 * строка остаётся в RELEASE_FAILED, а не превращается в успех. */
#define PLAT_TASK_RELEASE_JOIN_MS 2000

typedef struct plat_task_attr {
    size_t stack_bytes;   /* 0 = по умолчанию */
    int    guard_pages;   /* 0 = по умолчанию; < 0 запрещено */
    int    heartbeat_sec; /* 0 = heartbeat выключен */
    /* Если arg_len > 0, ровно arg_len байт из arg_copy кладутся в слот, и
     * точка входа получает указатель на КОПИЮ, а не на arg_copy. */
    const void *arg_copy;
    size_t      arg_len;
} plat_task_attr_t;

/* Значения по умолчанию: всё нулями — валидная конфигурация. */
void plat_task_attr_init(plat_task_attr_t *a);

/* Проверка атрибутов без порождения. 0 — годны; -1 и errno:
 *   EINVAL  размер стека вне диапазона или не кратен PLAT_TASK_STACK_GRAIN,
 *           guard_pages < 0, arg_len > PLAT_TASK_ARG_INLINE. */
int plat_task_attr_check(const plat_task_attr_t *a);

/* Порождение с атрибутами. Все отказы происходят ДО pthread_create:
 *   EINVAL     аргументы или атрибуты не годятся
 *   EAGAIN     таблица задач заполнена
 *   ENOSPC     превышен лимит профиля (A1-P04-167) — отдельный код, потому
 *              что «политика запрещает» чинится не тем же, чем «мест нет»
 *   EPERM      владелец запечатан остановкой (A1-P04-181)
 *   EOVERFLOW  идентификаторы задач исчерпаны
 * Отличать их обязательно: первая сверхлимитная задача должна получить отказ
 * до создания потока, а не после. */
int plat_task_spawn_ex(plat_owner_t owner, const char *name,
                       plat_task_entry_t entry, void *arg,
                       const plat_task_attr_t *attr, plat_task_id_t *out);

/* ── Лимит профиля (A1-P04-167) ──────────────────────────────────────────
 *
 * Действующий лимит = min(PLAT_TASK_MAX, limits.max_tasks профиля), где ноль
 * в профиле означает «не задан» и лимитом не является. Возвращается всегда
 * применяемое число, а не то, что написано в профиле: если профиль разрешает
 * больше, чем помещается в таблице, применяется таблица, и диагностика должна
 * видеть именно её. */
size_t plat_task_limit(void);
size_t plat_task_live_total(void);

/* ── Барьер остановки (A1-P04-181) ───────────────────────────────────────
 *
 * После seal владелец не может породить новую задачу: spawn — EPERM. Ровно
 * тот же смысл, что у plat_res_owner_seal(): между «останавливаемся» и
 * «остановились» не должно быть окна, в которое проскакивает новая работа. */
int plat_task_owner_seal(plat_owner_t owner);
int plat_task_owner_sealed(plat_owner_t owner);
int plat_task_owner_unseal(plat_owner_t owner);   /* fixtures */

/* ── Снимок без лока таблицы (A1-P04-183) ────────────────────────────────
 *
 * Watchdog обязан читать heartbeat в тот момент, когда с задачей что-то не
 * так, — а «что-то не так» часто означает, что worker держит лок таблицы. Тот,
 * кто ждёт лок, чтобы узнать, не завис ли держатель лока, зависает вместе с
 * ним. Поэтому публикация идёт через seqlock на слот: писатель под g_mu,
 * читатель — без единой блокировки.
 *
 * Возвращает число заполненных строк, либо -1, если снимок не сошёлся за
 * отведённые попытки (таблица переписывалась всё это время). -1 — это
 * «измерение недоступно», а не «задач нет»: разница принципиальна для
 * evidence, и ноль вместо неё был бы ложным фактом. */
typedef struct plat_task_view {
    plat_task_id_t    id;
    plat_owner_t      owner;
    plat_task_state_t state;
    int64_t           beat_mono_s;
    int               heartbeat_sec;
    int               silent;        /* 1 = молчит дольше 2×heartbeat_sec */
    char              name[48];
} plat_task_view_t;

int plat_task_snapshot(const plat_owner_t *owner,
                       plat_task_view_t *out, size_t cap, size_t *n_out);

/* ── Допуск молчания из профиля (A1-P04-184) ─────────────────────────────
 *
 * «2× heartbeat_sec» было зашитым числом: его никто не утверждал, и изменить
 * его нельзя было, не пересобрав ядро. Теперь допуск берётся из
 * plat_watchdog_policy_t опубликованного профиля —
 * interval_ms × max_strikes, — а зашитое правило остаётся только там, где
 * профиля нет вовсе.
 *
 * Причина молчания названа отдельным кодом, а не выводится из нуля или
 * единицы. Разница содержательная: «задача не настроена стучать» и «задача
 * настроена и молчит» требуют разных действий, а «измерить не удалось» не
 * является ни тем, ни другим и не должно превращаться в «всё в порядке».
 *
 * Устаревший heartbeat НЕ трогает поколение владельца: молчание — повод
 * принять решение о восстановлении, а не само решение. Смена поколения
 * принадлежит lifecycle, и heartbeat её не инициирует. */
typedef enum plat_task_silence {
    PLAT_TASK_SIL_OK           = 0,  /* стучит вовремя                     */
    PLAT_TASK_SIL_NOT_ARMED    = 1,  /* heartbeat_sec == 0, стучать нечему */
    PLAT_TASK_SIL_SILENT       = 2,  /* настроена и молчит дольше допуска  */
    PLAT_TASK_SIL_UNMEASURABLE = 3,  /* снимок не сошёлся или часы отказали */
    PLAT_TASK_SIL_NO_TASKS     = 4   /* у владельца нет живых задач        */
} plat_task_silence_t;

const char *plat_task_silence_name(plat_task_silence_t s);

/* Действующий допуск молчания в СЕКУНДАХ для задачи с таким heartbeat_sec.
 * Профиль важнее зашитого правила; 0 означает «допуск не определён».
 *
 * Гранулярность намеренно посекундная: отметка heartbeat хранится и отдаётся
 * как time_t (plat_task_last_heartbeat), поэтому разность двух отметок точна
 * до ±1 с. Допуск меньше двух секунд задавать бессмысленно — он неотличим от
 * соседних значений, и watchdog на нём будет срабатывать через раз. Профиль,
 * запросивший меньше секунды, получает одну, а не ноль: обнулять допуск
 * значило бы считать молчащей каждую задачу. */
int plat_task_silence_limit_s(int heartbeat_sec);

/* Худший исход по всем живым задачам владельца. UNMEASURABLE важнее OK:
 * недоступное измерение не является подтверждением исправности. */
plat_task_silence_t plat_task_owner_silence(plat_owner_t owner);

/* Heartbeat одной задачи без лока. 0 — заполнено; -1 — не сошлось или нет
 * такой задачи (различить помогает plat_task_state()). */
int plat_task_beat_nolock(plat_task_id_t id, int64_t *out_mono_s);

/* ── Уборка с общим дедлайном (A1-P04-164/179/194) ───────────────────────
 *
 * plat_task_reap_owner(owner, timeout_ms) даёт КАЖДОЙ задаче свой timeout_ms;
 * при сорока задачах согласованный бюджет остановки молча умножается на сорок.
 * Здесь бюджет один на всю уборку и отсчитывается по CLOCK_MONOTONIC.
 *
 * Задача, не успевшая завершиться, остаётся живой и попадает в leftover —
 * слот не очищается, идентификатор не переиспользуется, успех над
 * незавершённым потоком не возвращается. */
typedef struct plat_task_reap_result {
    size_t found;
    size_t joined;
    size_t leftover;   /* остались после дедлайна */
    int    timed_out;
} plat_task_reap_result_t;

int plat_task_reap_owner_by(plat_owner_t owner, int budget_ms,
                            plat_task_reap_result_t *out);

/* Монотонные миллисекунды из того же источника, что и дедлайны выше. */
uint64_t plat_task_mono_ms(void);

/* ── Счётчики (A1-P04-175/182) ───────────────────────────────────────────
 *
 * finalized — сколько раз слот был очищен подтверждённым завершением. Это
 * счётчик exactly-once: гонка cancel и штатного выхода обязана дать ровно
 * одну финализацию на задачу, и fixture сверяет именно это число, а не
 * косвенный признак.
 *
 * heartbeats и completions разделены намеренно (A1-P04-182): heartbeat — это
 * «я ещё жив», completion — «меня больше нет». Один счётчик на оба смысла
 * позволял бы зависшей задаче, продолжающей стучать, выглядеть завершённой. */
typedef struct plat_task_counters {
    uint32_t reserved;
    uint32_t live;
    uint32_t cancel_requested;
    uint64_t spawned;
    uint64_t spawn_refused_limit;   /* лимит профиля            */
    uint64_t spawn_refused_full;    /* таблица                  */
    uint64_t spawn_refused_sealed;  /* барьер остановки         */
    uint64_t spawn_rollback;        /* pthread_create отказал   */
    uint64_t cancels;
    uint64_t finalized;             /* подтверждённые завершения */
    uint64_t join_timeouts;
    uint64_t join_busy;             /* второй joiner            */
    uint64_t heartbeats;
    uint64_t reap_timeouts;
} plat_task_counters_t;

void plat_task_counters_get(plat_task_counters_t *out);
/* Точки инъекции ТОЛЬКО для тестовых сборок фикстур A1-P04. Продуктовая
 * сборка PLAT_TASK_TEST_HOOKS не определяет, и этих символов в ней нет.
 *   fail_next_create — следующий spawn получит отказ create с этим errno (171);
 *   mu_lock/mu_unlock — удержать/отпустить внутреннюю таблицу задач (183). */
void plat_task_test_fail_next_create(int err);
void plat_task_test_mu_lock(void);
void plat_task_test_mu_unlock(void);
include/platx/task_owner.sync-conflict-20260912-171433-APFVAKO.h
/* platx/task_owner.h — A1-P04: то, чего опубликованный plat_task_api_t
 * выразить не может.
 *
 * `platx/task.h` заморожен вместе с plat_abi_t: четыре функции, структура
 * опций из двух int и ни одного места, куда можно положить лимит профиля,
 * размер стека, состояние задачи или снимок heartbeat. Расширять
 * `plat_task_opts_t` нельзя — её размер входит в раскладку, которую A3
 * сверяет своей fixture. Поэтому всё новое живёт здесь, отдельными функциями,
 * и ни один offset в ABI не двигается.
 *
 * ЧТО ЗДЕСЬ ЕСТЬ
 *
 *   plat_task_spawn_ex()      порождение с атрибутами и копией аргумента
 *   plat_task_state()         CANCEL_REQUESTED отдельно от терминального
 *   plat_task_snapshot()      heartbeat без взятия таблицы задач
 *   plat_task_reap_owner_by() уборка с ОДНИМ дедлайном на всех
 *   plat_task_limit()         лимит профиля, применяемый до pthread_create
 *
 * SPDX-License-Identifier: GPL-2.0
 */

#define PLAT_TASK_MAX 128

/* ── Состояния (A1-P04-174) ──────────────────────────────────────────────
 *
 * Запрос отмены и факт завершения — разные вещи, и это единственная причина,
 * по которой перечисление существует. `cancel()` возвращает 0, когда запрос
 * ДОСТАВЛЕН; поток при этом может работать ещё сколько угодно, а вызывающий,
 * читающий этот ноль как «задача снята», читает несуществующий факт.
 *
 *   RESERVED  слот занят, pthread_create ещё не вернулся. Задача уже
 *             занимает ёмкость и уже считается в count_owner — иначе
 *             остановка увидит ноль задач у владельца, у которого прямо
 *             сейчас рождается поток (A1-P04-170).
 *   LIVE      поток создан, отмена не запрашивалась.
 *   CANCEL_REQUESTED  отмена доставлена, поток НЕ подтверждён завершившимся.
 *             Слот жив, задача считается, reap вернёт её как остаток.
 *   EXITED    подтверждено join'ом. Слот в этот момент уже очищен, поэтому
 *             снаружи это состояние видно только в снимке, снятом раньше. */
typedef enum plat_task_state {
    PLAT_TASK_ST_NONE             = 0,
    PLAT_TASK_ST_RESERVED         = 1,
    PLAT_TASK_ST_LIVE             = 2,
    PLAT_TASK_ST_CANCEL_REQUESTED = 3,
    PLAT_TASK_ST_EXITED           = 4
} plat_task_state_t;

const char *plat_task_state_name(plat_task_state_t s);

/* Состояние задачи владельца. -1 (и *out не тронут), если задачи нет. */
int plat_task_state(plat_owner_t owner, plat_task_id_t id,
                    plat_task_state_t *out);

/* ── Атрибуты порождения (A1-P04-168) ────────────────────────────────────
 *
 * stack_bytes == 0 — системный размер по умолчанию. Иначе значение обязано
 * лежать в [PLAT_TASK_STACK_MIN, PLAT_TASK_STACK_MAX] и быть кратным 4096.
 * Отказ явный и до pthread_create: стек ниже минимума не «работает хуже», он
 * даёт переполнение в первом же глубоком вызове, и обнаружится это как
 * повреждение памяти в чужом коде.
 *
 * guard_pages == 0 — системная защитная страница; отрицательное значение
 * запрещено. Отключить guard page нельзя вовсе: это не настройка
 * производительности, а единственное, что превращает переполнение стека в
 * сигнал вместо тихой записи в соседний стек. */
#define PLAT_TASK_STACK_MIN   (64u * 1024u)
#define PLAT_TASK_STACK_MAX   (64u * 1024u * 1024u)
#define PLAT_TASK_STACK_GRAIN 4096u

/* Аргумент, скопированный внутрь слота (A1-P04-180).
 *
 * Слот живёт до подтверждённого завершения задачи, поэтому копия переживает
 * любой стек вызывающего. Это закрывает случай, ради которого карточка и
 * заведена: породили задачу, вернулись из функции, локальная структура с
 * аргументом умерла, а задача только начала выполняться.
 *
 * Размер намеренно мал: сюда кладут дескриптор, индекс, пару чисел. Большой
 * контекст остаётся заботой вызывающего — ядро не заводит кучу ради него. */
#define PLAT_TASK_ARG_INLINE 64u

/* Срок, который releaser ресурса PLAT_RES_THREAD даёт задаче на завершение.
 * Не «сколько нужно», а «сколько можно ждать в чужой уборке»: releaser
 * вызывается из sweep владельца, у которого есть собственный дедлайн, и
 * бесконечное ожидание здесь съело бы его целиком. Не уложился — отказ,
 * строка остаётся в RELEASE_FAILED, а не превращается в успех. */
#define PLAT_TASK_RELEASE_JOIN_MS 2000

typedef struct plat_task_attr {
    size_t stack_bytes;   /* 0 = по умолчанию */
    int    guard_pages;   /* 0 = по умолчанию; < 0 запрещено */
    int    heartbeat_sec; /* 0 = heartbeat выключен */
    /* Если arg_len > 0, ровно arg_len байт из arg_copy кладутся в слот, и
     * точка входа получает указатель на КОПИЮ, а не на arg_copy. */
    const void *arg_copy;
    size_t      arg_len;
} plat_task_attr_t;

/* Значения по умолчанию: всё нулями — валидная конфигурация. */
void plat_task_attr_init(plat_task_attr_t *a);

/* Проверка атрибутов без порождения. 0 — годны; -1 и errno:
 *   EINVAL  размер стека вне диапазона или не кратен PLAT_TASK_STACK_GRAIN,
 *           guard_pages < 0, arg_len > PLAT_TASK_ARG_INLINE. */
int plat_task_attr_check(const plat_task_attr_t *a);

/* Порождение с атрибутами. Все отказы происходят ДО pthread_create:
 *   EINVAL     аргументы или атрибуты не годятся
 *   EAGAIN     таблица задач заполнена
 *   ENOSPC     превышен лимит профиля (A1-P04-167) — отдельный код, потому
 *              что «политика запрещает» чинится не тем же, чем «мест нет»
 *   EPERM      владелец запечатан остановкой (A1-P04-181)
 *   EOVERFLOW  идентификаторы задач исчерпаны
 * Отличать их обязательно: первая сверхлимитная задача должна получить отказ
 * до создания потока, а не после. */
int plat_task_spawn_ex(plat_owner_t owner, const char *name,
                       plat_task_entry_t entry, void *arg,
                       const plat_task_attr_t *attr, plat_task_id_t *out);

/* ── Лимит профиля (A1-P04-167) ──────────────────────────────────────────
 *
 * Действующий лимит = min(PLAT_TASK_MAX, limits.max_tasks профиля), где ноль
 * в профиле означает «не задан» и лимитом не является. Возвращается всегда
 * применяемое число, а не то, что написано в профиле: если профиль разрешает
 * больше, чем помещается в таблице, применяется таблица, и диагностика должна
 * видеть именно её. */
size_t plat_task_limit(void);
size_t plat_task_live_total(void);

/* ── Барьер остановки (A1-P04-181) ───────────────────────────────────────
 *
 * После seal владелец не может породить новую задачу: spawn — EPERM. Ровно
 * тот же смысл, что у plat_res_owner_seal(): между «останавливаемся» и
 * «остановились» не должно быть окна, в которое проскакивает новая работа. */
int plat_task_owner_seal(plat_owner_t owner);
int plat_task_owner_sealed(plat_owner_t owner);
int plat_task_owner_unseal(plat_owner_t owner);   /* fixtures */

/* ── Снимок без лока таблицы (A1-P04-183) ────────────────────────────────
 *
 * Watchdog обязан читать heartbeat в тот момент, когда с задачей что-то не
 * так, — а «что-то не так» часто означает, что worker держит лок таблицы. Тот,
 * кто ждёт лок, чтобы узнать, не завис ли держатель лока, зависает вместе с
 * ним. Поэтому публикация идёт через seqlock на слот: писатель под g_mu,
 * читатель — без единой блокировки.
 *
 * Возвращает число заполненных строк, либо -1, если снимок не сошёлся за
 * отведённые попытки (таблица переписывалась всё это время). -1 — это
 * «измерение недоступно», а не «задач нет»: разница принципиальна для
 * evidence, и ноль вместо неё был бы ложным фактом. */
typedef struct plat_task_view {
    plat_task_id_t    id;
    plat_owner_t      owner;
    plat_task_state_t state;
    int64_t           beat_mono_s;
    int               heartbeat_sec;
    int               silent;        /* 1 = молчит дольше 2×heartbeat_sec */
    char              name[48];
} plat_task_view_t;

int plat_task_snapshot(const plat_owner_t *owner,
                       plat_task_view_t *out, size_t cap, size_t *n_out);

/* ── Допуск молчания из профиля (A1-P04-184) ─────────────────────────────
 *
 * «2× heartbeat_sec» было зашитым числом: его никто не утверждал, и изменить
 * его нельзя было, не пересобрав ядро. Теперь допуск берётся из
 * plat_watchdog_policy_t опубликованного профиля —
 * interval_ms × max_strikes, — а зашитое правило остаётся только там, где
 * профиля нет вовсе.
 *
 * Причина молчания названа отдельным кодом, а не выводится из нуля или
 * единицы. Разница содержательная: «задача не настроена стучать» и «задача
 * настроена и молчит» требуют разных действий, а «измерить не удалось» не
 * является ни тем, ни другим и не должно превращаться в «всё в порядке».
 *
 * Устаревший heartbeat НЕ трогает поколение владельца: молчание — повод
 * принять решение о восстановлении, а не само решение. Смена поколения
 * принадлежит lifecycle, и heartbeat её не инициирует. */
typedef enum plat_task_silence {
    PLAT_TASK_SIL_OK           = 0,  /* стучит вовремя                     */
    PLAT_TASK_SIL_NOT_ARMED    = 1,  /* heartbeat_sec == 0, стучать нечему */
    PLAT_TASK_SIL_SILENT       = 2,  /* настроена и молчит дольше допуска  */
    PLAT_TASK_SIL_UNMEASURABLE = 3,  /* снимок не сошёлся или часы отказали */
    PLAT_TASK_SIL_NO_TASKS     = 4   /* у владельца нет живых задач        */
} plat_task_silence_t;

const char *plat_task_silence_name(plat_task_silence_t s);

/* Действующий допуск молчания в СЕКУНДАХ для задачи с таким heartbeat_sec.
 * Профиль важнее зашитого правила; 0 означает «допуск не определён».
 *
 * Гранулярность намеренно посекундная: отметка heartbeat хранится и отдаётся
 * как time_t (plat_task_last_heartbeat), поэтому разность двух отметок точна
 * до ±1 с. Допуск меньше двух секунд задавать бессмысленно — он неотличим от
 * соседних значений, и watchdog на нём будет срабатывать через раз. Профиль,
 * запросивший меньше секунды, получает одну, а не ноль: обнулять допуск
 * значило бы считать молчащей каждую задачу. */
int plat_task_silence_limit_s(int heartbeat_sec);

/* Худший исход по всем живым задачам владельца. UNMEASURABLE важнее OK:
 * недоступное измерение не является подтверждением исправности. */
plat_task_silence_t plat_task_owner_silence(plat_owner_t owner);

/* Heartbeat одной задачи без лока. 0 — заполнено; -1 — не сошлось или нет
 * такой задачи (различить помогает plat_task_state()). */
int plat_task_beat_nolock(plat_task_id_t id, int64_t *out_mono_s);

/* ── Уборка с общим дедлайном (A1-P04-164/179/194) ───────────────────────
 *
 * plat_task_reap_owner(owner, timeout_ms) даёт КАЖДОЙ задаче свой timeout_ms;
 * при сорока задачах согласованный бюджет остановки молча умножается на сорок.
 * Здесь бюджет один на всю уборку и отсчитывается по CLOCK_MONOTONIC.
 *
 * Задача, не успевшая завершиться, остаётся живой и попадает в leftover —
 * слот не очищается, идентификатор не переиспользуется, успех над
 * незавершённым потоком не возвращается. */
typedef struct plat_task_reap_result {
    size_t found;
    size_t joined;
    size_t leftover;   /* остались после дедлайна */
    int    timed_out;
} plat_task_reap_result_t;

int plat_task_reap_owner_by(plat_owner_t owner, int budget_ms,
                            plat_task_reap_result_t *out);

/* Монотонные миллисекунды из того же источника, что и дедлайны выше. */
uint64_t plat_task_mono_ms(void);

/* ── Счётчики (A1-P04-175/182) ───────────────────────────────────────────
 *
 * finalized — сколько раз слот был очищен подтверждённым завершением. Это
 * счётчик exactly-once: гонка cancel и штатного выхода обязана дать ровно
 * одну финализацию на задачу, и fixture сверяет именно это число, а не
 * косвенный признак.
 *
 * heartbeats и completions разделены намеренно (A1-P04-182): heartbeat — это
 * «я ещё жив», completion — «меня больше нет». Один счётчик на оба смысла
 * позволял бы зависшей задаче, продолжающей стучать, выглядеть завершённой. */
typedef struct plat_task_counters {
    uint32_t reserved;
    uint32_t live;
    uint32_t cancel_requested;
    uint64_t spawned;
    uint64_t spawn_refused_limit;   /* лимит профиля            */
    uint64_t spawn_refused_full;    /* таблица                  */
    uint64_t spawn_refused_sealed;  /* барьер остановки         */
    uint64_t spawn_rollback;        /* pthread_create отказал   */
    uint64_t cancels;
    uint64_t finalized;             /* подтверждённые завершения */
    uint64_t join_timeouts;
    uint64_t join_busy;             /* второй joiner            */
    uint64_t heartbeats;
    uint64_t reap_timeouts;
} plat_task_counters_t;

void plat_task_counters_get(plat_task_counters_t *out);
53

trace

Трассировка операций, spans и корреляция
src/trace/Наблюдение и исследование6 файлов2 API headers

Trace связывает длительные операции spans и correlation: command, graph plan, lifecycle hook, XIO, child IPC, RA2C и module work. Он предназначен для причинной диагностики latency/failure, а не заменяет audit.

Граница ответственности

  • Span creation cheap/bounded; exporter optional.
  • No secret payload/addresses by default.
  • Tracing outage не меняет operation outcome, кроме explicit compliance profile.

Устройство подсистемы

  • Trace/span ids, parent, owner/generation, operation, start/end monotonic, status и bounded attributes.
  • Per-thread/task context propagation explicit; child/RA2C carries serialized parent token with auth/bounds.
  • Active span table bounded with timeout cleanup.

Поток работы

  • Intent creates root span/correlation.
  • Nested lifecycle/XIO/IPC spans.
  • End records status/error class.
  • Stats/dump/export.

Отказ и восстановление

  • Table full sampling/drop counter, critical audit unaffected.
  • Invalid remote parent creates new root with link, no spoof trusted actor.
  • Clock monotonic durations.

Основные возможности

  • Creates traces and nested spans.
  • Correlates module operations with audit/events.
  • Stats/dump aid appliance and server diagnostics.
Архитектурные детали и инварианты

Overview

128-bit trace-ids + 64-bit span-ids, propagated via thread-local context.

Spans are pushed to a lock-free MPSC ring and drained by trace_tick().

Components

- trace.c — core: thread-local context, ring, store

- trace_span.c — span generation and lifecycle

- trace_propagate.c — capture/restore for async boundaries; HTTP header encode/decode

- trace_export.c — binary + JSON-Lines export

Integration

Audit events automatically gain trace_id/span_id fields when a span is

open (ТР-5.6.3). Use TRACE_SCOPE(comp, op) for the common synchronous case.

Управление и диагностика

Корневые команды: trace. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / trace →
Состав подсистемы / 6 файлов
Файл / компонентНазначение и граница
src/trace/cmd_trace.cCLI namespace "trace" + subsystem registration (Phase 6). trace start [--comp=NAME] открыть корневой спан на этом потоке trace span [--comp=NAME] открыть дочерний спан trace end закрыть текущий спан trace show сводка по трейсу
src/trace/trace.cAny thread may open/close spans, so the ring is MPSC, not SPSC: producers claim a slot with a CAS on `head` and publish it with a per-slot sequence counter (Vyukov bounded queue). A producer never blocks and never waits on the consumer — a full ring drops the span and bumps a counter instead
src/trace/trace.hEach logical operation gets a 128-bit trace-id. Within that operation every sub-step is a span (64-bit span-id + parent span-id). Both live in a thread-local context (__thread), so they propagate automatically across all subsystem calls on the same thread without touching any function signature
src/trace/trace_export.cРеализация trace / export
src/trace/trace_propagate.cРеализация trace / propagate
src/trace/trace_span.cDistributed trace span generation and lifecycle. STAB-134: span_gen, stale_span, recovery_span, parent_span.
Контракты API / 2 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/trace.h
/* trace.h — cross-cutting trace/span-id infrastructure (Phase 6, ТР-5.6.1–5.6.4)
 *
 * Design
 * ──────
 * Each logical operation gets a 128-bit trace-id.  Within that operation every
 * sub-step is a span (64-bit span-id + parent span-id).  Both live in a
 * thread-local context (__thread), so they propagate automatically across all
 * subsystem calls on the same thread without touching any function signature
 * (ТР-5.6.1, ТР-5.6.2).
 *
 * Open spans are held in a per-thread STACK, so a parent span keeps its own
 * start time while children open and close beneath it — parent durations
 * correctly cover the whole nested operation.  Depth beyond TRACE_DEPTH_MAX is
 * still counted (push/pop stay balanced) but those frames are not recorded.
 *
 * Closing a span pushes it into a lock-free MPSC ring — any thread may push
 * without blocking (ТР-5.6.4).  The ring is drained by trace_tick() from the
 * subsystem's background tick, which moves spans into a queryable store.
 *
 * Integration with audit (ТР-5.6.3)
 * ──────────────────────────────────
 * audit_write() calls trace_current() internally.  When a span is open the
 * JSON line gains two fields:  "trace_id":"<32hex>","span_id":"<16hex>"
 * The audit_write() signature is unchanged.
 *
 * Async boundaries (ТР-5.6.2)
 * ────────────────────────────
 * Before handing work to another thread, capture the context with
 * trace_capture(&ctx) and restore it on the worker with trace_restore(&ctx).
 * TRACE_SCOPE(comp, op) covers the common synchronous case via
 * __attribute__((cleanup)).
 */
/* ── ID types ────────────────────────────────────────────────────────────── */

#define TRACE_ID_BYTES   16          /* 128-bit trace identifier              */
#define SPAN_ID_BYTES     8          /* 64-bit span identifier                */

#define TRACE_ID_HEXLEN  33          /* 32 hex chars + NUL                    */
#define SPAN_ID_HEXLEN   17          /* 16 hex chars + NUL                    */

typedef struct { uint8_t b[TRACE_ID_BYTES]; } trace_id_t;
typedef struct { uint8_t b[SPAN_ID_BYTES];  } span_id_t;

extern const trace_id_t TRACE_ID_ZERO;
extern const span_id_t  SPAN_ID_ZERO;

int trace_id_is_zero(const trace_id_t *id); /* inline interface */
int span_id_is_zero(const span_id_t *id); /* inline interface */

/* Hex formatting / parsing (caller-supplied buffers). */
void trace_id_hex(const trace_id_t *id, char out[TRACE_ID_HEXLEN]);
void span_id_hex (const span_id_t  *id, char out[SPAN_ID_HEXLEN]);
int  trace_id_parse(const char *hex, trace_id_t *out);   /* 0 = ok */
int  span_id_parse (const char *hex, span_id_t  *out);

/* ── Labels ──────────────────────────────────────────────────────────────── */
#define TRACE_OP_MAX     64          /* operation label                       */
#define TRACE_COMP_MAX   32          /* component / subsystem name            */
#define TRACE_DEPTH_MAX  16          /* max recorded nesting depth per thread */

/* ── Finished span (what the store holds) ────────────────────────────────── */
typedef struct {
    trace_id_t trace_id;
    span_id_t  span_id;
    span_id_t  parent_id;           /* SPAN_ID_ZERO for the root span         */
    char       op  [TRACE_OP_MAX];
    char       comp[TRACE_COMP_MAX];
    int64_t    start_ns;            /* CLOCK_MONOTONIC                        */
    int64_t    end_ns;
    int64_t    duration_ns;
    int        depth;               /* nesting level, 0 = root                */
} trace_span_t;

/* ── Open span frame (one entry of the per-thread stack) ─────────────────── */
typedef struct {
    span_id_t  span_id;
    span_id_t  parent_id;
    char       op  [TRACE_OP_MAX];
    char       comp[TRACE_COMP_MAX];
    int64_t    start_ns;
} trace_frame_t;

/* ── Thread-local context ────────────────────────────────────────────────── */
typedef struct {
    trace_id_t    trace_id;
    trace_frame_t stack[TRACE_DEPTH_MAX];
    int           depth;            /* logical depth; may exceed DEPTH_MAX    */
    uint32_t      overflow;         /* frames not recorded due to depth limit */
} trace_ctx_t;

/* A context is active when at least one span is open. */
int trace_ctx_active(const trace_ctx_t *c); /* inline interface */

/* Innermost open frame, or NULL when no span is open. */
const trace_frame_t *trace_ctx_top(const trace_ctx_t *c); /* inline interface */

/* ── Lifecycle ───────────────────────────────────────────────────────────── */
int  trace_init(void);
void trace_fini(void);

/* Сколько раз дверь trace отказала входу в саму себя.
 *
 * Trace записывает I/O, выполняя I/O. Как только что-нибудь начнёт наблюдать
 * XIO-операции и докладывать через trace, вторая половина этой фразы станет
 * первой половиной следующей: op -> hook -> trace write -> op. Дверь отказывает
 * такому входу с ELOOP, а не пропускает молча: исчезнувшая запись оставила бы
 * картину, утверждающую операцию, которую никто не записал, и оператор не
 * отличил бы её от операции, которой не было.
 *
 * Счётчик, а не флаг: защита, о которой нельзя спросить, неотличима от защиты,
 * которая ни разу не сработала. Суммарно по всем потокам. */
uint64_t trace_io_reentry_refused(void);

/* ── Per-thread context API ──────────────────────────────────────────────── */

/* Open a ROOT span: fresh trace-id + span-id, resets this thread's stack.
 * Returns the new trace-id so the caller can log or forward it. */
trace_id_t trace_start(const char *comp, const char *op);

/* Open a CHILD span under the current one (auto-starts a root if none open).
 * Returns the new span-id (SPAN_ID_ZERO if the depth limit was exceeded). */
span_id_t trace_span(const char *comp, const char *op);

/* Close the innermost open span and push it to the ring. */
void trace_end(void);

/* Copy this thread's context out / restore it on another thread (ТР-5.6.2). */
void trace_capture(trace_ctx_t *out);
void trace_restore(const trace_ctx_t *ctx);

/* Read-only view of this thread's context; NULL when no span is open.
 * Used by audit_write().  Do not store the pointer. */
const trace_ctx_t *trace_current(void);

/* Drop this thread's context entirely (unbalanced spans are discarded). */
void trace_clear(void);

/* ── TRACE_SCOPE ─────────────────────────────────────────────────────────── */
/* Opens a child span at entry, closes it at scope exit.
 *
 *   void handle_request(void) {
 *       TRACE_SCOPE("transport", "handle_request");
 *       ...
 *   }
 */
void _trace_scope_cleanup(const int *guard); /* inline interface */

#define _TRACE_CAT2(a, b) a ## b
#define _TRACE_CAT(a, b)  _TRACE_CAT2(a, b)

#define TRACE_SCOPE(comp, op)                                       \
    const int _TRACE_CAT(_trace_scope_guard_, __LINE__)             \
        __attribute__((cleanup(_trace_scope_cleanup), unused)) =    \
        (trace_span((comp), (op)), 0)

/* ── Store query API ─────────────────────────────────────────────────────── */
#define TRACE_STORE_MAX  4096        /* spans kept for querying (circular)    */

/* Copy up to max spans of one trace into out[], ordered by start_ns.
 * Returns the number copied. */
int trace_get_spans(const trace_id_t *id, trace_span_t *out, int max);

/* Distinct trace-ids currently in the store (most recent first). */
int trace_list_ids(trace_id_t *out, int max);

typedef struct {
    uint64_t spans_opened;    /* spans started                              */
    uint64_t spans_pushed;    /* spans closed and pushed to the ring         */
    uint64_t spans_drained;   /* spans moved ring → store                    */
    uint64_t spans_dropped;   /* lost: ring was full                         */
    uint64_t spans_evicted;   /* overwritten in the store (circular wrap)    */
    uint64_t depth_overflow;  /* frames skipped: nesting deeper than the max */
    uint32_t ring_used;
    uint32_t store_used;
} trace_stats_t;

void trace_get_stats(trace_stats_t *out);

/* Drain the ring into the store.  Safe to call from any thread: concurrent
 * callers are serialised, and a caller that finds a drain already running
 * returns immediately. */
void trace_tick(void);
include/platx/trace_xio.h
/* platx/trace_xio.h — Trace spans for XIO/RA2C/DSL/MSX (INT-118). */
/* Open a trace span for an XIO operation, capturing corr_id. */
void plat_trace_xio_begin(const char *comp, const char *op);
void plat_trace_xio_end(void);

/* Capture current trace context before async handoff.  0/-1. */
int  plat_trace_async_capture(trace_ctx_t *out);

/* Restore trace context on worker thread.  0/-1. */
int  plat_trace_async_restore(const trace_ctx_t *ctx);

/* RAII span macro for XIO/RA2C layers. */
#define PLAT_TRACE_XIO_SCOPE(comp, op) TRACE_SCOPE((comp), (op))
54

transports

Общий реестр транспортов, адаптеров и daemon-интерфейсов
src/transports/Связь и ввод-вывод21 файлов2 API headers

src/transports предоставляет generic adapter registry/config/daemon и implementations DNS, HTTP, IPC, TLS, WebSocket и network backends. Он реализует transport.core resolver и legacy adapter aliases, но не содержит RA2C protocol state.

Граница ответственности

  • Adapter identity/version/lifecycle owner-scoped; registry постепенно converges с capability/descriptor.
  • TLS/keyring secrets через services; adapters не читают global secret files сами.
  • Server/listener функции отсутствуют в client-only agent composition.

Устройство подсистемы

  • Transport adapter descriptor объявляет schemes/features, config schema, connect/listen/send/recv/close/status ops и requirements.
  • Registry publishes transport.core resolver; per-adapter legacy names имеют deprecation.
  • Connection/listener handles owner/generation and XIO resources.
  • DNS cache bounded TTL/negative entries; daemon/config reload atomic generation.

Поток работы

  • Consumer resolve scheme/features.
  • Validate config/secret refs → adapter connect/listen.
  • XIO data operations → status/metrics.
  • Close/reload/loss → revoke handles/resources.

Отказ и восстановление

  • Unknown scheme/feature explicit unsupported.
  • DNS/TLS/connect timeout typed and recovery caller decides retry.
  • Config reload failure keeps old generation.
  • Adapter unload with live handles denied/quiesced.

Основные возможности

  • Adapter registry and configuration/daemon lifecycle.
  • DNS cache, HTTP, IPC, TLS and WebSocket helpers.
  • Keyring integration and service resolution.
  • Backend/stack/server operational controls.

Управление и диагностика

Корневые команды: transport. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / transports →
Состав подсистемы / 21 файлов
Файл / компонентНазначение и граница
src/transports/cmd_transport.cnamespace "transport": управление транспортным слоем. transport list список транспортов и их режимы transport status [name] подробный статус transport start|stop [name] управление transport mode переключение на лету
src/transports/transport.cреестр транспортов, конфигурация и маршрутизация по режиму. Здесь живёт единственная копия логики «какой режим — куда вызывать». Адаптеры (transport_icmp.c и др.) реализуют только LOCAL-путь.
src/transports/transport.hединый транспортный интерфейс платформы platx. Раньше каждый сетевой модуль (cmd_icmp.c, cmd_dns.c, cmd_tcp.c, …) сам резолвил netstack_if_t и умел работать только in-process: transport_sys_register(); const netstack_if_t *ns = netstack_resolve(name ? name : "sys");
src/transports/transport_api.hUnified transport backend API. All backends (TCP, TLS, UNIX, loopback, covert channels) implement this vtable. The caller never calls OS sockets directly — always goes through the vtable.
src/transports/transport_backend.cреестр backend-ов transport_api.h (vtable API). transport_register/transport_find/transport_list, но НИ ОДНОЙ реализации только transport_register(transport_t*) из transport.c, это другой API с другой сигнатурой (A3-W1-F012). Поэтому t_transport_unix.c не собирался
src/transports/transport_cmd.cреализация шима для cmd_*.c (см. transport_cmd.h).
src/transports/transport_cmd.hобщий шим для перевода cmd_*.c на транспортный слой. Команды исторически резолвили стек напрямую: transport_sys_register(); const netstack_if_t *ns = netstack_resolve(name ? name : "sys"); ns->ping(ns->stk, ip, timeout, &rtt); Из-за этого они умели только LOCAL-режим. tcmd_get() возвращает
src/transports/transport_daemon.cDAEMON-режим: транспорты в отдельном процессе. Отличие от режима ipc — только во владельце сокета. Протокол тот же (transport_ipc.c), поэтому клиентский код не меняется вовсе: он просто подключается к сокету, который держит не этот процесс, а порождённый.
src/transports/transport_dnscache.cкэш DNS-ответов поверх keyring_store. Почему именно keyring, а не своя хэш-таблица: keyring_store уже умеет TTL и сам вычищает протухшее (keyring_store_gc). Значит, самая тонкая часть кэша — инвалидация — достаётся бесплатно и одинаково ведёт себя для kernel- и memory-бэкенда. Плюс кэш виден остальной платформе
src/transports/transport_httpd.cHTTP-сервер транспортного слоя. Поверх тех же TCP-операций transport_if_t, что и остальное: сервер одинаково поднимается на сокетах ядра и на собственном стеке txpstack. Маршруты регистрируются по префиксу пути; при совпадении нескольких выигрывает самый длинный префикс — так «/api/v2» перекрывает «/api»,
src/transports/transport_ipc.cIPC-канал транспортов поверх Unix-сокета. Протокол: один запрос — один ответ, соединение закрывается. [transport_ipc_hdr_t][payload] Заголовок фиксированного размера, порядок байт — родной (обе стороны на одной машине; сокет Unix-домена по определению локальный).
src/transports/transport_keyring.cтранспорт поверх keyring_store. Зачем это вообще нужно: keyring уже даёт нам именованное хранилище с TTL, доступное всем подсистемам платформы и переживающее отдельные процессы (при kernel-backend). Значит, его можно использовать как почтовый ящик — канал запрос/ответ там, где сокеты недоступны (жёсткая песочница seccomp
src/transports/transport_loop.cLoopback transport backend for tests. Implements transport_vtable_t using a pair of in-process pipes. Every connection pair uses pre-allocated slots.
src/transports/transport_proto.cадаптеры протоколов над netstack_if_t. Все пять транспортов (icmp/dns/tcp/udp/http) построены на одном базовом состоянии: они резолвят netstack_if_t по имени бэкенда ("sys" — сокеты ядра, "platx" — собственный стек txpstack) и вызывают его vtable. Здесь реализован ТОЛЬКО LOCAL-путь. Режимы DAEMON/IPC/KEYRING работают
src/transports/transport_sys.cреализация netstack_if_t поверх системных сокетов Linux. Все дескрипторы — обычные fd ядра. stk = NULL (контекст не нужен). Регистрируется в platform-реестре под ключом "netstack:sys".
src/transports/transport_sys.hреализация netstack_if_t поверх системных сокетов Linux. Регистрирует интерфейс под именем "netstack:sys" в реестре платформы. Модули вызывают netstack_resolve("sys") и получают тот же vtable, что и у txpstack, но работающий с обычными fd ядра. - tcp_listen / tcp_accept / tcp_connect / tcp_send / tcp_recv / tcp_close
src/transports/transport_sys_io.htransport_sys.c's one door for typed I/O. Private to this TU. In a header so a door test can include the choke point without linking the netstack vtable, ping path and registry. socket/bind/listen/poll/setsockopt are not among the eight and stay on the vtable where they were.
src/transports/transport_tls.cPTLS: защищённый канал транспортного слоя. ═══ ЧТО ЭТО И ЧЕМ НЕ ЯВЛЯЕТСЯ ═══════════════════════════════════════════ В crypto.c платформы есть только симметричная криптография: SHA-256, HMAC-SHA256, HKDF, ChaCha20-Poly1305, XChaCha20-Poly1305, AES-256-GCM,
src/transports/transport_tls13.cПочему отдельный файл, а не правка transport_tls.c: это PTLS, он сам в шапке честно пишет, что TLS не является: нет асимметрики, нет X.509, НЕТ FORWARD SECRECY, ветки Он живёт по API transport_t из transport.h и стоит в поставке. Здесь — backend по transport_api.h (vtable), собирается только с
src/transports/transport_unix.cUNIX domain socket backend (transport_api.h vtable). - loop игнорирует timeout_ms во всех трёх местах (connect/accept/recv): код возврата TRANSPORT_TIMEOUT из transport_api.h он не может вернуть никогда. Здесь дедлайн реальный, через poll(), и TRANSPORT_TIMEOUT
src/transports/transport_ws.cWebSocket (RFC 6455) и HTTP-сервер транспортного слоя. Оба построены поверх TCP-операций transport_if_t, поэтому одинаково работают и на сокетах ядра, и на собственном стеке txpstack, и — что важнее — через любой из четырёх режимов транспорта: маршрутизация уже
Контракты API / 2 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/services/transport_v1.h
/* platx/services/transport_v1.h — logical name: transport.core / transport.* */
#define PLAT_CAP_TRANSPORT  PLAT_NAME_TRANSPORT_CORE
#define PLAT_TRANSPORT_V1   0x00010000u

struct plat_transport_v1 {
    uint32_t struct_size;
    int (*resolve)(const char *name, const void **out_if);
};
include/platx/transport_cap.h
/* platx/transport_cap.h — Transport capability ABI (INT-101). */
#define PLAT_TRANSPORT_CAP_LOCAL   "transport.local:v1"
#define PLAT_TRANSPORT_CAP_DAEMON  "transport.daemon:v1"
#define PLAT_TRANSPORT_CAP_IPC     "transport.ipc:v1"
#define PLAT_TRANSPORT_CAP_REASON  128

typedef enum {
    PLAT_TXPCAP_OK     = 0,
    PLAT_TXPCAP_NOLINK = 1,
    PLAT_TXPCAP_DENIED = 2,
    PLAT_TXPCAP_ERROR  = 3,
} plat_txpcap_status_t;

typedef struct {
    plat_txpcap_status_t status;
    int   mode;   /* transport_mode_t cast */
    char  reason[PLAT_TRANSPORT_CAP_REASON];
} plat_txpcap_result_t;

/* Validate context has lease for the given transport mode cap.  0/-1. */
int plat_transport_cap_check(uint64_t ctx_id, uint32_t ctx_gen,
                             const char *cap_name,
                             plat_txpcap_result_t *out);

/* Provide cap into ABI (calls plat_abi_provide weak hook).  0/-1. */
int plat_transport_cap_provide(const char *cap_name);
55

trust

Доказательства, аппаратное доверие и ограниченные полномочия
src/trust/Доверие и защита27 файлов12 API headers

Trust объединяет проверку пакетов, внешний якорь истории, криптографические наборы, аттестацию, условия защиты и профили размещения. TPM, TEE и seL4 предоставляют разные свойства. Верификатор, издатель полномочий и потребитель ресурса сохраняют отдельную ответственность.

Граница ответственности

  • Trust объединяет проверку пакетов, внешний якорь истории, криптографические наборы, аттестацию, условия защиты и профили размещения. TPM, TEE и seL4 предоставляют разные свойства. Верификатор, издатель полномочий и потребитель ресурса сохраняют отдельную ответственность.

Устройство подсистемы

  • Верификатор бандла проверяет структуру и криптографическую связность материалов; внешний якорь фиксирует историю за пределами проверяемого узла.
  • Crypto suite описывает состав алгоритмов по слотам и согласование требований; наличие набора и допустимость профиля проверяются отдельно.
  • Capability optionality связывает builtin, profile, operator, site и signed слои конфигурации. Explain показывает, откуда пришло условие допуска.
  • TPM и HV backend предоставляют диагностический материал через собственные адаптеры. Self-report сохраняет свой уровень происхождения и не становится observed без внешней проверки.

Поток работы

  • Собрать материал с идентичностью, временем, nonce и контекстом операции.
  • Проверить контейнер, подписи, цепочку истории и применимую политику.
  • Сопоставить результат с обязательными механизмами выбранного профиля.
  • Выпустить ограниченное полномочие и проверить его на границе ресурса.

Отказ и восстановление

  • Неправильный размер, версия, подпись или несогласованная история останавливают проверку.
  • Потеря внешнего якоря и недостаточная свежесть имеют отдельный результат.
  • Наличие TPM либо HV не заменяет допустимую аттестацию и актуальные основания выдачи grant.

Управление и диагностика

Корневые команды: trust. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / trust →
Состав подсистемы / 27 файлов
Файл / компонентНазначение и граница
src/trust/cmd_trust.cадаптер CLI домена trust к консоли платформы. Логики нет: она в trust_cli.c и печатает в FILE*. Вывод собирается в поток в памяти и отдаётся консоли одним куском — иначе пришлось бы держать второй набор форматирования, и две реализации одной команды однажды разошлись бы молча (тот же довод, что в cmd_ndr.c).
src/trust/hv/hv_dev.cнизкоуровневый доступ к /dev/platx_hv. Класс: host (открывает устройство — значит, системный вызов; в core не идёт). Классификация: tests/trust/TU_CLASSES.tsv строка hv/hv_dev.c host /dev/platx_hv — char-устройство модуля platx_hv.ko. Протокол — ioctl с magic 'H': STATUS (nr=1), HEALTH (nr=7), ATTEST (nr=23).
src/trust/hv/hv_dev.hнизкоуровневый доступ к /dev/platx_hv. Объявления. Класс: host (открывает устройство — значит, системный вызов; в core не идёт). Классификация: tests/trust/TU_CLASSES.tsv строка hv/hv_dev.c host ЗАЧЕМ ОТДЕЛЬНЫЙ СЛОЙ У /dev/platx_hv два потребителя: описатель backend'а (trust_hv_backend.c,
src/trust/hv/trust_cli_hv.cподкоманды "hv" для trust CLI. Класс: host (открывает /dev/platx_hv через hv_dev.c). trust hv probe — есть ли platx_hv, режим, здоровье, пригодность trust hv status — полный STATUS: vCPU, vmexits, generation, build trust hv health — краткое здоровье + потерянные события + selftest
src/trust/hv/trust_cli_hv.hподкоманды "hv" для trust CLI. Объявление. Вызывается из trust_cli.c: if (!strcmp(sub, "hv")) return trust_cli_hv(rest_argc, rest_argv, out, err);
src/trust/hv/trust_hv_backend.ctrust_backend_v1_t для TRUST_MECH_HV_PLATX. ── C22 (HV trust backend) ───────────────────────────────────────────────── downstream consumer: trust_backend_bind(TRUST_MECH_HV_PLATX) → trust_cli "hv" (probe/quote через /dev/platx_hv ioctl) public capability : hv_harden_init() — открыть /dev/platx_hv и зарегистрировать
src/trust/hv/trust_hv_backend.hпубличный заголовок HV-полосы trust. downstream consumer: trust_backend_bind(TRUST_MECH_HV_PLATX) public capability : hv_harden_init() — инициализация при старте платформы rollback semantics : нет (только observation via ioctl — нет состояния)
src/trust/tpm/tpm2_dev.cнизкоуровневый доступ к /dev/tpm0. Класс: host (открывает файл — значит, системный вызов; в core не идёт). Классификация: tests/trust/TU_CLASSES.tsv строка tpm/tpm2_dev.c host /dev/tpm0 — char-устройство ядра (драйвер tpm_tis или tpm_crb). Протокол простой: write() отправляет полную TPM-команду (header + body),
src/trust/tpm/tpm2_dev.hнизкоуровневый доступ к /dev/tpm0. Объявления. Класс host: этот заголовок включается только в host-файлы домена trust. Ядро домена (core) никогда не включает его: оно не знает об устройстве.
src/trust/tpm/tpm2_wire.hПокрывает только те команды, которые нужны домену trust: TPM2_CC_PCR_Read — считать расширения PCR TPM2_CC_Quote — атестация с nonce: TPM подписывает набор PCR TPM2_CC_Create — создать запечатанный blob (seal) TPM2_CC_Load / Unseal — распечатать (unseal)
src/trust/tpm/tpm_backend.ctrust_backend_v1_t для механизма TRUST_MECH_TPM20. ── Объявление по C22 (иначе SPEC_ONLY) ──────────────────────────────── downstream consumer: trust_backend_bind() → trust_cli "capab" / "protection" public capability : TPM_BACKEND (экспортируется в tpm_backend.h)
src/trust/tpm/tpm_backend.hПубличная граница: tpm_harden_init() вызывается платформой при старте. Всё остальное — детали реализации, закрытые в tpm_backend.c.
src/trust/tpm/trust_cli_tpm.cподкоманды "tpm" для trust CLI. Класс: host (открывает /dev/tpm0 через tpm2_dev.c). trust tpm pcr [0-7] — вывести PCR-значения (SHA-256, банк 0) trust tpm quote NONCE_HEX — синтетический quote PCR 0-7 с nonce trust tpm seal FILE — (stub) запечатать содержимое файла
src/trust/tpm/trust_cli_tpm.hподкоманды "tpm" для trust CLI. Объявление. Вызывается из trust_cli.c: if (!strcmp(sub, "tpm")) return trust_cli_tpm(rest_argc, rest_argv, out, err);
src/trust/trust_anchor.cсо-подписанты корня и наблюдение за якорем. Здесь решается один вопрос: сколько РАЗНЫХ сторон подтвердили корень. Не сколько подписей — подписей можно наделать сколько угодно, если ключи лежат на одной машине и выданы одним удостоверяющим центром. Такой «кворум» состоит из одного мнения, повторённого N раз, и ловит
src/trust/trust_attest.cизмеряемая загрузка, аттестованный допуск и свойства. Порядок проверок в trust_attest_admit выбран не по удобству, а по цене ошибки. Сначала — то, отсутствие чего делает остальное бессмысленным (подпись, backend, привязка к challenge), и только потом свежесть и
src/trust/trust_backend.cисполнительный шов backend'а seL4/TEE. ИНВАРИАНТЫ (каждый проверяется мутацией в t_te_backend.c): INV-TBK-01 привязка к механизму, которого нет в этой сборке, отвергается INV-TBK-02 без backend'а операция отказывает ДО эффекта: выходной буфер не трогается, *out_len = 0
src/trust/trust_bundle.cтри раздельные проверки evidence-бандла (E3-ANCH-04/05). Раздельность — не стилистика. Слитая проверка «бандл цел» отвечает одним битом на три разных вопроса, и первый же зелёный ответ закрывает два оставшихся. Здесь каждая проверка возвращает свой результат, а
src/trust/trust_capab.cмеханизм защиты как настраиваемая опция с замком. Реализация include/platx/trust_capab.h. ТЗ: TZ_PLATX_TRUSTED_EXECUTION_V1 (TE-STAT-01…04, TE-SEC-04, TE-CON-01, TE-PLACE-01), решение владельца Два инварианта держат весь файл, и оба проверяются мутацией в
src/trust/trust_cli.chost-часть домена trust: файлы, каталоги, печать. Здесь и только здесь домен трогает внешний мир. Ядро (merkle, anchor, bundle, suite, attest, operator, zeroize) работает над структурами в памяти и не делает ни одного системного вызова — это проверяемое свойство, а не соглашение (tests/trust/t_trust_no_effects.sh).
src/trust/trust_host_dir_posix.cPOSIX-реализация шва каталогов (trust_host_dir.h). Класс host (tests/trust/TU_CLASSES.tsv): системные вызовы здесь ОЖИДАЮТСЯ, и это единственное место в домене, где они есть, помимо stdio в trust_cli.c. Windows-двойник — platx-windows/win/trustdom/ Поведение ПОБИТОВО повторяет то, что trust_cli.c делал до введения шва:
src/trust/trust_merkle.cдерево Меркла журнала прозрачности (E3-ANCH-01/02). Реализация по RFC 6962 (Certificate Transparency) и алгоритмам проверки дерево»: у чужого проверяющего должна быть возможность проверить наш корень известным ему алгоритмом, не читая наш исходник. Два префикса — 0x00 для листа и 0x01 для узла — не украшение. Без них
src/trust/trust_operator.cдопуск оператора (E3-AUTH). Единственное место, где здесь легко ошибиться, — видимый ответ. Соблазн написать "ограниченный режим" в DURESS-ветке огромен: так честнее по отношению к интерфейсу. И так же принуждающий, стоящий за спиной, за одну секунду узнаёт, что оператор его обманул.
src/trust/trust_suite.cреестр криптографических наборов (E3-CRY). Здесь нет ни одного вызова криптографии. Это намеренно: файл решает, ЧТО должно быть применено и можно ли это применить, а не применяет. Такой раздел позволяет проверить политику согласования тестом, который не требует ни ключей, ни сети, ни аппаратуры.
src/trust/trust_witness.cось наблюдения (E3-WIT). Весь файл держится на одной асимметрии: понизить уровень наблюдения можно, повысить — нельзя. Все проверки здесь работают в одну сторону, и ни одна не возвращает уровень выше заявленного. Это единственная защита от того, чтобы «независимый наблюдатель» получался из гостевой
src/trust/trust_zeroize.cпроверяемый zeroize: скан остатков, sealing, ложное срабатывание (E3-ZERO-01…03). Про честность результата. Скан, прерванный на середине, не даёт «ноль попаданий» — он даёт complete=0, и это разные утверждения. В этом дереве уже находили обратное на другом слое: SKIP, возвращающий 0, и
src/trust/trustctl_main.cавтономная утилита домена trust. Отдельный бинарь, а не только команда консоли: проверять бандл нужно и там, где платформы нет вовсе — на машине приёмщика, в CI чужой стороны, на носителе с одним каталогом evidence. Верификатор, который требует установить проверяемую систему, доверия не добавляет.
Контракты API / 12 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/platx_crypto.h
/* platx_crypto.h — crypto primitives public API (INV-CRYPTO-01) */

/* INV-CRYPTO-01: all functions require explicit output_len parameter */

/* SHA-256 */
typedef struct { uint32_t s[8]; uint64_t n; uint8_t b[64]; } platx_sha256_t;
void platx_sha256_init(platx_sha256_t *c);
void platx_sha256_update(platx_sha256_t *c, const uint8_t *d, size_t n);
void platx_sha256_final(platx_sha256_t *c, uint8_t *out, size_t out_len);
int  platx_sha256(const uint8_t *data, size_t dlen, uint8_t *out, size_t out_len);

/* ChaCha20-Poly1305 AEAD */
int chacha20poly1305_encrypt(const uint8_t key[32], const uint8_t nonce[12],
                             const uint8_t *aad, size_t aad_len,
                             const uint8_t *plain, size_t plain_len,
                             uint8_t *cipher, size_t cipher_cap, size_t *cipher_len,
                             uint8_t tag[16]);
int chacha20poly1305_decrypt(const uint8_t key[32], const uint8_t nonce[12],
                             const uint8_t *aad, size_t aad_len,
                             const uint8_t *cipher, size_t cipher_len,
                             uint8_t *plain, size_t plain_cap, size_t *plain_len,
                             const uint8_t tag[16]);

/* Ed25519 (for MSX §MIL-10) */
int platx_ed25519_keygen(const uint8_t seed[32],
                         uint8_t priv[64], size_t priv_len,
                         uint8_t pub[32],  size_t pub_len);
int platx_ed25519_sign(const uint8_t priv[64],
                       const uint8_t *msg, size_t mlen,
                       uint8_t sig[64], size_t sig_len);
int platx_ed25519_verify(const uint8_t pub[32],
                         const uint8_t *msg, size_t mlen,
                         const uint8_t sig[64], size_t sig_len);

/* X25519 (for RA2C key exchange) */
int platx_x25519(const uint8_t scalar[32], const uint8_t point[32],
                 uint8_t out[32], size_t out_len);
int platx_x25519_keygen(const uint8_t seed[32],
                        uint8_t priv[32], size_t priv_len,
                        uint8_t pub[32],  size_t pub_len);
include/platx/trust_anchor.h
/* platx/trust_anchor.h — E3-ANCH: внешний якорь и прозрачный журнал.
 *
 * ТЗ TZ_PLATX_SEL4_TEE_V2.md §13. Требования E3-ANCH-01…06.
 *
 * Зачем это вообще нужно. Сценарий CUT-E: согласованный инсайдер меняет
 * вердикт внутри бандла, пересчитывает все свёртки, перепечатывает
 * MANIFEST и заново ставит печать. Изнутри бандл безупречен — потому что
 * проверяющий и подделыватель пользуются одним и тем же основанием.
 * Ловит такое ровно одно: утверждение было опубликовано РАНЬШЕ и вне
 * досягаемости того, кто его правит.
 *
 * Отсюда конструкция:
 *
 *   • Журнал только дописывается, и его состояние сводится к корню
 *     дерева Меркла (RFC 6962). Корень — короткий, его можно опубликовать
 *     и подписать чужими ключами.
 *
 *   • Доказательство включения и доказательство согласованности
 *     проверяются БЕЗ доступа к журналу (E3-ANCH-02). Поэтому функции
 *     trust_verify_* не принимают trust_log_t: проверяющий, которому для
 *     проверки нужен сам журнал, проверяет журнал, а не факт.
 *
 *   • Корень подписывают со-подписанты, не подчинённые выпускающей
 *     стороне (E3-ANCH-03). Независимость здесь — не декларация, а
 *     проверяемое свойство ключа: другой узел, другая организация, другой
 *     домен полномочий. Два ключа на одной машине — один голос.
 *
 *   • Возврат старого корня — событие «откат» (E3-ANCH-06), а не
 *     «обновление до предыдущего состояния». Откат не воскрешает
 *     отозванные утверждения: эпоха отзыва не убывает никогда.
 *
 * Хэш и подпись сюда не встроены. SHA-256 задан определением дерева
 * (RFC 6962) и берётся из platx/platx_crypto.h; проверка подписи приходит
 * колбэком — у со-подписанта свой алгоритм из своего suite, и якорь не
 * вправе решать за него.
 *
 * ABI: TRUST_ANCHOR_ABI 1.
 */

#define TRUST_ANCHOR_ABI     1u
#define TRUST_HASH_LEN       32u
#define TRUST_LOG_CAP        4096u   /* листьев в журнале одной сессии */
#define TRUST_PROOF_MAX      64u     /* узлов в доказательстве */
#define TRUST_SUBJECT_MAX    64u
#define TRUST_SIGNER_NAME    32u
#define TRUST_DOMAIN_MAX     48u
#define TRUST_COSIGN_MAX     8u
#define TRUST_SIG_LEN        64u
#define TRUST_PUBKEY_LEN     32u
#define TRUST_ANCH_REASON    256u

typedef struct trust_hash {
    uint8_t b[TRUST_HASH_LEN];
} trust_hash_t;

/* ── Что вообще попадает в журнал (E3-ANCH-01) ───────────────────────
 * «Каждое утверждение, которое платформа предъявляет наружу». Класс
 * фиксирован перечислением: если утверждение не отнесено ни к одному
 * классу, оно не публикуется — и значит, не может быть предъявлено. */
typedef enum trust_claim_kind {
    TRUST_CLAIM_NONE          = 0,
    TRUST_CLAIM_RELEASE       = 1,  /* выпуск */
    TRUST_CLAIM_BUNDLE        = 2,  /* evidence-бандл */
    TRUST_CLAIM_RECEIPT       = 3,  /* receipt действия */
    TRUST_CLAIM_QUALIFICATION = 4,  /* вердикт квалификации */
    TRUST_CLAIM_REVOCATION    = 5,  /* отзыв ранее опубликованного */
    TRUST_CLAIM_KIND_MAX      = 6
} trust_claim_kind_t;

/* Запись журнала. Сериализуется канонически (см. trust_entry_encode):
 * никакого JSON и никакого «как получилось» — иначе один и тот же факт
 * даст два разных листа, и доказательство включения станет лотереей. */
typedef struct trust_log_entry {
    uint32_t     kind;                        /* trust_claim_kind_t */
    uint64_t     time_s;                      /* когда утверждение сделано */
    char         subject[TRUST_SUBJECT_MAX];  /* о чём утверждение */
    trust_hash_t payload;                     /* свёртка содержимого */
    uint64_t     epoch;                       /* эпоха полномочий автора */
} trust_log_entry_t;

/* ── Журнал ──────────────────────────────────────────────────────────── */
typedef struct trust_log {
    uint32_t     abi_version;
    size_t       n;                       /* сколько листьев */
    trust_hash_t leaf[TRUST_LOG_CAP];
} trust_log_t;

/* Доказательство — упорядоченный список узлов. Один тип на оба вида
 * доказательств: их различает не форма, а функция проверки. */
typedef struct trust_proof {
    uint32_t     n;
    trust_hash_t node[TRUST_PROOF_MAX];
} trust_proof_t;

typedef enum trust_anch_rc {
    TRUST_ANCH_OK            = 0,
    TRUST_ANCH_EINVAL        = 1,
    TRUST_ANCH_FULL          = 2,
    TRUST_ANCH_NOT_INCLUDED  = 3,  /* доказательство не сходится с корнем */
    TRUST_ANCH_INCONSISTENT  = 4,  /* новый корень не продолжает старый */
    TRUST_ANCH_ROLLBACK      = 5,  /* предъявлен корень меньшего размера */
    TRUST_ANCH_FORK          = 6,  /* тот же размер, другой корень */
    TRUST_ANCH_NO_QUORUM     = 7,  /* независимых со-подписантов не хватает */
    TRUST_ANCH_UNREACHABLE   = 8,  /* обязательный со-подписант недоступен */
    TRUST_ANCH_BAD_SIG       = 9,
    TRUST_ANCH_RC_MAX        = 10
} trust_anch_rc_t;

const char *trust_anch_rc_name(trust_anch_rc_t rc);
const char *trust_claim_kind_name(uint32_t k);

/* ── Листья и дерево (RFC 6962) ─────────────────────────────────────── */

/* Канонический байтовый вид записи. Возвращает длину или -1.
 * out_len должен быть не меньше TRUST_ENTRY_ENC_LEN. */
#define TRUST_ENTRY_ENC_LEN (4u + 8u + 8u + TRUST_SUBJECT_MAX + TRUST_HASH_LEN)
int trust_entry_encode(const trust_log_entry_t *e, uint8_t *out, size_t out_len);

/* Хэш листа: SHA-256(0x00 || данные). Префикс отличает лист от узла —
 * без него дерево допускает подстановку поддерева вместо листа. */
void trust_leaf_hash(const uint8_t *data, size_t len, trust_hash_t *out);
void trust_entry_leaf_hash(const trust_log_entry_t *e, trust_hash_t *out);

/* Хэш внутреннего узла: SHA-256(0x01 || left || right). */
void trust_node_hash(const trust_hash_t *l, const trust_hash_t *r,
                     trust_hash_t *out);

void trust_log_init(trust_log_t *log);
trust_anch_rc_t trust_log_append(trust_log_t *log, const trust_log_entry_t *e,
                                 size_t *index_out);
/* Добавление уже готового листа — для журнала, ведущегося чужой стороной. */
trust_anch_rc_t trust_log_append_leaf(trust_log_t *log, const trust_hash_t *leaf,
                                      size_t *index_out);

/* Корень дерева размера log->n. Пустой журнал даёт SHA-256("") — так
 * определено в RFC 6962, и это отличает «журнал пуст» от «корня нет». */
trust_anch_rc_t trust_log_root(const trust_log_t *log, trust_hash_t *out);

/* Корень префикса длиной m (m <= n). Нужен и для доказательств, и для
 * того, чтобы проверяющий мог убедиться в согласованности сам. */
trust_anch_rc_t trust_log_root_prefix(const trust_log_t *log, size_t m,
                                      trust_hash_t *out);

trust_anch_rc_t trust_log_inclusion_proof(const trust_log_t *log, size_t index,
                                          trust_proof_t *out);
trust_anch_rc_t trust_log_consistency_proof(const trust_log_t *log, size_t m,
                                            trust_proof_t *out);

/* ── Проверка без журнала (E3-ANCH-02) ──────────────────────────────── */
trust_anch_rc_t trust_verify_inclusion(const trust_hash_t *leaf,
                                       size_t index, size_t tree_size,
                                       const trust_proof_t *proof,
                                       const trust_hash_t *root);

trust_anch_rc_t trust_verify_consistency(const trust_hash_t *old_root, size_t m,
                                         const trust_hash_t *new_root, size_t n,
                                         const trust_proof_t *proof);

/* ── Со-подписанты (E3-ANCH-03) ─────────────────────────────────────── */

/* Кто такой подписант. Три поля независимости — не описание, а вход
 * проверки: совпадение любого из них делает голоса зависимыми. */
typedef struct trust_signer {
    uint32_t id;
    char     name[TRUST_SIGNER_NAME];
    uint8_t  pubkey[TRUST_PUBKEY_LEN];
    char     org[TRUST_DOMAIN_MAX];        /* организация */
    char     host[TRUST_DOMAIN_MAX];       /* узел исполнения */
    char     authority[TRUST_DOMAIN_MAX];  /* домен полномочий (кто выдал ключ) */
    uint8_t  reachable;                    /* доступен ли сейчас */
    uint8_t  required;                     /* обязателен по политике */
} trust_signer_t;

typedef struct trust_root_sig {
    uint32_t signer_id;
    uint8_t  sig[TRUST_SIG_LEN];
    uint8_t  present;
} trust_root_sig_t;

/* Подписанный корень — это то, что публикуется наружу. tree_size входит
 * в подписываемые байты: без него подпись корня не отличает состояние
 * из десяти записей от состояния из десяти тысяч. */
typedef struct trust_signed_root {
    trust_hash_t     root;
    uint64_t         tree_size;
    uint64_t         time_s;
    uint32_t         issuer_id;              /* выпускающая сторона */
    uint32_t         n_sigs;
    trust_root_sig_t sig[TRUST_COSIGN_MAX];
} trust_signed_root_t;

/* Канонические байты для подписи корня. */
#define TRUST_ROOT_SIGN_LEN (TRUST_HASH_LEN + 8u + 8u + 4u)
int trust_root_sign_bytes(const trust_signed_root_t *sr,
                          uint8_t *out, size_t out_len);

/* Проверка подписи приходит извне: у со-подписанта свой алгоритм.
 * Возвращает 1 при годной подписи, 0 иначе. */
typedef int (*trust_sig_verify_fn)(void *ctx,
                                   const uint8_t pubkey[TRUST_PUBKEY_LEN],
                                   const uint8_t *msg, size_t mlen,
                                   const uint8_t sig[TRUST_SIG_LEN]);

typedef struct trust_cosign_policy {
    uint32_t min_independent;      /* сколько независимых голосов нужно */
    uint8_t  require_offhost;      /* со-подписант обязан быть с другого узла */
    uint8_t  require_offauthority; /* и из другого домена полномочий */
    uint8_t  issuer_counts;        /* засчитывать ли подпись выпускающего */
} trust_cosign_policy_t;

typedef struct trust_cosign_result {
    trust_anch_rc_t rc;
    uint32_t n_valid;        /* годных подписей всего */
    uint32_t n_independent;  /* независимых голосов после схлопывания групп */
    uint32_t n_bad;
    uint32_t n_unreachable_required;
    char     reason[TRUST_ANCH_REASON];
} trust_cosign_result_t;

/* Считает независимые голоса под корнем и выносит вердикт.
 * Недоступный обязательный со-подписант — это TRUST_ANCH_UNREACHABLE,
 * а не «продолжаем с меньшим кворумом» (E3-T22). */
trust_anch_rc_t trust_cosign_verify_root(const trust_signed_root_t *sr,
                                         const trust_signer_t *signers,
                                         size_t n_signers,
                                         const trust_cosign_policy_t *pol,
                                         trust_sig_verify_fn verify, void *ctx,
                                         trust_cosign_result_t *out);

/* Независимы ли два подписанта между собой. Вынесено наружу: тот же
 * предикат нужен и при настройке набора со-подписантов, и в объяснении. */
int trust_signer_independent(const trust_signer_t *a, const trust_signer_t *b);

/* ── Состояние наблюдателя за якорем (E3-ANCH-06) ───────────────────── */
typedef struct trust_anchor_state {
    uint32_t     abi_version;
    trust_hash_t last_root;
    uint64_t     last_tree_size;
    uint64_t     last_time_s;
    /* Эпоха отзыва не убывает. Именно она делает откат бесполезным:
     * старый корень можно предъявить, но права, отозванные в более
     * поздней эпохе, обратно не включаются. */
    uint64_t     revocation_epoch;
    uint32_t     rollback_seen;
    uint32_t     fork_seen;
    uint8_t      initialized;
} trust_anchor_state_t;

void trust_anchor_state_init(trust_anchor_state_t *st);

/* Принять новый подписанный корень. proof — доказательство согласованности
 * от last_tree_size к sr->tree_size; при первом корне может быть NULL.
 * Состояние меняется только при TRUST_ANCH_OK; откат и форк
 * регистрируются счётчиком и не двигают корень. */
trust_anch_rc_t trust_anchor_update(trust_anchor_state_t *st,
                                    const trust_signed_root_t *sr,
                                    const trust_proof_t *proof,
                                    char *reason, size_t reason_len);

/* Отзыв поднимает эпоху. Возврат старого корня её не опускает. */
void trust_anchor_revoke_epoch(trust_anchor_state_t *st, uint64_t epoch);

/* Действительно ли утверждение, включённое в эпоху claim_epoch, всё ещё
 * в силе с точки зрения состояния якоря. */
int trust_anchor_claim_live(const trust_anchor_state_t *st, uint64_t claim_epoch);

/* Печать шестнадцатеричного хэша. out_len >= 2*TRUST_HASH_LEN+1. */
int trust_hash_hex(const trust_hash_t *h, char *out, size_t out_len);
int trust_hash_from_hex(const char *hex, trust_hash_t *out);
include/platx/trust_attest.h
/* platx/trust_attest.h — E3-ATT: измеряемая загрузка и аттестованный допуск,
 *                        E2-PROP: свойства защиты и деградация.
 *
 * ТЗ TZ_PLATX_SEL4_TEE_V2.md §7 и §15. Требования E3-ATT-01…04, E2-PROP-01…04.
 *
 * Разница, ради которой всё это существует: измерение — не принуждение.
 * Measured Boot записывает, что было загружено; Secure Boot отказывается
 * загружать неподписанное. Первое без второго даёт честный протокол
 * компрометации, второе без первого — необъяснимую блокировку. Поэтому
 * для профиля Central appliance требуются оба (E3-ATT-01), и это здесь
 * проверяемый предикат, а не строчка в описании профиля.
 *
 * Второе: допуск выдаётся ИЗМЕРЕННОМУ СОСТОЯНИЮ узла, а не узлу
 * (E3-ATT-02). Разница видна в момент, когда узел перезагрузился в другой
 * образ: узел тот же, состояние другое — и допуск обязан кончиться. Для
 * этого grant несёт слепок PCR, при котором был выдан, и сверяется с
 * текущим при каждом применении.
 *
 * Третье: класс эффекта задаёт свежесть (E3-ATT-03). Необратимое действие
 * по получасовому quote — это действие по состоянию, которого, возможно,
 * уже нет. Пороги разные по классам, и IRREVERSIBLE не может пользоваться
 * порогом REVERSIBLE.
 *
 * Четвёртое: отзыв аттестации аннулирует ВСЕ выданные по ней допуски
 * (E3-ATT-04) — с проверяемым следом, а не молча.
 *
 * И отдельно E2-PROP: три состояния свойства — degraded, unsupported,
 * unknown — интерпретируются ОДИНАКОВО: требуемое свойство не выполнено.
 * Иначе «неизвестно» тихо превращается в «наверное, да».
 */

#define TRUST_ATT_ABI        1u
#define TRUST_PCR_COUNT      24u
#define TRUST_PCR_SELECT_MAX 24u
#define TRUST_EVENTLOG_MAX   128u
#define TRUST_GRANT_MAX      32u
#define TRUST_ATT_DESC_MAX   64u
#define TRUST_ATT_REASON_MAX 256u
#define TRUST_TRAIL_MAX      64u

/* ── Профиль размещения (ТЗ §4) ──────────────────────────────────────── */
typedef enum trust_placement {
    TRUST_PLACE_HOSTED_DEV      = 0,  /* PLATX на обычной ОС */
    TRUST_PLACE_CENTRAL_APPLIANCE = 1,/* системный образ, seL4 как база */
    TRUST_PLACE_CONFIDENTIAL    = 2,  /* защищённые функции в TEE/VM */
    TRUST_PLACE_NODE_PROTECTED  = 3,
    TRUST_PLACE_MAX             = 4
} trust_placement_t;

/* ── Backend аттестации (ТЗ §6.3) ───────────────────────────────────── */
typedef enum trust_backend {
    TRUST_BE_NONE      = 0,
    TRUST_BE_SOFTWARE  = 1,   /* не является TEE; пригоден для отработки */
    TRUST_BE_TPM20     = 2,
    TRUST_BE_SGX       = 3,
    TRUST_BE_SEV_SNP   = 4,
    TRUST_BE_TDX       = 5,
    TRUST_BE_TRUSTZONE = 6,
    TRUST_BE_NITRO     = 7,
    TRUST_BE_KEYSTONE  = 8,
    TRUST_BE_MAX       = 9
} trust_backend_t;

/* ── Свойства защиты (E2-PROP) ──────────────────────────────────────── */
typedef enum trust_property {
    TRUST_PROP_MEMORY_ISOLATION   = 0,
    TRUST_PROP_SEALED_STORAGE     = 1,
    TRUST_PROP_REMOTE_ATTESTATION = 2,
    TRUST_PROP_ROLLBACK_PROTECT   = 3,
    TRUST_PROP_MEASURED_BOOT      = 4,
    TRUST_PROP_SECURE_BOOT        = 5,
    TRUST_PROP_RNG_ENTROPY        = 6,
    TRUST_PROP_TIME_BOUND         = 7,
    TRUST_PROP_MAX                = 8
} trust_property_t;

typedef enum trust_prop_state {
    TRUST_PS_UNKNOWN     = 0,
    TRUST_PS_UNSUPPORTED = 1,
    TRUST_PS_DEGRADED    = 2,
    TRUST_PS_SUPPORTED   = 3,
    TRUST_PS_STATE_MAX   = 4
} trust_prop_state_t;

/* Личность backend хранится ОТДЕЛЬНО от свойств (E2-PROP-01): «это SGX»
 * и «память изолирована» — разные утверждения, и первое не доказывает
 * второго ни на одной платформе. */
typedef struct trust_protection {
    uint32_t abi_version;
    uint32_t placement;                     /* trust_placement_t */
    uint32_t backend;                       /* trust_backend_t */
    char     backend_id[TRUST_ATT_DESC_MAX];/* конкретный экземпляр */
    uint32_t state[TRUST_PROP_MAX];         /* trust_prop_state_t по свойству */
    /* Область действия защиты. Деградация может её только сузить. */
    uint32_t scope_bits;
} trust_protection_t;

void trust_protection_init(trust_protection_t *p, uint32_t placement,
                           uint32_t backend, const char *backend_id);

const char *trust_property_name(uint32_t p);
const char *trust_prop_state_name(uint32_t s);
const char *trust_backend_name(uint32_t b);
const char *trust_placement_name(uint32_t p);

/* Свойство выполнено, только если SUPPORTED. E2-PROP-04: degraded,
 * unsupported и unknown интерпретируются одинаково — не выполнено. */
int trust_prop_satisfied(const trust_protection_t *p, uint32_t prop);

/* Проверка требуемого набора ДО эффекта (E2-PROP-03). required — битовая
 * маска по trust_property_t. Возвращает 1, если все требуемые выполнены. */
int trust_props_required_check(const trust_protection_t *p, uint32_t required,
                               char *why, size_t whylen);

/* Деградация свойства. Расширить область при деградации нельзя: попытка
 * задать scope_bits шире текущего отвергается (E2-PROP-03). */
int trust_prop_degrade(trust_protection_t *p, uint32_t prop, uint32_t new_state,
                       uint32_t new_scope_bits, char *why, size_t whylen);

/* E3-ATT-01: Secure Boot и Measured Boot — обязательная пара для профиля
 * Central appliance. Возвращает 1, если профиль удовлетворён. */
int trust_boot_pair_ok(const trust_protection_t *p, char *why, size_t whylen);

/* ── Измеряемая загрузка ─────────────────────────────────────────────── */
typedef struct trust_pcr_bank {
    uint8_t pcr[TRUST_PCR_COUNT][TRUST_HASH_LEN];
} trust_pcr_bank_t;

typedef struct trust_event {
    uint32_t     pcr_index;
    trust_hash_t digest;
    char         desc[TRUST_ATT_DESC_MAX];
} trust_event_t;

void trust_pcr_reset(trust_pcr_bank_t *b);
/* PCR[i] = SHA-256(PCR[i] || digest). Однонаправленность расширения —
 * то, что не даёт «дописать» правильное измерение после неправильного. */
int  trust_pcr_extend(trust_pcr_bank_t *b, uint32_t idx,
                      const trust_hash_t *digest);
/* Проигрывание журнала событий в чистый банк. Совпадение результата с
 * настоящими PCR — единственное, что делает журнал событий доказательством,
 * а не рассказом. */
int  trust_eventlog_replay(const trust_event_t *ev, size_t n,
                           trust_pcr_bank_t *out);
/* Свёртка выбранных PCR — то, что подписывается в quote. */
int  trust_pcr_digest(const trust_pcr_bank_t *b, const uint32_t *select,
                      size_t n_select, trust_hash_t *out);

/* ── Quote и допуск ──────────────────────────────────────────────────── */
typedef enum trust_effect_class {
    TRUST_EFFECT_REVERSIBLE   = 0,
    TRUST_EFFECT_BOUNDED      = 1,
    TRUST_EFFECT_IRREVERSIBLE = 2,
    TRUST_EFFECT_MAX          = 3
} trust_effect_class_t;

typedef struct trust_quote {
    uint32_t     attestation_id;
    uint32_t     backend;
    trust_hash_t nonce;        /* challenge проверяющего */
    trust_hash_t pcr_digest;   /* свёртка выбранных PCR */
    uint32_t     select[TRUST_PCR_SELECT_MAX];
    uint32_t     n_select;
    uint64_t     signed_at_s;
    uint8_t      signature_valid;  /* проверено вызывающим */
} trust_quote_t;

typedef struct trust_att_policy {
    /* Порог свежести по классам эффекта (E3-ATT-03). */
    uint64_t max_age_s[TRUST_EFFECT_MAX];
    /* Ожидаемая свёртка PCR. Нулевая — «ожидание не задано»: отказ, а не
     * «подойдёт любое». */
    trust_hash_t expected_pcr;
    uint8_t      expected_set;
    /* Допустимые backend'ы, битовая маска по trust_backend_t. */
    uint32_t     allowed_backends;
    /* Требовать, чтобы nonce совпал с выданным challenge. */
    uint8_t      require_nonce_match;
} trust_att_policy_t;

typedef enum trust_admit_rc {
    TRUST_ADMIT_OK            = 0,
    TRUST_ADMIT_STALE         = 1,
    TRUST_ADMIT_PCR_MISMATCH  = 2,
    TRUST_ADMIT_BAD_SIG       = 3,
    TRUST_ADMIT_BACKEND       = 4,
    TRUST_ADMIT_NONCE         = 5,
    TRUST_ADMIT_NO_EXPECTED   = 6,
    TRUST_ADMIT_REVOKED       = 7,
    TRUST_ADMIT_EINVAL        = 8,
    TRUST_ADMIT_FULL          = 9,
    TRUST_ADMIT_RC_MAX        = 10
} trust_admit_rc_t;

const char *trust_admit_rc_name(trust_admit_rc_t rc);
const char *trust_effect_class_name(uint32_t c);

typedef struct trust_grant {
    uint32_t     grant_id;
    uint32_t     attestation_id;   /* от какой аттестации произошёл */
    uint32_t     effect_class;
    trust_hash_t pcr_digest;       /* состояние, которому выдан допуск */
    uint64_t     issued_at_s;
    uint64_t     expires_at_s;
    uint8_t      live;             /* 0 после отзыва */
    char         revoked_why[TRUST_ATT_DESC_MAX];
} trust_grant_t;

typedef struct trust_att_state {
    uint32_t      abi_version;
    uint32_t      n_grants;
    trust_grant_t grant[TRUST_GRANT_MAX];
    uint32_t      next_grant_id;
    /* След отзыва: сколько допусков аннулировано и по какой аттестации. */
    uint32_t      n_revocations;
    uint32_t      revoked_attestation[TRUST_TRAIL_MAX];
    uint32_t      revoked_count[TRUST_TRAIL_MAX];
} trust_att_state_t;

void trust_att_state_init(trust_att_state_t *st);

/* Выдать допуск под класс эффекта. Именно здесь применяются E3-ATT-02/03. */
trust_admit_rc_t trust_attest_admit(trust_att_state_t *st,
                                    const trust_quote_t *q,
                                    const trust_att_policy_t *pol,
                                    const trust_hash_t *challenge,
                                    uint32_t effect_class,
                                    uint64_t now_s,
                                    uint32_t *grant_id_out,
                                    char *why, size_t whylen);

/* Применить допуск сейчас. Проверяет срок, живость и — главное —
 * совпадение слепка PCR с текущим состоянием: допуск выдан состоянию. */
trust_admit_rc_t trust_grant_use(const trust_att_state_t *st, uint32_t grant_id,
                                 const trust_hash_t *current_pcr,
                                 uint64_t now_s, char *why, size_t whylen);

/* E3-ATT-04: отзыв аттестации аннулирует все выданные по ней допуски.
 * Возвращает число аннулированных. */
uint32_t trust_attest_revoke(trust_att_state_t *st, uint32_t attestation_id,
                             const char *why);

/* Сколько допусков живо сейчас. */
uint32_t trust_att_live_grants(const trust_att_state_t *st);
include/platx/trust_backend.h
/* trust_backend.h — ИСПОЛНИТЕЛЬНЫЙ шов backend'а seL4/TEE (TE-STAT/TE-ATT).
 *
 * ЗАЧЕМ ЭТОТ ФАЙЛ СУЩЕСТВУЕТ
 * В дереве есть политика ПРО seL4/TEE (trust_capab: механизмы, режимы,
 * замки, допуск), но нет ни одного backend'а: ни seL4, ни SGX/TDX/SEV,
 * ни TrustZone, ни TPM-стека. Проверено 09.09.2026 поиском по дереву —
 * ноль файлов. Это нормальное состояние (владелец: «не факт, что хватит
 * прав развернуть TEE/seL4»), но оно обязано быть ОБРАБОТАНО, а не просто
 * отсутствовать: иначе однажды появится weak-символ, вернувший успех без
 * провайдера, и «защита работает» станет неотличимо от «защиты нет».
 *
 * ЧЕМ ЭТОТ ШОВ НЕ ЯВЛЯЕТСЯ
 *  1. Это НЕ источник наблюдений. Состояние механизма по-прежнему берётся
 *     из независимого источника (TE-SEC-03): опрос механизма самим
 *     механизмом — его заявление о себе, а не свидетельство. Привязка
 *     backend'а НЕ делает механизм usable и НЕ влияет на trust_capab_admit.
 *  2. Это НЕ пятый реестр (C15/C20). Здесь один слот на механизм, массив
 *     фиксированного размера TRUST_MECH_MAX, без поиска по имени, без
 *     динамики и без владения памятью.
 *
 * ЧТО ЭТО: набор операций, которые может выполнить ТОЛЬКО настоящий
 * механизм (запечатать секрет так, чтобы его не прочли снаружи; выдать
 * свидетельство о состоянии, привязанное к nonce). Пока backend'а нет,
 * каждая такая операция обязана отказать ДО эффекта.
 *
 * ── Объявление по C22 (иначе SPEC_ONLY) ─────────────────────────────────
 *  upstream gate      : make te-backend (гейт этого шва)
 *  downstream consumer: trust_cli/trustctl (печать состояния шва); будущий
 *                       trust_attest при появлении настоящего backend'а
 *  public capability  : trust_backend_v1_t + функции этого заголовка,
 *                       ABI-версия TRUST_BACKEND_ABI
 *  owner данных       : вызывающий (буферы его, шов ничего не выделяет)
 *  owner решения      : trust_capab (политика) — шов решений не принимает
 *  owner эффекта      : сам backend; без него эффекта не происходит вовсе
 *  failure semantics  : отказ ДО эффекта, выходной буфер не трогается,
 *                       *out_len = 0; частичного результата не бывает
 *  rollback semantics : откатывать нечего — операция либо выполнена
 *                       backend'ом целиком, либо не начата
 *  fixture            : tests/roadmap/agent4/te/t_te_backend.c
 *  exit gate          : te-backend PASS + мутации пойманы
 *  удаляемый прототип : trust_backend_null() — исчезает в тот день, когда
 *                       появится первый настоящий backend; до тех пор он
 *                       обязан ОТКАЗЫВАТЬ, а не возвращать пустой успех
 */

#define TRUST_BACKEND_ABI      1u
#define TRUST_BACKEND_NAME_MAX 32u

/* Результат операции шва. Значения различимы намеренно: «backend'а нет»
 * и «backend отказал» — разные факты, и сливать их нельзя (TE-SEC-04). */
typedef enum trust_bk_rc {
    TRUST_BK_OK          = 0,
    TRUST_BK_ENOBACKEND  = 1,  /* backend не привязан либо не собран      */
    TRUST_BK_EINVAL      = 2,  /* аргументы негодны — отказ до эффекта    */
    TRUST_BK_ENOSPACE    = 3,  /* выходной буфер мал — отказ до эффекта   */
    TRUST_BK_EBACKEND    = 4,  /* backend есть и ОТКАЗАЛ                  */
    TRUST_BK_EBOUND      = 5,  /* слот уже занят: подмена запрещена       */
    TRUST_BK_ENOTCOMPILED= 6,  /* механизма нет в ЭТОЙ сборке             */
    TRUST_BK_RC_MAX      = 7
} trust_bk_rc_t;

const char *trust_bk_rc_name(uint32_t rc);

/* Операции, которые может выполнить только настоящий механизм.
 * Любая может быть NULL — тогда именно она даёт TRUST_BK_ENOBACKEND,
 * а остальные продолжают работать. Частичный backend — обычное дело:
 * seL4-узел умеет изоляцию, но не умеет quote. */
typedef struct trust_backend_v1 {
    uint32_t    abi;                       /* TRUST_BACKEND_ABI          */
    uint32_t    mech;                      /* trust_mech_t               */
    const char *name;                      /* "sel4-microkit", "sgx-dcap"*/

    /* Запечатать так, чтобы прочесть можно было только внутри механизма. */
    int (*seal)(const uint8_t *in, size_t in_len,
                uint8_t *out, size_t out_cap, size_t *out_len);
    int (*unseal)(const uint8_t *in, size_t in_len,
                  uint8_t *out, size_t out_cap, size_t *out_len);
    /* Свидетельство о состоянии, привязанное к nonce (TE-ATT-13:
     * без nonce свидетельство переигрывается). */
    int (*quote)(const uint8_t nonce[32],
                 uint8_t *out, size_t out_cap, size_t *out_len);
} trust_backend_v1_t;

/* Сбросить все привязки. Только для тестов и переинициализации узла. */
void trust_backend_reset(void);

/* Привязать backend к механизму. Один слот на механизм:
 *  - механизм вне этой сборки        -> TRUST_BK_ENOTCOMPILED
 *  - слот уже занят                  -> TRUST_BK_EBOUND (подмена запрещена)
 *  - abi не совпал / b == NULL       -> TRUST_BK_EINVAL
 * Привязка НЕ делает механизм usable: состояние решает наблюдение. */
trust_bk_rc_t trust_backend_bind(uint32_t mech, const trust_backend_v1_t *b);

/* Привязанный backend или NULL. */
const trust_backend_v1_t *trust_backend_of(uint32_t mech);

/* Сколько механизмов имеют привязанный backend (для паспорта TCB). */
uint32_t trust_backend_count(void);

/* ── Операции. Все отказывают ДО эффекта, если backend'а нет ──────────── */
trust_bk_rc_t trust_backend_seal(uint32_t mech,
                                 const uint8_t *in, size_t in_len,
                                 uint8_t *out, size_t out_cap,
                                 size_t *out_len);
trust_bk_rc_t trust_backend_unseal(uint32_t mech,
                                   const uint8_t *in, size_t in_len,
                                   uint8_t *out, size_t out_cap,
                                   size_t *out_len);
trust_bk_rc_t trust_backend_quote(uint32_t mech, const uint8_t nonce[32],
                                  uint8_t *out, size_t out_cap,
                                  size_t *out_len);

/* Удаляемый прототип: backend, который честно НИЧЕГО не умеет. Нужен,
 * чтобы отличать «слот пуст» от «backend привязан и отказал», и чтобы у
 * теста был предмет. Возвращает отказ на каждой операции — именно этим
 * он и ценен: пустой успех здесь был бы тем самым дефектом. */
const trust_backend_v1_t *trust_backend_null(uint32_t mech);
include/platx/trust_bundle.h
/* platx/trust_bundle.h — проверка evidence-бандла (E3-ANCH-04/05).
 *
 * ТЗ TZ_PLATX_SEL4_TEE_V2.md §13, сценарий приёмки E3-T21 (CUT-E).
 *
 * Бандл проверяется ТРЕМЯ раздельными проверками, и все три обязательны:
 *
 *   1. Набор файлов. Каждый файл манифеста на месте и совпадает по
 *      свёртке; и — что чаще забывают — ни одного файла сверх манифеста.
 *      Добавленный файл, которого нет в манифесте, — отказ. Иначе в
 *      бандл кладётся «пояснительная записка», меняющая прочтение улик,
 *      и ни одна свёртка этого не замечает.
 *
 *   2. Вердикт против улик. Вердикт обязан ссылаться на улики, каждая
 *      ссылка обязана разрешаться, и PASS не может опираться на улику,
 *      которая не PASS. Вердикт без ссылок — мнение, а не вывод.
 *
 *   3. Внешняя привязка. Утверждение бандла должно быть включено во
 *      внешний журнал (§13). Печать самого бандла основанием НЕ является:
 *      тот, кто правит бандл, ставит и печать. Поле seal_ok существует
 *      только для диагностики и в общий вердикт не входит — это прямое
 *      требование E3-ANCH-04, и оно же — единственное, что ловит CUT-E.
 *
 * Сценарий CUT-E, ради которого всё это: инсайдер меняет вердикт,
 * приводит улики в соответствие, пересчитывает манифест и перепечатывает
 * бандл. Проверки 1 и 2 у него зелёные — он их и подгонял. Красной
 * остаётся только 3.
 *
 * Файл описывает бандл как ДАННЫЕ. Чтения каталога здесь нет: ядро домена
 * не делает I/O, а разбор файлов живёт в host-части (trustctl).
 */

#define TRUST_BUNDLE_ABI        1u
#define TRUST_BUNDLE_FILES_MAX  256u
#define TRUST_BUNDLE_EVID_MAX   128u
#define TRUST_BUNDLE_VERD_MAX   64u
#define TRUST_BUNDLE_EXTRA_MAX  32u
#define TRUST_PATH_MAX          128u
#define TRUST_EVID_ID_MAX       48u
#define TRUST_CITES_MAX         8u
#define TRUST_BUNDLE_REASON     512u

/* Состояние улики. SKIP отделён от PASS намеренно: в этом дереве уже
 * находили обратное (A3-F002 — тест возвращал 0 на SKIP), и цена ошибки
 * там была «зелёный гейт на невыполненной проверке». */
typedef enum trust_evid_status {
    TRUST_EVID_UNKNOWN = 0,
    TRUST_EVID_PASS    = 1,
    TRUST_EVID_FAIL    = 2,
    TRUST_EVID_SKIP    = 3,
    TRUST_EVID_MISSING = 4,
    TRUST_EVID_STATUS_MAX = 5
} trust_evid_status_t;

typedef enum trust_verdict_claim {
    TRUST_VERDICT_UNKNOWN = 0,
    TRUST_VERDICT_PASS    = 1,
    TRUST_VERDICT_FAIL    = 2,
    TRUST_VERDICT_PARTIAL = 3,
    TRUST_VERDICT_DEFER   = 4,
    TRUST_VERDICT_MAX     = 5
} trust_verdict_claim_t;

typedef struct trust_bundle_file {
    char         path[TRUST_PATH_MAX];
    trust_hash_t declared;     /* свёртка из манифеста */
    trust_hash_t actual;       /* свёртка, посчитанная проверяющим */
    uint8_t      present;      /* файл найден */
    uint8_t      actual_known; /* свёртка посчитана */
} trust_bundle_file_t;

typedef struct trust_evidence {
    char     id[TRUST_EVID_ID_MAX];
    uint32_t status;                   /* trust_evid_status_t */
    char     artifact[TRUST_PATH_MAX]; /* файл-подтверждение в манифесте */
} trust_evidence_t;

typedef struct trust_verdict {
    char     id[TRUST_EVID_ID_MAX];
    uint32_t claimed;                  /* trust_verdict_claim_t */
    uint32_t n_cites;
    char     cite[TRUST_CITES_MAX][TRUST_EVID_ID_MAX];
} trust_verdict_t;

typedef struct trust_bundle {
    uint32_t abi_version;
    char     name[TRUST_PATH_MAX];

    uint32_t n_files;
    trust_bundle_file_t file[TRUST_BUNDLE_FILES_MAX];

    /* Файлы, найденные в каталоге бандла, но отсутствующие в манифесте. */
    uint32_t n_extra;
    char     extra[TRUST_BUNDLE_EXTRA_MAX][TRUST_PATH_MAX];

    uint32_t n_evidence;
    trust_evidence_t evidence[TRUST_BUNDLE_EVID_MAX];

    uint32_t n_verdicts;
    trust_verdict_t verdict[TRUST_BUNDLE_VERD_MAX];

    /* Собственная печать бандла. Диагностика, не основание. */
    uint8_t      seal_present;
    uint8_t      seal_valid;

    /* Привязка к внешнему журналу.
     *
     * Хранится ЗАПИСЬ, а не готовый лист. Разница решает исход CUT-E:
     * если верификатор принимает лист как данность, инсайдер оставляет
     * старый (честный) лист и его доказательство рядом с переписанным
     * вердиктом — включение сходится, потому что проверяется присутствие
     * ЧУЖОГО факта в журнале. Запись позволяет пересчитать свёртку
     * содержимого бандла и потребовать, чтобы опубликовано было ИМЕННО
     * то, что лежит в бандле сейчас. */
    uint8_t          anchor_present;   /* бандл заявляет включение */
    trust_log_entry_t anchor_entry;    /* запись, которая была опубликована */
    uint64_t     anchor_index;
    uint64_t     anchor_tree_size;
    trust_proof_t anchor_proof;
    trust_hash_t  anchor_root;     /* корень, подтверждённый со-подписантами */
    uint8_t       anchor_root_trusted; /* корень прошёл кворум */
} trust_bundle_t;

typedef struct trust_bundle_result {
    int      fileset_ok;
    int      verdict_ok;
    int      anchor_ok;
    int      overall_ok;

    /* Печать проверяется и печатается, но в overall_ok не входит. */
    int      seal_ok;
    int      seal_counted_as_grounds;   /* всегда 0 — инвариант E3-ANCH-04 */

    uint32_t n_missing;
    uint32_t n_digest_mismatch;
    uint32_t n_extra;
    uint32_t n_unresolved_cites;
    uint32_t n_uncited_verdicts;
    uint32_t n_overclaimed;      /* PASS поверх не-PASS улики */

    /* Опубликованная запись не соответствует содержимому бандла.
     * Ровно этот счётчик краснеет в сценарии CUT-E (E3-T21). */
    uint32_t claim_rebound;

    char     reason[TRUST_BUNDLE_REASON];
} trust_bundle_result_t;

void trust_bundle_init(trust_bundle_t *b, const char *name);

/* Свёртка содержимого бандла: манифест плюс вердикты плюс улики, на
 * которые вердикты ссылаются. Это то, что публикуется во внешнем журнале
 * и что нельзя переписать задним числом, не разойдясь с публикацией. */
int trust_bundle_claim_digest(const trust_bundle_t *b, trust_hash_t *out);

/* Проверка 1: набор файлов. */
int trust_bundle_check_fileset(const trust_bundle_t *b,
                               trust_bundle_result_t *r);

/* Проверка 2: вердикт против улик. */
int trust_bundle_check_verdicts(const trust_bundle_t *b,
                                trust_bundle_result_t *r);

/* Проверка 3: внешняя привязка. Требует доверенного корня: корень,
 * не прошедший кворум со-подписантов, основанием не является. */
int trust_bundle_check_anchor(const trust_bundle_t *b,
                              trust_bundle_result_t *r);

/* Все три вместе. Возвращает 1 только если все три зелёные. */
int trust_bundle_verify(const trust_bundle_t *b, trust_bundle_result_t *r);

const char *trust_evid_status_name(uint32_t s);
const char *trust_verdict_claim_name(uint32_t c);
include/platx/trust_capab.h
/* platx/trust_capab.h — TE-STAT/TE-PLACE: механизм защиты как НАСТРАИВАЕМАЯ
 *                       опция, у которой есть замок.
 *
 * ТЗ: docs/TZ_PLATX_TRUSTED_EXECUTION_V1.md — TE-STAT-01…04, TE-SEC-04,
 * TE-CON-01, TE-PLACE-01…02, TE-ATT-10. Решение владельца 09.09.2026:
 * «TEE, seL4 и подобное — опциональны, их можно отключить; и можно
 * запретить отключать конфигом. Не факт, что хватит прав развернуть TEE
 * или seL4 — платформа обязана работать и без них».
 *
 * Из этого решения следуют три вещи, и все три здесь закодированы, а не
 * описаны словами.
 *
 *   1. ОТСУТСТВИЕ МЕХАНИЗМА — НЕ ДЕФЕКТ САМО ПО СЕБЕ. Дефект — это
 *      расхождение между тем, что от узла ТРЕБУЕТСЯ, и тем, что он
 *      делает. Поэтому у каждого механизма есть режим требования
 *      (trust_req_mode_t), и «нет TEE» на узле, где TEE OPTIONAL, —
 *      обычное рабочее состояние, а не деградация и не авария.
 *
 *   2. ОТКЛЮЧАЕМОСТЬ САМА ПО СЕБЕ УПРАВЛЯЕМА. Иначе гибкость становится
 *      дырой: тот, кто может отключить обязательный контур, отменяет всю
 *      защиту одной строкой конфигурации. Поэтому у режима есть замок
 *      (trust_lock_t) и у замка есть владелец-слой (trust_origin_t).
 *      Слой ниже не снимает замок слоя выше — это и есть «запретить
 *      отключать конфигом». Отказ при этом ЯВНЫЙ (TRUST_CAP_ELOCKED),
 *      с именем запершего слоя: тихо проигнорированная настройка хуже
 *      отвергнутой, потому что оператор уверен, что применил её.
 *
 *   3. НЕТ ЕДИНОЙ ШКАЛЫ «СЛАБЕЕ → СИЛЬНЕЕ» (TE-STAT-01). Состояние
 *      механизма описывается ШЕСТЬЮ независимыми осями. Числовой ранг
 *      backend'ов («SGX = TDX = 4»), четырёхрежимная лестница A/B/C/D и
 *      любое «оно слабее, но сойдёт» отсюда исключены намеренно: именно
 *      через них происходит тихая подмена исполнителя.
 *
 * И одно правило, которое дороже остальных (TE-STAT-02): при обязательном
 * механизме выдача результата ОБХОДНЫМ путём — не деградация, а нарушение.
 * Нет TEE, а секрет требует TEE → отказ. Не «положим ключ в файл, зато
 * работает». Универсального флага --degraded-ok в этой модели нет и не
 * может быть; функция trust_capab_substitution_allowed() существует
 * ровно затем, чтобы на этот вопрос отвечал код, а не привычка.
 *
 * Домен самодостаточен: stdint/stddef и ничего больше. Контур доверия,
 * зависящий от половины дерева, доверием не является.
 *
 * ABI: TRUST_CAPAB_ABI 1.
 */

#define TRUST_CAPAB_ABI       1u
#define TRUST_CAP_NAME_MAX    24u
#define TRUST_CAP_WHY_MAX     192u

/* ── Механизмы ───────────────────────────────────────────────────────────
 * Значения фиксированы: они уходят в evidence и в подписанную политику.
 * Освободившееся значение не переиспользуется — старое evidence станет
 * читаться неверно.
 *
 * В списке нарочно соседствуют вещи разного веса: TPM есть почти везде,
 * seL4 не будет почти нигде. Это и есть предмет настройки. */
typedef enum trust_mech {
    TRUST_MECH_TPM20        = 0,   /* дискретный или firmware TPM 2.0     */
    TRUST_MECH_SECUREBOOT   = 1,   /* UEFI Secure Boot: принуждение       */
    TRUST_MECH_MEASUREDBOOT = 2,   /* измерение загрузки: протокол        */
    TRUST_MECH_TEE_SGX      = 3,
    TRUST_MECH_TEE_TDX      = 4,
    TRUST_MECH_TEE_SEV_SNP  = 5,
    TRUST_MECH_TEE_TRUSTZONE= 6,
    TRUST_MECH_SEL4_NODE    = 7,   /* узел на seL4/Microkit               */
    TRUST_MECH_HV_PLATX     = 8,   /* собственный гипервизор (вне spine)  */
    TRUST_MECH_IOMMU        = 9,   /* IOMMU/SMMU с ПРОВЕРЕННОЙ конфигурацией */
    TRUST_MECH_BPF_LSM      = 10,
    TRUST_MECH_LANDLOCK     = 11,
    TRUST_MECH_SECCOMP      = 12,
    TRUST_MECH_PQ_HYBRID    = 13,  /* гибридная постквантовая криптография */
    TRUST_MECH_ANCHOR_COSIGN= 14,  /* независимый со-подписант якоря       */
    TRUST_MECH_HW_ZEROIZE   = 15,  /* аппаратно подтверждаемое уничтожение */
    TRUST_MECH_MAX          = 16
} trust_mech_t;

/* ── Режим требования (TE-CON-01) ────────────────────────────────────────
 * Порядок значений = сила требования. На этом порядке стоят замки FLOOR и
 * CEILING, поэтому менять его нельзя без пересмотра trust_capab_set(). */
typedef enum trust_req_mode {
    TRUST_REQ_FORBIDDEN = 0,  /* использовать запрещено; работающий механизм
                               * при этом режиме — нарушение, а не бонус   */
    TRUST_REQ_OPTIONAL  = 1,  /* можно; на допуск не влияет никак          */
    TRUST_REQ_PREFERRED = 2,  /* использовать при наличии; отсутствие —
                               * записываемое понижение, но не отказ       */
    TRUST_REQ_REQUIRED  = 3,  /* без него отказ ДО эффекта                 */
    TRUST_REQ_MODE_MAX  = 4
} trust_req_mode_t;

/* ── Замок (ядро решения владельца) ──────────────────────────────────────
 * Замок не запрещает изменения вообще — он задаёт РАЗРЕШЁННОЕ НАПРАВЛЕНИЕ.
 * Смысл каждого варианта проще всего читать через вопрос «что именно
 * нельзя сделать»:
 *
 *   FLOOR   — нельзя ослабить. Обязательный механизм не отключить снизу.
 *   CEILING — нельзя усилить. Механизм, которого на этом узле не будет
 *             (нет прав, нет железа, лабораторный стенд), не потребовать.
 *   PINNED  — нельзя ни то, ни другое.
 *
 * CEILING нужен не для симметрии: без него узел, где TEE физически
 * недоступен, получает от вышестоящей политики REQUIRED и встаёт колом,
 * хотя правильный ответ — «здесь этот механизм не применяется». */
typedef enum trust_lock {
    TRUST_LOCK_NONE    = 0,
    TRUST_LOCK_FLOOR   = 1,
    TRUST_LOCK_CEILING = 2,
    TRUST_LOCK_PINNED  = 3,
    TRUST_LOCK_MAX     = 4
} trust_lock_t;

/* ── Слой, от имени которого идёт изменение ──────────────────────────────
 * Ранг растёт вниз. Оператор нарочно СЛАБЕЕ конфигурации: «запретить
 * отключать конфигом» означает ровно то, что человек за консолью не
 * снимает замок, поставленный конфигурацией узла. А конфигурация узла, в
 * свою очередь, не снимает замок подписанной политики — иначе подпись
 * ничего не стоит. */
typedef enum trust_origin {
    TRUST_ORIGIN_BUILTIN  = 0,  /* умолчание сборки                        */
    TRUST_ORIGIN_PROFILE  = 1,  /* профиль размещения (TE-PLACE-01)        */
    TRUST_ORIGIN_OPERATOR = 2,  /* CLI, живой человек                      */
    TRUST_ORIGIN_SITE     = 3,  /* конфигурация узла                       */
    TRUST_ORIGIN_SIGNED   = 4,  /* подписанная политика владельца          */
    TRUST_ORIGIN_MAX      = 5
} trust_origin_t;

/* ── Шесть осей состояния (TE-STAT-01) ───────────────────────────────────
 * Оси независимы. Ни одна не выводится из другой, и ни одна не заменяет
 * другую. Наличие устройства не означает прав на него; права не означают
 * инициализации; инициализация не означает принуждения; принуждение не
 * означает, что кто-то это наблюдает. */
typedef enum trust_axis_avail {
    TRUST_AVAIL_UNKNOWN = 0, TRUST_AVAIL_ABSENT = 1, TRUST_AVAIL_PRESENT = 2
} trust_axis_avail_t;

typedef enum trust_axis_authz {
    TRUST_AUTHZ_UNKNOWN = 0, TRUST_AUTHZ_DENIED = 1, TRUST_AUTHZ_GRANTED = 2
} trust_axis_authz_t;

typedef enum trust_axis_life {
    TRUST_LIFE_UNINIT = 0, TRUST_LIFE_PREPARING = 1, TRUST_LIFE_ACTIVE = 2,
    TRUST_LIFE_QUIESCED = 3, TRUST_LIFE_FAULTED = 4
} trust_axis_life_t;

typedef enum trust_axis_ev {
    TRUST_EV_NONE = 0, TRUST_EV_STALE = 1, TRUST_EV_INVALID = 2, TRUST_EV_VALID = 3
} trust_axis_ev_t;

typedef enum trust_axis_enf {
    TRUST_ENF_OFF = 0, TRUST_ENF_ADVISORY = 1, TRUST_ENF_ENFORCING = 2
} trust_axis_enf_t;

typedef enum trust_axis_obs {
    TRUST_OBS_UNOBSERVED = 0,   /* никто не смотрит — это НЕ «нарушений нет» */
    TRUST_OBS_UNOBSERVABLE = 1, /* смотреть нечем: отдельный результат       */
    TRUST_OBS_OBSERVED = 2
} trust_axis_obs_t;

/* ── Шесть различимых причин (TE-SEC-04) ─────────────────────────────────
 * Слить их в одну «недоступно» — значит потерять единственное, что
 * отличает «на этой машине нет TPM» от «TPM есть, но evidence подделан».
 * Причины 4, 5 и 6 не переводят исполнение на более слабого исполнителя
 * автоматически: они порождают решение политики (см. TRUST_ADM_DECIDE). */
typedef enum trust_cap_reason {
    TRUST_WHY_OK            = 0,
    TRUST_WHY_NO_DEVICE     = 1,  /* устройства нет физически              */
    TRUST_WHY_NO_PERMISSION = 2,  /* устройство есть, прав нет             */
    TRUST_WHY_NO_BACKEND    = 3,  /* права есть, реализация не установлена */
    TRUST_WHY_EV_STALE      = 4,
    TRUST_WHY_EV_INVALID    = 5,
    TRUST_WHY_COMPROMISED   = 6,  /* наблюдаемая компрометация             */
    TRUST_WHY_NOT_ENFORCING = 7,  /* работает, но ничего не принуждает     */
    TRUST_WHY_FORBIDDEN_ON  = 8,  /* запрещён политикой, но действует      */
    TRUST_WHY_MAX           = 9
} trust_cap_reason_t;

/* ── Профили размещения (TE-PLACE-01) ────────────────────────────────────
 * Профиль — это ИМЕНОВАННЫЙ НАБОР ТРЕБОВАНИЙ, а не отдельная реализация.
 * PORTABLE существует прежде всего: узел без TPM, без TEE, без seL4 и без
 * прав на них — это поддерживаемая конфигурация, а не недоделанная. */
typedef enum trust_profile {
    TRUST_PROFILE_PORTABLE    = 0,
    TRUST_PROFILE_OS_ENFORCED = 1,
    TRUST_PROFILE_TEE_SERVICE = 2,
    TRUST_PROFILE_SEL4_NODE   = 3,
    TRUST_PROFILE_HYBRID      = 4,
    TRUST_PROFILE_MAX         = 5
} trust_profile_t;

/* ── Запись реестра ──────────────────────────────────────────────────── */
typedef struct trust_cap_entry {
    uint8_t  mode;         /* trust_req_mode_t                            */
    uint8_t  lock;         /* trust_lock_t                                */
    uint8_t  lock_origin;  /* trust_origin_t, поставивший замок           */
    uint8_t  mode_origin;  /* trust_origin_t, установивший режим          */
} trust_cap_entry_t;

typedef struct trust_capab_registry {
    uint32_t          abi_version;
    uint32_t          profile;      /* trust_profile_t, из которого выросло */
    trust_cap_entry_t e[TRUST_MECH_MAX];
    /* Строгий режим: UNKNOWN на любой оси трактуется как «не выполнено».
     * По умолчанию включён — fail-closed (C7). */
    uint8_t           strict_unknown;
    /* Счётчик отвергнутых по замку изменений. Это не украшение: попытка
     * снять обязательный контур — событие безопасности, и она обязана
     * быть видимой без чтения логов. */
    uint32_t          refused_changes;
} trust_capab_registry_t;

/* ── Наблюдаемое состояние механизма ─────────────────────────────────── */
typedef struct trust_mech_status {
    uint8_t availability;   /* trust_axis_avail_t */
    uint8_t authorization;  /* trust_axis_authz_t */
    uint8_t lifecycle;      /* trust_axis_life_t  */
    uint8_t evidence;       /* trust_axis_ev_t    */
    uint8_t enforcement;    /* trust_axis_enf_t   */
    uint8_t observation;    /* trust_axis_obs_t   */
} trust_mech_status_t;

/* ── Коды возврата настройки ─────────────────────────────────────────── */
typedef enum trust_cap_rc {
    TRUST_CAP_OK      = 0,
    TRUST_CAP_ELOCKED = 1,  /* замок вышестоящего слоя не даёт            */
    TRUST_CAP_EINVAL  = 2,
    TRUST_CAP_ENOENT  = 3,
    TRUST_CAP_RC_MAX  = 4
} trust_cap_rc_t;

/* ── Вердикт допуска ─────────────────────────────────────────────────── */
typedef enum trust_adm_verdict {
    TRUST_ADM_ALLOW  = 0,
    TRUST_ADM_DECIDE = 1,   /* требуется решение политики: устаревшее либо
                             * недостоверное evidence, либо компрометация —
                             * тихо понижать исполнителя запрещено         */
    TRUST_ADM_DENY   = 2,
    TRUST_ADM_MAX    = 3
} trust_adm_verdict_t;

typedef struct trust_adm_result {
    uint8_t  verdict;             /* trust_adm_verdict_t                   */
    uint8_t  weakest_mech;        /* механизм, определивший вердикт        */
    uint8_t  weakest_reason;      /* trust_cap_reason_t                    */
    uint8_t  substitution_allowed;/* TE-STAT-02: обходной путь разрешён?   */
    uint32_t n_required;          /* сколько механизмов обязательны        */
    uint32_t n_required_met;
    uint32_t n_preferred_missing; /* записываемое понижение                */
    uint32_t n_violations;        /* FORBIDDEN, который действует          */
    uint32_t n_unobserved;        /* работает, но никем не наблюдается     */
    char     why[TRUST_CAP_WHY_MAX];
} trust_adm_result_t;

/* ── Имена (печать в evidence и CLI) ─────────────────────────────────── */
const char *trust_mech_name(uint32_t m);
const char *trust_req_mode_name(uint32_t m);
const char *trust_lock_name(uint32_t l);
const char *trust_origin_name(uint32_t o);
const char *trust_cap_reason_name(uint32_t r);
const char *trust_profile_name(uint32_t p);
const char *trust_adm_verdict_name(uint32_t v);
const char *trust_cap_rc_name(uint32_t rc);

/* Разбор имени механизма и режима из текста конфигурации. Чистые функции:
 * файл читает host-часть, разбирает — ядро. Возвращают TRUST_MECH_MAX /
 * TRUST_REQ_MODE_MAX при неизвестном имени (а не «по умолчанию»). */
uint32_t trust_mech_by_name(const char *s);
uint32_t trust_req_mode_by_name(const char *s);
uint32_t trust_lock_by_name(const char *s);
uint32_t trust_profile_by_name(const char *s);

/* ── Реестр ──────────────────────────────────────────────────────────── */
/* Инициализация профилем. Всё, что профиль не назвал обязательным,
 * получает OPTIONAL — то есть «может отсутствовать, и это нормально». */
void trust_capab_init(trust_capab_registry_t *r, uint32_t profile);

/* Изменить режим от имени слоя. Единственная точка, где применяются
 * замки. why (может быть NULL) получает объяснение отказа с ИМЕНЕМ
 * запершего слоя — чтобы оператор знал, куда идти, а не гадал. */
trust_cap_rc_t trust_capab_set(trust_capab_registry_t *r, uint32_t mech,
                               uint32_t mode, uint32_t origin,
                               char *why, size_t whylen);

/* Поставить или снять замок. Снять может только слой не ниже того,
 * который замок поставил. */
trust_cap_rc_t trust_capab_lock_set(trust_capab_registry_t *r, uint32_t mech,
                                    uint32_t lock, uint32_t origin,
                                    char *why, size_t whylen);

/* Разрешено ли слою origin изменить режим механизма на mode. Ответ без
 * побочного эффекта — нужен CLI, чтобы не предлагать невозможного. */
int trust_capab_change_allowed(const trust_capab_registry_t *r, uint32_t mech,
                               uint32_t mode, uint32_t origin);

/* Объяснение «почему этот механизм нельзя отключить». Пишет в why и
 * возвращает 1, если механизм действительно заперт от ослабления. */
int trust_capab_explain_lock(const trust_capab_registry_t *r, uint32_t mech,
                             char *why, size_t whylen);

uint32_t trust_capab_mode(const trust_capab_registry_t *r, uint32_t mech);
uint32_t trust_capab_lock_of(const trust_capab_registry_t *r, uint32_t mech);

/* ── Состояние ───────────────────────────────────────────────────────── */
void trust_mech_status_init(trust_mech_status_t *s);

/* Механизм пригоден к использованию: есть, разрешён, активен, evidence
 * достоверно, принуждает. Наблюдаемость СЮДА НЕ ВХОДИТ намеренно — она
 * влияет на уверенность в вердикте, а не на работоспособность. */
int trust_mech_usable(const trust_mech_status_t *s, int strict_unknown);

/* Причина непригодности — ровно одна из шести (TE-SEC-04), выбранная в
 * порядке «что мешает раньше всего». */
uint32_t trust_mech_reason(const trust_mech_status_t *s, int strict_unknown);

/* ── Допуск ──────────────────────────────────────────────────────────── */
/* Решение ДО эффекта. st — массив на TRUST_MECH_MAX элементов. */
void trust_capab_admit(const trust_capab_registry_t *r,
                       const trust_mech_status_t *st,
                       trust_adm_result_t *out);

/* TE-STAT-02: разрешён ли обходной путь получения результата. Возвращает
 * 0 всегда, когда хоть один механизм обязателен. Отдельная функция, а не
 * поле, чтобы на вопрос отвечал код в одном месте. */
int trust_capab_substitution_allowed(const trust_capab_registry_t *r);

/* Сколько механизмов обязательно сейчас. */
uint32_t trust_capab_required_count(const trust_capab_registry_t *r);

/* ── Опциональность НА УРОВНЕ СБОРКИ (mk/trust-features.mk) ───────────────
 * Отдельно от режимов механизма: режим — это политика во время работы,
 * а здесь ответ на вопрос «есть ли в ЭТОМ бинаре код backend'а вообще».
 * Сборка с -DPLATX_NO_TEE / -DPLATX_NO_SEL4 / -DPLATX_NO_HV / -DPLATX_NO_TPM
 * убирает соответствующие backend'ы физически; функции ниже читают ровно эти
 * дефайны и говорят про бинарь правду. «Не собрано» — НЕ «не установлено»
 * и не «нет прав»: это четвёртая, отличимая ситуация (TE-SEC-04). */
#define TRUST_FEAT_TEE   0x1u   /* SGX/TDX/SEV-SNP/TrustZone вкомпилированы */
#define TRUST_FEAT_SEL4  0x2u   /* узел seL4/Microkit вкомпилирован        */
#define TRUST_FEAT_HV    0x4u   /* собственный гипервизор platx_hv собран  */
/* TRUST_FEAT_TPM появился 10.09.2026 вместе с продвижением полосы TE.5 в
 * продукт. До него у TPM не было СВОЕЙ единицы трансляции: механизм
 * существовал как номер в перечислении, и «вкомпилирован ли он» был
 * вопросом без предмета — отвечать 1 было честно. Теперь предмет есть:
 * src/trust/tpm/{tpm2_dev,tpm_backend,trust_cli_tpm}.c открывают /dev/tpm0.
 * Сборка без них обязана отвечать 0, иначе trust_mech_reason назовёт
 * причиной NOT_PRESENT (нет железа) там, где истинная причина —
 * NO_BACKEND (кода нет в бинаре). Две разные причины, два разных
 * действия оператора; смешать их — значит послать человека искать TPM,
 * которого его сборка всё равно не увидит. */
#define TRUST_FEAT_TPM   0x8u   /* backend TPM 2.0 (/dev/tpm0) собран      */

/* Битовая маска собранных семейств backend'ов (TRUST_FEAT_*). */
uint32_t trust_build_features(void);

/* 1, если backend данного механизма физически присутствует в этой сборке.
 * Для механизма без аппаратного backend'а (LSM, seccomp, …) всегда 1: его
 * «наличие» решается во время работы, а не сборкой. Для TEE/seL4/HV/TPM —
 * зависит от -DPLATX_NO_* и потому от того, как собран бинарь. */
int trust_mech_compiled(uint32_t mech);

/* Применить строку конфигурации вида "tee.sgx=required" либо
 * "sel4.node=forbidden:pinned". Разбор + применение с учётом замков. */
trust_cap_rc_t trust_capab_apply_line(trust_capab_registry_t *r,
                                      const char *line, uint32_t origin,
                                      char *why, size_t whylen);
include/platx/trust_host.h
/* platx/trust_host.h — host-часть домена trust: разбор аргументов и I/O.
 *
 * Граница «ядро без эффектов» проходит здесь. Всё, что открывает файлы,
 * читает каталоги и печатает, живёт в src/trust/trust_cli.c и в этом
 * заголовке; ядро домена (trust_merkle/anchor/bundle/suite/witness/
 * attest/operator/zeroize) не делает ни одного системного вызова.
 *
 * Проверяется это не обещанием, а гейтом tests/trust/t_trust_no_effects.sh:
 * он смотрит на список внешних символов объектников ядра. Если однажды
 * fopen понадобится внутри ядра — гейт покраснеет, и это правильно.
 */

/* Единая точка входа CLI. Печатает в out/err, возвращает код процесса:
 * 0 — успех, 1 — проверка не прошла (fail-closed), 2 — ошибка вызова. */
int trust_cli(int argc, char **argv, FILE *out, FILE *err);
include/platx/trust_host_dir.h
/* platx/trust_host_dir.h — шов host-части домена trust: каталоги и вид файла.
 *
 * ЗАЧЕМ ОТДЕЛЬНЫЙ ШОВ. trust_cli.c читает бандл с диска: обходит каталог в
 * поисках файлов, которых нет в манифесте (scan_extra), и различает
 * обычный файл от каталога (load_manifest). Это ровно пять мест, где
 * host-часть зависит от ОС: opendir/readdir/closedir и stat ×2. Всё
 * остальное в trust_cli.c — stdio, переносимо по C11.
 *
 * Шов существует ради одного правила: #ifdef _WIN32 внутри trust_cli.c
 * ЗАПРЕЩЁН. Условная компиляция в общем файле делает «собрали под Linux» и
 * «собрали под Windows» двумя разными программами с одним именем, и
 * расхождение между ними не увидит ни один тест — каждый прогон видит
 * ровно одну ветку. Здесь вместо этого две ТЕ, по одной на ОС:
 *
 *   src/trust/trust_host_dir_posix.c              opendir / stat
 *   platx-windows/win/trustdom/trust_host_dir_win.c FindFirstFileW / GetFileAttributesExW
 *
 * Обе видны манифесту сборки. Отсутствие нужной — ошибка линковки, а не
 * тихо выпавшая функция. (ТЗ docs/TZ_TPM_HARDENING_V2_20260909.md §8.3.)
 *
 * ИНВАРИАНТЫ, которые обязана держать КАЖДАЯ реализация:
 *   INV-HDIR-01  "." и ".." не выдаются — вызывающий их не фильтрует.
 *   INV-HDIR-02  next() возвращает 0 ТОЛЬКО по концу каталога; сбой чтения
 *                записи — тоже 0 (бандл, который нельзя дочитать, — это
 *                бандл с неизвестным содержимым, а не «почти проверенный»),
 *                и его можно отличить по trust_host_dir_failed().
 *   INV-HDIR-03  kind() на несуществующем пути — TRUST_HOST_NONE, не OTHER.
 *   INV-HDIR-04  имя записи, не влезающее в cap, ОБРЕЗАЕТСЯ с завершающим
 *                нулём и помечается failed: усечённое имя — не имя файла.
 *   INV-HDIR-05  порядок записей — порядок ОС, шов его не сортирует.
 *                (Открытый вопрос TE-D-TPM-12: сортировать ли ради
 *                побитово одинакового вывода на разных ФС.)
 *
 * Заголовок OS-free: только <stddef.h>. Это позволяет собирать trust_cli.c
 * одной и той же командой на обеих платформах.
 */

typedef enum trust_host_kind {
    TRUST_HOST_NONE  = 0,   /* пути нет либо он недоступен            */
    TRUST_HOST_REG   = 1,   /* обычный файл                            */
    TRUST_HOST_DIR   = 2,   /* каталог                                  */
    TRUST_HOST_OTHER = 3    /* есть, но не файл и не каталог (сокет…)  */
} trust_host_kind_t;

/* Вид объекта по пути. Символические ссылки разыменовываются — так же,
 * как это делал stat(2) до введения шва; поведение на Linux не менялось. */
trust_host_kind_t trust_host_kind(const char *path);

/* Непрозрачный итератор. Память принадлежит шву. */
typedef struct trust_host_dir trust_host_dir_t;

/* NULL — каталог не открылся (нет, не каталог, нет прав). */
trust_host_dir_t *trust_host_dir_open(const char *path);

/* Следующее имя записи БЕЗ "." и "..". 1 — имя записано в out
 * (нуль-терминировано), 0 — записей больше нет либо чтение сорвалось
 * (см. trust_host_dir_failed). d == NULL допустим и даёт 0. */
int trust_host_dir_next(trust_host_dir_t *d, char *out, size_t cap);

/* 1, если после next() вернул 0 из-за сбоя, а не по концу каталога,
 * либо хотя бы одно имя не влезло в cap. Читать ПОСЛЕ последнего next(). */
int trust_host_dir_failed(const trust_host_dir_t *d);

/* NULL допустим. */
void trust_host_dir_close(trust_host_dir_t *d);
include/platx/trust_operator.h
/* platx/trust_operator.h — E3-AUTH: аутентификация оператора и защита
 *                          от принуждения.
 *
 * ТЗ TZ_PLATX_SEL4_TEE_V2.md §17, требования E3-AUTH-01…03.
 *
 * Здесь три разных механизма, и их легко перепутать:
 *
 *   E3-AUTH-01. Аппаратный фактор для необратимых действий. Пароль,
 *      каким бы длинным он ни был, воспроизводим тем, кто его увидел.
 *      Токен нужно иметь физически.
 *
 *   E3-AUTH-02. Duress-код — это НЕ «тревожная кнопка». Тревожная кнопка
 *      видна принуждающему: он стоит рядом и смотрит на экран. Duress-код
 *      обязан выглядеть как обычный успешный вход и при этом перевести
 *      узел в ограниченный режим. Поэтому в этом API видимый ответ для
 *      GRANTED и для DURESS — одна и та же строка, побайтово, и это
 *      проверяется тестом. Любая разница в тексте, в коде возврата,
 *      попадающем в интерфейс, или даже в длине ответа — это способ
 *      принуждающему узнать, что его обманули.
 *
 *   E3-AUTH-03. Правило двух человек — про НЕЗАВИСИМОСТЬ, а не про
 *      количество. Два фактора одного оператора — это один человек с
 *      двумя карманами. Начальник и подчинённый — тоже, по сути, один:
 *      второй подпишет то, что скажет первый. Поэтому проверяется
 *      отсутствие отношения подчинения в обе стороны.
 *
 * Криптографии здесь нет: проверку самого фактора (подпись FIDO2, PIN
 * смарт-карты) делает вызывающий и передаёт сюда результат. Этот файл
 * решает, ДОСТАТОЧНО ЛИ предъявленного, — и только это.
 */

#define TRUST_OP_ABI          1u
#define TRUST_OP_NAME_MAX     32u
#define TRUST_OP_ACTION_MAX   64u
#define TRUST_OP_FACTORS_MAX  8u
#define TRUST_OP_MAX          16u
#define TRUST_OP_REASON_MAX   192u

typedef enum trust_factor_kind {
    TRUST_FACTOR_NONE      = 0,
    TRUST_FACTOR_PASSWORD  = 1,   /* знание */
    TRUST_FACTOR_TOTP      = 2,   /* знание, производное от общего секрета */
    TRUST_FACTOR_FIDO2     = 3,   /* владение: аппаратный токен */
    TRUST_FACTOR_SMARTCARD = 4,   /* владение: аппаратная карта */
    TRUST_FACTOR_DURESS    = 5,   /* принуждение */
    TRUST_FACTOR_KIND_MAX  = 6
} trust_factor_kind_t;

typedef struct trust_operator {
    uint32_t id;
    char     name[TRUST_OP_NAME_MAX];
    /* Кому подчинён. 0 — никому. Используется только для правила двух
     * человек: подчинённый не является независимым свидетелем. */
    uint32_t supervisor_id;
} trust_operator_t;

typedef struct trust_factor {
    uint32_t kind;         /* trust_factor_kind_t */
    uint32_t operator_id;
    uint8_t  verified;     /* фактор проверен вызывающим */
} trust_factor_t;

typedef struct trust_auth_policy {
    /* Классы эффекта (маска по trust_effect_class_t из trust_attest.h),
     * требующие аппаратного фактора. */
    uint32_t hw_token_required_mask;
    /* Классы, требующие правила двух человек. */
    uint32_t two_person_mask;
    /* Минимум проверенных факторов вообще. */
    uint32_t min_factors;
} trust_auth_policy_t;

typedef enum trust_auth_outcome {
    TRUST_AUTH_DENIED   = 0,
    TRUST_AUTH_GRANTED  = 1,
    /* Внешне неотличим от GRANTED. Отличается тем, что делает система. */
    TRUST_AUTH_DURESS   = 2,
    TRUST_AUTH_OUT_MAX  = 3
} trust_auth_outcome_t;

typedef enum trust_auth_mode {
    TRUST_MODE_NORMAL     = 0,
    TRUST_MODE_RESTRICTED = 1   /* режим под принуждением */
} trust_auth_mode_t;

typedef struct trust_auth_result {
    uint32_t outcome;          /* trust_auth_outcome_t */
    uint32_t mode;             /* trust_auth_mode_t */
    uint32_t n_verified;
    uint32_t n_hw;
    uint32_t n_distinct_operators;
    /* Внутренняя причина. НЕ показывается принуждающему: для DURESS она
     * содержит правду, а видимый ответ берётся из trust_auth_visible(). */
    char     internal_reason[TRUST_OP_REASON_MAX];
} trust_auth_result_t;

const char *trust_factor_kind_name(uint32_t k);
const char *trust_auth_outcome_name(uint32_t o);
const char *trust_auth_mode_name(uint32_t m);

/* Является ли фактор аппаратным (владение, а не знание). */
int trust_factor_is_hardware(uint32_t kind);

/* Независимы ли два оператора для правила двух человек. */
int trust_operators_independent(const trust_operator_t *a,
                                const trust_operator_t *b);

/* Решение о допуске. effect_class — из trust_attest.h.
 * Возвращает outcome; подробности в out. */
uint32_t trust_op_authorize(const trust_operator_t *ops, size_t n_ops,
                            const trust_factor_t *factors, size_t n_factors,
                            const trust_auth_policy_t *pol,
                            uint32_t effect_class,
                            const char *action,
                            trust_auth_result_t *out);

/* Видимый ответ оператору. Для GRANTED и DURESS возвращает ОДНУ И ТУ ЖЕ
 * строку — это и есть требование E3-AUTH-02. Никогда не NULL. */
const char *trust_auth_visible(const trust_auth_result_t *r);

/* Что системе фактически разрешено после этого решения: под принуждением
 * — ограниченный набор. Возвращает маску разрешённых классов эффекта. */
uint32_t trust_auth_allowed_effects(const trust_auth_result_t *r,
                                    uint32_t requested_class);
include/platx/trust_suite.h
/* platx/trust_suite.h — E3-CRY: криптоагильность как именованный контракт.
 *
 * ТЗ TZ_PLATX_SEL4_TEE_V2.md §14. Требования E3-CRY-01…05.
 *
 * Одна мысль держит весь файл: набор алгоритмов — это ИМЯ, а не набор
 * умолчаний, разбросанных по коду. Имя согласуется в handshake, печатается
 * в evidence и проверяется годы спустя тем, кто не видел этой сборки.
 *
 * Отсюда три правила, которые здесь и закодированы:
 *
 *   1. Отсутствие требуемого suite — ОТКАЗ, а не подмена (E3-CRY-01).
 *      Тихий откат на «что-нибудь совместимое» — это способ, которым
 *      гибридная защита превращается в классическую без единой записи
 *      в журнале.
 *
 *   2. Объявленный suite ≠ доступный suite. ML-KEM, названный в таблице,
 *      не появляется в бинаре от того, что его назвали. Каждый слот несёт
 *      признак реализованности в ЭТОЙ сборке, и недоступный слот делает
 *      весь suite невыбираемым. ГОСТ-слот (E3-CRY-04) существует именно
 *      так: имя есть, реализации нет, подмены не будет.
 *
 *   3. Гибрид — это «обе части обязательны» (E3-CRY-02/03). Suite, у
 *      которого postquantum-слот пуст, не является выполнением E3-CRY,
 *      как бы он ни назывался. Проверка вынесена в отдельный предикат,
 *      чтобы политика могла требовать гибрид, а не надеяться на него.
 *
 * Домен самодостаточен: заголовок не тянет ничего из платформы, кроме
 * stdint/stddef. Это проверяемое свойство, а не удобство — контур
 * доверия, зависящий от половины дерева, доверием не является.
 *
 * ABI: TRUST_SUITE_ABI 1.
 */

#define TRUST_SUITE_ABI        1u
#define TRUST_SUITE_NAME_MAX   32u
#define TRUST_SUITE_MAX        16u   /* сколько suite держит реестр */
#define TRUST_OFFER_MAX        16u   /* длина списка предложения */

/* ── Классы алгоритмических слотов ───────────────────────────────────── */
typedef enum trust_alg_class {
    TRUST_ALG_KEM_CLASSICAL = 0,
    TRUST_ALG_KEM_PQ        = 1,
    TRUST_ALG_SIG_CLASSICAL = 2,
    TRUST_ALG_SIG_PQ        = 3,
    TRUST_ALG_AEAD          = 4,
    TRUST_ALG_HASH          = 5,
    TRUST_ALG_KDF           = 6,
    TRUST_ALG_CLASS_MAX     = 7
} trust_alg_class_t;

/* Конкретные алгоритмы. Значения фиксируются: они уходят в evidence и
 * в согласование с чужой сборкой. Переиспользовать освободившееся
 * значение нельзя — старое evidence станет читаться неверно. */
typedef enum trust_alg {
    TRUST_ALG_NONE        = 0,

    TRUST_ALG_X25519      = 10,
    TRUST_ALG_P256_ECDH   = 11,

    TRUST_ALG_MLKEM768    = 20,
    TRUST_ALG_MLKEM1024   = 21,

    TRUST_ALG_ED25519     = 30,
    TRUST_ALG_P256_ECDSA  = 31,
    TRUST_ALG_GOST3410    = 32,   /* ГОСТ Р 34.10-2012 — слот E3-CRY-04 */

    TRUST_ALG_MLDSA65     = 40,
    TRUST_ALG_MLDSA87     = 41,
    TRUST_ALG_FALCON1024  = 42,

    TRUST_ALG_CHACHA20POLY1305 = 50,
    TRUST_ALG_AES256GCM        = 51,
    TRUST_ALG_KUZNYECHIK_MGM   = 52,   /* ГОСТ-слот */

    TRUST_ALG_SHA256      = 60,
    TRUST_ALG_SHA512      = 61,
    TRUST_ALG_GOST3411    = 62,   /* ГОСТ Р 34.11-2012 — слот E3-CRY-04 */

    TRUST_ALG_HKDF_SHA256 = 70,
    TRUST_ALG_KDF_GOST    = 71
} trust_alg_t;

/* ── Признаки suite ──────────────────────────────────────────────────── */
/* Обе половины обмена ключами присутствуют и обязательны. */
#define TRUST_SUITE_F_HYBRID_KEM   (1u << 0)
/* Обе половины подписи присутствуют и обязательны. */
#define TRUST_SUITE_F_HYBRID_SIG   (1u << 1)
/* Suite объявлен как контракт, но в этой сборке не реализован. */
#define TRUST_SUITE_F_DECLARED_ONLY (1u << 2)
/* Suite оставлен для совместимости и не должен выбираться новой политикой. */
#define TRUST_SUITE_F_LEGACY       (1u << 3)
/* Национальный набор: отдельная сертификационная линия. */
#define TRUST_SUITE_F_NATIONAL     (1u << 4)

/* ── Описание suite ──────────────────────────────────────────────────── */
typedef struct trust_suite {
    uint32_t    abi_version;
    uint32_t    struct_size;

    uint32_t    id;                              /* стабильный идентификатор */
    char        name[TRUST_SUITE_NAME_MAX];      /* печатается в evidence */
    uint32_t    version;                         /* редакция набора */
    uint32_t    flags;

    trust_alg_t alg[TRUST_ALG_CLASS_MAX];        /* по слоту на класс */
} trust_suite_t;

/* ── Политика выбора ─────────────────────────────────────────────────── */
typedef struct trust_suite_policy {
    /* Требовать гибридный обмен ключами (E3-CRY-02). */
    uint8_t  require_hybrid_kem;
    /* Требовать гибридную подпись (E3-CRY-03). */
    uint8_t  require_hybrid_sig;
    /* Запретить legacy-наборы даже при совпадении. */
    uint8_t  forbid_legacy;
    /* Разрешить национальный набор (отдельное решение владельца). */
    uint8_t  allow_national;
    /* Минимальная редакция набора; 0 — без ограничения. */
    uint32_t min_version;
} trust_suite_policy_t;

/* ── Результат согласования ──────────────────────────────────────────── */
typedef enum trust_neg_rc {
    TRUST_NEG_OK              = 0,
    TRUST_NEG_NO_COMMON       = 1,  /* пересечение пусто */
    TRUST_NEG_POLICY_REFUSED  = 2,  /* пересечение есть, политика не берёт */
    TRUST_NEG_UNAVAILABLE     = 3,  /* выбран бы, но не реализован в сборке */
    TRUST_NEG_EINVAL          = 4
} trust_neg_rc_t;

#define TRUST_REASON_MAX 128u

typedef struct trust_neg_result {
    trust_neg_rc_t rc;
    uint32_t       suite_id;                  /* валиден только при OK */
    char           suite_name[TRUST_SUITE_NAME_MAX];
    char           reason[TRUST_REASON_MAX];  /* всегда заполнен */
    /* Сколько наборов отсеяно каждой причиной — для честного объяснения,
     * почему «совместимости не нашлось». Без этого отказ выглядит как
     * сбой сети, и первым же решением станет ослабить политику. */
    uint32_t       n_common;
    uint32_t       n_dropped_policy;
    uint32_t       n_dropped_unavailable;
} trust_neg_result_t;

/* ── Реестр ──────────────────────────────────────────────────────────── */

/* Встроенные наборы. Возвращает число наборов в реестре. */
size_t trust_suite_count(void);

/* Набор по индексу [0, count). NULL вне диапазона. */
const trust_suite_t *trust_suite_at(size_t idx);

/* Набор по id / по имени. NULL, если нет. */
const trust_suite_t *trust_suite_by_id(uint32_t id);
const trust_suite_t *trust_suite_by_name(const char *name);

/* Имя алгоритма для печати. Никогда не NULL ("unknown" для чужого). */
const char *trust_alg_name(trust_alg_t a);
const char *trust_alg_class_name(trust_alg_class_t c);

/* Реализован ли алгоритм в ЭТОЙ сборке. Ровно этот предикат отделяет
 * «мы поддерживаем ML-KEM» от «мы написали ML-KEM в таблице». */
int trust_alg_available(trust_alg_t a);

/* Доступен ли набор целиком: все непустые слоты реализованы и сам набор
 * не помечен DECLARED_ONLY. */
int trust_suite_available(const trust_suite_t *s);

/* Гибридность — предикат, а не название. */
int trust_suite_is_hybrid_kem(const trust_suite_t *s);
int trust_suite_is_hybrid_sig(const trust_suite_t *s);

/* Проходит ли набор политику (без учёта доступности). */
int trust_suite_policy_ok(const trust_suite_t *s,
                          const trust_suite_policy_t *pol,
                          char *why, size_t whylen);

/* ── Согласование ────────────────────────────────────────────────────── */
/* Пересечение двух предложений под политикой. Порядок предпочтения задаёт
 * local: первый общий набор, прошедший политику И доступный, выигрывает.
 *
 * Отказ никогда не превращается в выбор другого набора: если политика
 * требует гибрид, а общим оказался только классический, результат —
 * TRUST_NEG_POLICY_REFUSED, и вызывающий обязан прекратить обмен. */
trust_neg_rc_t trust_suite_negotiate(const uint32_t *local, size_t n_local,
                                     const uint32_t *peer,  size_t n_peer,
                                     const trust_suite_policy_t *pol,
                                     trust_neg_result_t *out);

/* ── Печать в evidence (E3-CRY-01) ───────────────────────────────────── */
/* Одна строка вида:
 *   suite=PX-HYBRID-1 v1 kem=X25519+ML-KEM-768 sig=Ed25519+ML-DSA-65 \
 *   aead=ChaCha20-Poly1305 hash=SHA-256 kdf=HKDF-SHA256 hybrid=kem,sig
 * Возвращает число записанных байт (без NUL) или -1. */
int trust_suite_evidence_line(const trust_suite_t *s, char *out, size_t outlen);

/* ── Долговременная валидность подписи (E3-CRY-05) ───────────────────── */
typedef enum trust_validity {
    TRUST_VALIDITY_VALID       = 0,
    TRUST_VALIDITY_GRACE       = 1,  /* срок вышел, действует grace-период */
    TRUST_VALIDITY_EXPIRED     = 2,
    TRUST_VALIDITY_NO_TIMESTAMP = 3, /* нет доверенной отметки времени */
    TRUST_VALIDITY_FUTURE      = 4   /* подписано «в будущем» — отказ */
} trust_validity_t;

typedef struct trust_ts_policy {
    uint64_t lifetime_s;     /* срок действия подписи */
    uint64_t grace_s;        /* сколько ещё принимать с пометкой GRACE */
    uint8_t  require_trusted_source; /* без доверенного источника — отказ */
    uint64_t max_skew_s;     /* допустимое опережение часов */
} trust_ts_policy_t;

typedef struct trust_timestamp {
    uint64_t signed_at_s;
    uint8_t  source_trusted; /* 1 — TSA/якорь, 0 — часы подписанта */
    uint32_t source_id;
} trust_timestamp_t;

trust_validity_t trust_sig_validity(const trust_timestamp_t *ts,
                                    uint64_t now_s,
                                    const trust_ts_policy_t *pol);

const char *trust_validity_name(trust_validity_t v);
const char *trust_neg_rc_name(trust_neg_rc_t rc);
include/platx/trust_witness.h
/* platx/trust_witness.h — E3-WIT: ось наблюдения.
 *
 * ТЗ TZ_PLATX_SEL4_TEE_V2.md §8, требования E3-WIT-01…04.
 *
 * Наблюдение несёт не только «что видно», но и «откуда смотрели». Без
 * второго первое стоит ровно столько, сколько стоит честность того, кого
 * как раз и проверяют: гость, рассказывающий о себе, — это не источник
 * сведений о госте, это его заявление.
 *
 * Четыре уровня, по возрастанию независимости от проверяемого:
 *   GUEST_TELEMETRY      — изнутри проверяемой системы;
 *   HYPERVISOR_VMI       — снизу, но с семантическим разрывом;
 *   INDEPENDENT_HARDWARE — сбоку (BMC, TPM event-log, watchdog);
 *   EXTERNAL_VERIFIER    — снаружи, вне досягаемости обеих сторон.
 *
 * Три правила:
 *
 *   E3-WIT-01. Вердикт не сильнее слабейшего наблюдателя в обосновании.
 *      Правило `weakest` уже действует в src/fabric/witness_coord.c для
 *      согласованности снимка; здесь оно распространено на силу вывода.
 *      Одно наблюдение гостя в обосновании опускает весь вывод до уровня
 *      гостя, сколько бы независимых наблюдений ни стояло рядом.
 *
 *   E3-WIT-02. Расхождение двух наблюдателей об одном факте — отдельный
 *      класс находки WITNESS_CONFLICT. Не «уточнение», не «более
 *      надёжный источник победил»: если гипервизор видит процесс, а гость
 *      его не показывает, ценность — в самом расхождении. Тихая замена
 *      одного показания другим стирает единственную улику руткита.
 *
 *   E3-WIT-03/04. Гипервизорное наблюдение обязано явно зафиксировать
 *      консистентность снимка (иначе семантический разрыв VMI не учтён и
 *      уровень понижается), а аппаратный наблюдатель обязан быть оформлен
 *      как provider с owner/generation/scope/deadline и поставлять улику,
 *      а не решение.
 */

#define TRUST_WIT_ABI          1u
#define TRUST_WIT_SUBJECT_MAX  64u
#define TRUST_WIT_FACT_MAX     48u
#define TRUST_WIT_OWNER_MAX    32u
#define TRUST_WIT_SCOPE_MAX    32u
#define TRUST_WIT_JUST_MAX     16u   /* наблюдений в обосновании */
#define TRUST_WIT_REASON_MAX   192u

typedef enum trust_witness_level {
    TRUST_WIT_NONE                 = 0,  /* негодное наблюдение */
    TRUST_WIT_GUEST_TELEMETRY      = 1,
    TRUST_WIT_HYPERVISOR_VMI       = 2,
    TRUST_WIT_INDEPENDENT_HARDWARE = 3,
    TRUST_WIT_EXTERNAL_VERIFIER    = 4,
    TRUST_WIT_LEVEL_MAX            = 5
} trust_witness_level_t;

/* Адресное пространство, в котором сделано наблюдение. Разрыв между
 * гостевым виртуальным адресом и физической страницей — это то место,
 * где VMI перестаёт быть «взглядом снизу» и становится догадкой. */
typedef enum trust_addr_space {
    TRUST_AS_UNSPEC     = 0,
    TRUST_AS_GUEST_VIRT = 1,
    TRUST_AS_GUEST_PHYS = 2,
    TRUST_AS_HOST_PHYS  = 3
} trust_addr_space_t;

/* Совпадает по смыслу с witness_consistency_t из witness_provider.h.
 * Дублирование значений намеренно: домен доверия не тянет плоскость
 * провайдеров, но обязан говорить о консистентности теми же словами. */
typedef enum trust_snap_cons {
    TRUST_CONS_OFFLINE     = 0,
    TRUST_CONS_BEST_EFFORT = 1,
    TRUST_CONS_COORDINATED = 2,
    TRUST_CONS_ATOMIC      = 3
} trust_snap_cons_t;

typedef struct trust_observation {
    uint32_t level;                          /* trust_witness_level_t */
    char     subject[TRUST_WIT_SUBJECT_MAX]; /* о чём наблюдение */
    char     fact[TRUST_WIT_FACT_MAX];       /* какой именно факт */
    uint64_t value;                          /* значение факта */
    uint8_t  value_known;
    uint64_t observed_at_ns;

    /* E3-WIT-03 */
    uint32_t addr_space;          /* trust_addr_space_t */
    uint32_t consistency;         /* trust_snap_cons_t */
    uint8_t  consistency_stated;  /* зафиксирована явно, а не «подразумевается» */

    /* E3-WIT-04: оформление наблюдателя как provider */
    char     owner[TRUST_WIT_OWNER_MAX];
    uint64_t generation;
    char     scope[TRUST_WIT_SCOPE_MAX];
    uint64_t deadline_ns;
    uint8_t  is_decision;         /* 1 — наблюдатель выдал решение: запрещено */
} trust_observation_t;

/* Классы находок. WITNESS_CONFLICT — самостоятельный класс (E3-WIT-02). */
typedef enum trust_finding_class {
    TRUST_FIND_NONE             = 0,
    TRUST_FIND_OBSERVATION      = 1,
    TRUST_FIND_WITNESS_CONFLICT = 2,
    TRUST_FIND_CLASS_MAX        = 3
} trust_finding_class_t;

typedef struct trust_finding {
    uint32_t            cls;                 /* trust_finding_class_t */
    char                subject[TRUST_WIT_SUBJECT_MAX];
    char                fact[TRUST_WIT_FACT_MAX];
    uint32_t            n_just;
    trust_observation_t just[TRUST_WIT_JUST_MAX];
    /* Заполняется trust_finding_strength(). Хранится, чтобы вывод и его
     * сила ездили вместе: сила, вычисляемая заново каждым потребителем,
     * рано или поздно вычисляется по-разному. */
    uint32_t            strength;
    char                reason[TRUST_WIT_REASON_MAX];
} trust_finding_t;

const char *trust_witness_level_name(uint32_t l);
const char *trust_addr_space_name(uint32_t a);
const char *trust_snap_cons_name(uint32_t c);
const char *trust_finding_class_name(uint32_t c);

/* Действительный уровень наблюдения после проверок E3-WIT-03/04.
 * Может быть НИЖЕ заявленного; TRUST_WIT_NONE означает, что наблюдение
 * негодно и в обосновании участвовать не может.
 * why (может быть NULL) получает причину понижения. */
uint32_t trust_obs_effective_level(const trust_observation_t *o,
                                   char *why, size_t whylen);

void trust_finding_init(trust_finding_t *f, uint32_t cls,
                        const char *subject, const char *fact);
int  trust_finding_add(trust_finding_t *f, const trust_observation_t *o);

/* Сила вывода = минимум действительных уровней обоснования (E3-WIT-01).
 * Пустое обоснование даёт TRUST_WIT_NONE: вывод без наблюдений — мнение. */
uint32_t trust_finding_strength(trust_finding_t *f);

/* Допустим ли вывод там, где требуется уровень не ниже required. */
int trust_finding_admissible(trust_finding_t *f, uint32_t required_level,
                             char *why, size_t whylen);

/* E3-WIT-02. Если два наблюдения об одном субъекте и факте расходятся,
 * заполняет out находкой класса WITNESS_CONFLICT (оба наблюдения внутри)
 * и возвращает 1. Если расхождения нет — 0, out не трогается.
 * Победитель не выбирается никогда: это не задача этой функции и не
 * задача этого слоя. */
int trust_witness_conflict(const trust_observation_t *a,
                           const trust_observation_t *b,
                           trust_finding_t *out);
include/platx/trust_zeroize_scan.h
/* platx/trust_zeroize_scan.h — E3-ZERO: проверяемый zeroize.
 *
 * ТЗ TZ_PLATX_SEL4_TEE_V2.md §18, требования E3-ZERO-01…03.
 *
 * Этот файл НЕ повторяет src/core/plat_zeroize.c. Тот модуль ведёт
 * инвентарь подконтрольных областей и честно различает «записал нули» и
 * «перечитал и увидел нули» (PLAT_ZX_OUT_WRITTEN против OUT_VERIFIED).
 * И он же прямо оговаривает, чего не обещает: ничего про копии, которых
 * не видит, — страницы ядра, буферы аллокатора, срезы в чужих стеках.
 *
 * Ровно эту дыру и закрывает E3-ZERO-01: после teardown область
 * СКАНИРУЕТСЯ на остатки. Инвентарь отвечает на вопрос «что я затёр»,
 * скан — на вопрос «что осталось». Второй вопрос интереснее.
 *
 * Секрет для поиска не хранится: хранится его отпечаток (SHA-256 плюс
 * длина). Хранить сам секрет ради проверки, что секрета не осталось, —
 * это способ гарантированно оставить одну копию.
 *
 * Стоимость честная: скан хэширует каждое окно длиной с секрет. Для
 * арены в мегабайт и секрета в 32 байта это около миллиона SHA-256.
 * Дешевле — только эвристики, которые пропускают ровно тот случай, ради
 * которого скан и заводят.
 *
 * E3-ZERO-02 (sealing к PCR) и E3-ZERO-03 (ложное срабатывание как
 * самостоятельный класс отказа) — здесь же.
 */

#define TRUST_ZS_ABI          1u
#define TRUST_ZS_FP_MAX       16u    /* отпечатков в одном скане */
#define TRUST_ZS_HIT_MAX      32u
#define TRUST_ZS_TAG_MAX      32u
#define TRUST_ZS_REASON_MAX   224u
#define TRUST_ZS_SECRET_MIN   8u     /* короче — слишком много ложных */
#define TRUST_ZS_SECRET_MAX   256u

/* Отпечаток секрета, который ДОЛЖЕН отсутствовать. */
typedef struct trust_secret_fp {
    char         tag[TRUST_ZS_TAG_MAX];
    trust_hash_t digest;    /* SHA-256 самого секрета */
    uint32_t     len;       /* его длина: без неё окно не определено */
} trust_secret_fp_t;

typedef struct trust_zs_hit {
    char     tag[TRUST_ZS_TAG_MAX];
    uint64_t offset;        /* смещение в сканируемой области */
    uint32_t len;
} trust_zs_hit_t;

typedef struct trust_zs_result {
    uint32_t       scanned_bytes;
    uint32_t       n_fingerprints;
    uint32_t       n_hits;
    trust_zs_hit_t hit[TRUST_ZS_HIT_MAX];
    /* Скан выполнен целиком. Прерванный скан НЕ является доказательством
     * чистоты — и это отдельное поле, а не «ноль попаданий». */
    uint8_t        complete;
    char           reason[TRUST_ZS_REASON_MAX];
} trust_zs_result_t;

/* Построить отпечаток. Секрет после этого можно (и нужно) затереть. */
int trust_secret_fp_make(trust_secret_fp_t *out, const char *tag,
                         const uint8_t *secret, size_t len);

/* E3-ZERO-01. Сканирует область на присутствие любого из отпечатков.
 * Возвращает число найденных остатков (0 — чисто), -1 при ошибке входа.
 * Ноль попаданий при complete == 0 чистотой не является. */
int trust_zeroize_scan(const void *arena, size_t arena_len,
                       const trust_secret_fp_t *fps, size_t n_fps,
                       trust_zs_result_t *out);

/* Затирание с последующей проверкой перечитыванием. Возвращает 1, если
 * область после записи действительно читается нулями. */
int trust_zeroize_and_verify(void *p, size_t len);

/* ── E3-ZERO-02: sealing к политике PCR ──────────────────────────────── */
typedef struct trust_seal_policy {
    uint32_t     select[TRUST_PCR_SELECT_MAX];
    uint32_t     n_select;
    trust_hash_t expected;   /* свёртка выбранных PCR при запечатывании */
    uint8_t      set;
} trust_seal_policy_t;

typedef enum trust_seal_rc {
    TRUST_SEAL_OK          = 0,  /* измерения сошлись, ключ доступен */
    TRUST_SEAL_MISMATCH    = 1,  /* не сошлись — ключ недоступен */
    TRUST_SEAL_NO_POLICY   = 2,  /* политика не задана — тоже отказ */
    TRUST_SEAL_EINVAL      = 3
} trust_seal_rc_t;

/* Запечатать к текущему состоянию: политика запоминает свёртку. */
trust_seal_rc_t trust_seal_bind(trust_seal_policy_t *pol,
                                const trust_pcr_bank_t *bank,
                                const uint32_t *select, size_t n_select);

/* Попытка распечатать при текущих измерениях. Несовпадение делает ключ
 * недоступным АВТОМАТИЧЕСКИ — без участия того, кого проверяют. */
trust_seal_rc_t trust_seal_unseal(const trust_seal_policy_t *pol,
                                  const trust_pcr_bank_t *bank,
                                  char *why, size_t whylen);

const char *trust_seal_rc_name(trust_seal_rc_t rc);

/* ── E3-ZERO-03: ложное срабатывание ─────────────────────────────────── */

/* Источник сигнала на затирание. Уровень наблюдателя здесь тот же, что в
 * trust_witness.h: сигнал от гостевой телеметрии и сигнал от аппаратного
 * датчика вскрытия — разные вещи, и путать их нельзя. */
typedef struct trust_zero_trigger {
    char     source[TRUST_ZS_TAG_MAX];
    uint32_t witness_level;      /* trust_witness_level_t */
    uint32_t corroborations;     /* сколько независимых подтверждений */
    uint8_t  self_test_passed;   /* датчик прошёл самопроверку */
} trust_zero_trigger_t;

typedef enum trust_zero_verdict {
    TRUST_ZERO_CONFIRMED = 0,   /* затирать */
    TRUST_ZERO_FALSE     = 1,   /* ложное срабатывание — класс отказа */
    TRUST_ZERO_UNKNOWN   = 2    /* нельзя решить: не затирать, поднять тревогу */
} trust_zero_verdict_t;

typedef struct trust_zero_policy {
    uint32_t min_witness_level;   /* ниже — сигналу не верим */
    uint32_t min_corroborations;  /* сколько подтверждений требуется */
    uint8_t  require_self_test;
} trust_zero_policy_t;

/* Классификация сигнала ДО затирания. Затирание необратимо: решение,
 * принятое по одному непроверенному датчику, — это отказ в обслуживании,
 * который противник получает бесплатно. */
trust_zero_verdict_t trust_zero_classify(const trust_zero_trigger_t *t,
                                         const trust_zero_policy_t *pol,
                                         char *why, size_t whylen);

/* Путь восстановления после ложного срабатывания. Возвращает 1, если
 * служба может быть восстановлена. Ключевой материал НЕ восстанавливается
 * никогда: восстановление сервиса и восстановление ключей — разные вещи,
 * и вторая делает первый zeroize бессмысленным. */
typedef struct trust_zero_recovery {
    uint8_t service_restored;
    uint8_t keys_restored;       /* обязан остаться 0 */
    uint8_t rekey_required;
    char    reason[TRUST_ZS_REASON_MAX];
} trust_zero_recovery_t;

int trust_zero_recover(trust_zero_verdict_t verdict,
                       trust_zero_recovery_t *out);

const char *trust_zero_verdict_name(trust_zero_verdict_t v);
56

txp

Квоты, ограничения и вспомогательная транспортная политика
src/txp/Связь и ввод-вывод1 файлов1 API headers

TXP содержит quota/accounting/policy primitives для transport/TXPStack experiments. Его задача — обеспечить bounded sessions/bytes/packets/rate и owner cleanup, не дублируя routing или generic resource registry.

Граница ответственности

  • Quota key включает owner/generation и class.
  • Accounting uses monotonic counters; policy decision explicit.
  • Raw network logic остаётся TXPStack.

Устройство подсистемы

  • Quota policy immutable generation: limits concurrency, memory, rate, burst, duration.
  • Bucket/counter record owner scoped and released at teardown.
  • Admission API reserve/commit/release supports transaction.

Поток работы

  • Operation estimates/reserves quota.
  • Resource/session start commits.
  • Usage charges/refunds.
  • Stop/failure owner release.

Отказ и восстановление

  • Counter overflow saturated/reject.
  • Owner crash bulk release with reconciliation actual resources.
  • Policy reload old records keep explicit version/migrate.
  • Concurrent reserve atomic.

Основные возможности

  • Tracks transport-oriented quotas and limits.
  • Provides policy support to network experiments.
  • Separates accounting from raw stack implementation.

Управление и диагностика

Домен не регистрирует собственную корневую команду. Его контракт используется вызывающими подсистемами через API и интерфейсы transports, txpstack.

Справочник CLI / txp →
Состав подсистемы / 1 файлов
Файл / компонентНазначение и граница
src/txp/txp_quota.cTXPStack resource quotas and TAP/raw fd cleanup. STAB-128: limits on stacks, routes, NAT; tap_fd closed on teardown.
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/txp.h
/* platx/txp.h — TXPStack: квоты ресурсов и снос стека (STAB-128).
 *
 * Контракт существовал только в src/txp/txp_quota.c: файл включал
 * "txp_quota.h", которого в дереве нет ни в одном каталоге, поэтому не
 * компилировался никогда. Пределы жили в .c рядом с массивами, объявленными
 * где-то ещё, — расхождение между границей проверки и размером массива
 * ничем не ловилось. Здесь предел и размер массива берутся из одного места.
 */
/* (a) одновременных сетевых стеков */
#define TXP_STACK_LIMIT   64u
/* (b) маршрутов на стек */
#define TXP_ROUTE_LIMIT  512u
/* (c) записей NAT суммарно */
#define TXP_NAT_LIMIT   4096u

typedef struct {
    uint32_t dst;
    uint32_t mask;
    uint32_t gw;
    uint32_t ifindex;
} txp_route_t;

typedef struct {
    uint32_t orig_ip;
    uint32_t nat_ip;
    uint16_t orig_port;
    uint16_t nat_port;
    uint8_t  proto;
    uint8_t  _pad[3];
} txp_nat_entry_t;

typedef struct {
    /* fd держатся как -1 в свободном состоянии: 0 — валидный дескриптор,
     * и «ещё не открыт» им обозначать нельзя. */
    int         tap_fd;
    int         raw_fd;
    uint32_t    n_routes;
    txp_route_t routes[TXP_ROUTE_LIMIT];
} txp_stack_t;

typedef struct {
    uint32_t        n_stacks;
    uint32_t        n_nat;
    txp_nat_entry_t nat[TXP_NAT_LIMIT];
} txp_ctx_t;

/* Все три возвращают 0 либо -ENOSPC при исчерпании соответствующего предела.
 * Полусостояния нет: при отказе счётчик не двигается и запись не пишется. */
int  txp_stack_create(txp_ctx_t *ctx);
int  txp_route_add(txp_stack_t *st, const txp_route_t *r);
int  txp_nat_add(txp_ctx_t *ctx, const txp_nat_entry_t *e);

/* Закрывает tap_fd/raw_fd и ставит их в -1. Идемпотентна. */
void txp_stack_teardown(txp_stack_t *st);
57

txpstack

Собственный сетевой стек и лаборатория сетевого обмена
src/txpstack/Связь и ввод-вывод7 файлов0 API headers

TXPStack — userspace/custom raw network laboratory с interfaces, ARP/IP/TCP behavior, routes/NAT/netem/capture/debug. Он демонстрирует сложный data plane поверх PLATX ownership/profile isolation, но не входит в trusted product Core.

Граница ответственности

  • Только full/research/dedicated lab profile и explicit CAP_NET_RAW/NET_ADMIN preflight.
  • Control lifecycle через descriptor/operations; packet fast path не держит Core locks.
  • Kernel stack coexistence/namespace documented; no silent host network mutation.

Устройство подсистемы

  • Stack instance owns interface backend loopback/TAP/AF_PACKET, protocol tables, routes/NAT, sessions, timers, capture и quotas.
  • Single frame ingress/egress choke points feed decode/protocol/netem/capture.
  • Protocol timers use scheduler/timer wheel with bounded tables.
  • Configuration compiles immutable generation; existing sessions migration policy explicit.

Поток работы

  • Create/preflight interface → claim fds/tasks.
  • RX frame → validate L2/L3/L4 → session/protocol.
  • TX packet → netem/routing → backend emit.
  • Stop → block RX/TX → drain/cancel sessions → close backend.

Отказ и восстановление

  • Kernel parallel response avoided via namespace/free IP/filter plan.
  • Malformed packet never overreads; checksum/length validation.
  • Table/session/timer full deterministic drop/reject.
  • Backend loss/reload no dangling sessions or raw fd.

Основные возможности

  • Custom interface/session/route/NAT and raw packet handling.
  • Extensive TCP/IP behavior knobs: fragmentation, TTL, reorder, rate, loss, CC, SACK, timestamps and more.
  • Capture/sniff/trace/debug/inspect/benchmark/export workflows.
  • Raw architecture documented separately in TXPSTACK_RAW.md.

Управление и диагностика

Корневые команды: netx, txpstack. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / txpstack →
Состав подсистемы / 7 файлов
Файл / компонентНазначение и граница
src/txpstack/cmd_netx.cдемонстрационный сетевой модуль поверх собственного стека. Namespace: "netx". Показывает, как ЛЮБОЙ сетевой модуль пользуется стеком, не линкуясь напрямую с его реализацией: он резолвит интерфейс по имени через реестр платформы (--stack=NAME → netstack:NAME) и работает через vtable
src/txpstack/cmd_txpstack.cnamespace "txpstack" (сетевая лаборатория): управление собственным TCP/IP стеком над TAP. txpstack create --ip=IP [--netmask=M|--prefix=N] [--gateway=GW] [--name=NM] [--mac=MAC] [--mtu=N] [--loopback | --tap | --raw | --link=NAME] [--iface=IF] [--promisc]
src/txpstack/txpstack.hпубличный API подсистемы «свой TCP/IP стек» (namespace txpstack). Not an XIO resolver. Not the working path (connect → handshake → send/recv → switch → disconnect). Подсистема реализует пользовательский сетевой стек поверх TAP-устройства (/dev/net/tun, IFF_TAP): Ethernet/ARP (L2), IPv4 c фрагментацией/сборкой (L3),
src/txpstack/txpstack_flow.hпассивный анализатор соединений. Разбирает проходящий трафик и ведёт таблицу обнаруженных потоков. Ничего не отправляет, не меняет и не задерживает: пассивность — определяющее свойство, её нарушение превратило бы модуль в другой продукт с другими требованиями к безопасности.
src/txpstack/txpstack_internal.hвнутренние структуры и прототипы стека. Разделяется между txpstack.c (устройство, L2/L3, UDP, ICMP, routing, NAT, netem, цикл событий) и txpstack_tcp.c (TCP RFC 793). Не входит в публичный API. Модель конкурентности: всё состояние стека защищено единственным мьютексом
src/txpstack/txpstack_proxy.hрежим tun2socks: перехваченные соединения уходят в вышестоящий прокси. клиент ──▶ TUN/TAP ──▶ txpstack (transparent) ──▶ relay ──▶ SOCKS/HTTP ──▶ мир │ │ терминирует TCP, txpstack_socks.c: помнит orig_dst рукопожатие с прокси
src/txpstack/txpstack_socks.hклиент вышестоящего прокси: SOCKS4/4a/5 и HTTP CONNECT. Модуль реализован как конечный автомат БЕЗ ввода-вывода (sans-I/O): он не знает про сокеты, не читает и не пишет, не блокируется и не заводит потоков. Ему подают принятые байты и подставляют буфер, куда он кладёт байты для
Контракты API / 0 заголовков

Отдельный заголовок из include/platx/ не закреплён за этим доменом в справочнике. Внутренние границы перечислены в составе файлов; управление и публичные связи описаны в соседних подсистемах.

58

uipc

Unix IPC и передача файловых дескрипторов
src/uipc/Связь и ввод-вывод3 файлов2 API headers

UIPC реализует Unix-domain IPC и SCM_RIGHTS descriptor passing с валидацией, ownership transfer и небольшим CLI. Он должен стать adapter под generic transport/child IPC, а не отдельным параллельным message framework.

Граница ответственности

  • Received fd untrusted до validation/claim.
  • Message schema/version/bounds mandatory.

Устройство подсистемы

  • Endpoint instance owns socket/listener/task and peer credentials policy.
  • Frame envelope length/type/version/correlation plus optional fd descriptors metadata.
  • recvmsg validates truncation/control messages/count/type, claims each fd in temporary scope.
  • Handler commits transfer to target owner or closes all on failure.

Поток работы

  • Bind/connect/accept through XIO/resource.
  • sendmsg frame + optional duplicated/transfer fd.
  • recvmsg validate credentials/envelope/fds.
  • Dispatch/commit ownership → response.

Отказ и восстановление

  • Peer disconnect during transfer explicit unknown/receipt policy.
  • Accept/listener stop race serialized.
  • Malformed credential/schema fuzz safe.

Основные возможности

  • Unix socket server/client operations.
  • SCM_RIGHTS descriptor transfer.
  • Validation checks received descriptors and message shape.

Управление и диагностика

Домен не регистрирует собственную корневую команду. Его контракт используется вызывающими подсистемами через API и интерфейсы uring.

Справочник CLI / uipc →
Состав подсистемы / 3 файлов
Файл / компонентНазначение и граница
src/uipc/ipc_console.cUnix-socket IPC front-end for prometheus. Invoked by: ./prometheus --console=IPC_MODE Protocol (line-oriented text, intentionally debuggable with socat): Client → Server : "\n" Server → Client : "\x00" (NUL byte = end of response)
src/uipc/uipc_scm.cпроверка дескрипторов, принятых через SCM_RIGHTS. STAB-123: fd_type, O_CLOEXEC, owner_claim, MSG_CTRUNC. Контракт — platx/uipc.h. До ремонта файл включал несуществующий "uipc.h", звал две несуществующие функции и передавал xio_own_fd аргументы в обратном порядке; ничего из этого не всплывало, потому что файл не
src/uipc/uipc_transport.cUIPC as fd-transfer transport (INT-103).
Контракты API / 2 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/uipc.h
/* platx/uipc.h — приём дескрипторов через SCM_RIGHTS с проверкой (STAB-123).
 *
 * src/uipc/uipc_scm.c включал "uipc.h", которого в дереве нет. Кроме того,
 * он звал xio_own_fd(u->xio, fd) — при настоящей сигнатуре
 * xio_own_fd(int fd, plat_owner_t owner). Порядок и типы аргументов не
 * сходились, и это не всплывало ровно потому, что файл не собирался ни
 * разу. Владелец здесь — тройка (module, instance, generation), как
 * требует C8: строкой или указателем владельца не обозначают.
 */
typedef struct {
    /* Владелец, от имени которого заявляется claim на принятый fd.
     * generation == 0 — отказ, а не «поправим на 1» (C8). */
    plat_owner_t owner;
    int          sock_fd;
} uipc_t;

/* Классы дескрипторов, которые UIPC различает. Значения не являются
 * битовой маской: дескриптор принадлежит ровно одному классу. */
enum {
    UIPC_FD_OTHER   = 0,
    UIPC_FD_REGULAR = 1,
    UIPC_FD_SOCKET  = 2,
};

/* Класс дескриптора по fstat. Возвращает UIPC_FD_* либо -1 при ошибке
 * fstat (дескриптор непригоден). Не меняет состояние дескриптора. */
int uipc_fd_type(int fd);

/* Разрешён ли класс к приёму. Принимаются только обычный файл и сокет:
 * каталог, устройство и всё прочее приходит от недоверенной стороны
 * (C27) и молча не пропускается. */
int uipc_fd_type_allowed(int fd_type);

/* Принимает ровно один дескриптор из ancillary-данных msg.
 * 0 — успех, *out_fd владеет вызывающий.
 * Отказы: -EMSGSIZE (MSG_CTRUNC — ancillary обрезано), -ENODATA (нет
 * SCM_RIGHTS), -EBADF (класс не разрешён), -EPERM (claim не встал),
 * прочее — -errno. При любом отказе дескриптор закрыт и *out_fd не
 * тронут: живого fd без владельца не остаётся (C17). */
int uipc_scm_recv_fd(uipc_t *u, struct msghdr *msg, int *out_fd);
include/platx/uipc_transport.h
/* platx/uipc_transport.h — UIPC as fd-transfer transport (INT-103). */
#define PLAT_UIPC_REASON 128

typedef enum {
    PLAT_UIPC_OK     = 0,
    PLAT_UIPC_NOLINK = 1,
    PLAT_UIPC_ERROR  = 2,
} plat_uipc_status_t;

typedef struct {
    plat_uipc_status_t status;
    int   transferred_fd;   /* -1 on failure */
    char  reason[PLAT_UIPC_REASON];
} plat_uipc_result_t;

/* Send fd over UIPC socket identified by name.  0/-1. */
int plat_uipc_send_fd(const char *socket_name, int fd,
                      plat_uipc_result_t *out);

/* Receive an fd from a UIPC socket.  0/-1, out->transferred_fd set. */
int plat_uipc_recv_fd(const char *socket_name,
                      plat_uipc_result_t *out);
59

uring

Асинхронное исполнение и интеграции io_uring
src/uring/Связь и ввод-вывод14 файлов1 API headers

Uring реализует io_uring backend, sync/async/futures, registered resources и integrations. Целевая роль — backend XIO; direct CLI остаётся diagnostics/research, а consumers не должны зависеть от ring internals.

Граница ответственности

  • XIO defines common semantics; uring private structs never public service.
  • Kernel feature probe and fallback chosen by profile/XIO policy, not hidden per call.
  • Exec/silent/LKM paths research-only.

Устройство подсистемы

  • Ring instance owns fd, mappings, SQ/CQ state, registered files/buffers, event task and epoch.
  • Submission record owner/generation, opcode, resources, deadline, completion callback/future.
  • Completion validates epoch/generation, updates state once and releases pins.
  • Shutdown blocks submissions, cancels/drains with deadline, unregisters, unmaps/closes.

Поток работы

  • XIO call → capability/opcode/size validation.
  • Reserve SQ/submission record → submit syscall.
  • CQ pump → match token → complete future.
  • Cancel/close → drain/release.

Отказ и восстановление

  • SQ/CQ full backpressure ENOSPC, no overwrite.
  • Late CQE after owner restart returns stale and only releases resources.
  • Ring syscall/mmap partial failure rollback.
  • Kernel unsupported explicit mode unavailable; no semantic-changing silent fallback.

Основные возможности

  • Synchronous and asynchronous read/write/copy/network operations.
  • Future/pending/wait/stats model.
  • Registered resources plus memfd/vault/IPC integrations.
  • Execution and silent/LKM test paths are research-oriented.
Архитектурные детали и инварианты

Назначение

Весь файловый и сетевой I/O EDR-сенсора идёт через io_uring, минуя блокирующие вызовы.

Реализация

- Прямые syscall (__NR_io_uring_setup, __NR_io_uring_enter) без liburing.

- SQ/CQ кольца монтируются через mmap на адрес из io_uring_params.

- Статический пул из **4 экземпляров** platx_uring_t (§SEC-3).

- При неудаче io_uring_setup — graceful fallback: ring_fd = -1, счётчик fallback_epoll.

Управление и диагностика

Корневые команды: uring, uipc. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / uring →
Состав подсистемы / 14 файлов
Файл / компонентНазначение и граница
src/uring/cmd_uring.cасинхронный ввод-вывод поверх Linux io_uring. Namespace: "uring". Движок общается с ядром НАПРЯМУЮ через системные вызовы io_uring_setup(2) / io_uring_enter(2) / io_uring_register(2) — без liburing. Кольца SQ/CQ и массив SQE отображаются через mmap(2); отправка операций и
src/uring/uring_async.cасинхронный движок io_uring: машина состояний (фаза 3b). См. uring_async.h. Слой submit-backend подключаемый: по умолчанию — встроенный синхронный fallback (mock-эхо), пригодный для автономной работы и тестов без реального кольца. Реальное кольцо подключается через
src/uring/uring_async.hасинхронный движок io_uring: машина состояний. Движок разделён на два слоя. 1) Машина состояний (этот модуль): реестр операций, callback-слой, submit/process/run_until_idle/cancel/status. Не зависит от того, как физически исполняется операция. 2) submit-backend (подключаемый): как операция реально выполняется.
src/uring/uring_exec.casync exec из памяти (фаза 6). См. uring_exec.h.
src/uring/uring_exec.hasync exec: запуск ELF/.so из памяти без файла на диске. fexecve / execveat из memfd (без файла на диске), либо dlopen для .so. Модель задач: неблокирующий (--bg) запуск возвращает id; состояние/отмена — uring_exec_status / uring_exec_cancel. Самодостаточно (не зависит от кольца) — исполнение это обычные syscall,
src/uring/uring_file.cРеализация uring / file
src/uring/uring_hl.cКомпозиция open + async READ/WRITE + close поверх uring_async. Открытие/ закрытие — синхронные (дёшево), данные идут через async-движок (реальное кольцо, если привязано, иначе синхронный fallback).
src/uring/uring_if.hпубличный контракт кирпича "uring" (капабилити-интерфейс). Потребители (vault, memfd, ipc, ebpf-транспорты) работают с асинхронным бэкендом io_uring ТОЛЬКО через этот vtable, полученный из platform_require(URING_IF_NAME, ...). Прямая C-линковка с cmd_uring.c не
src/uring/uring_io.hthe uring tree's one door for typed I/O. Private to src/uring. Shared by uring_hl.c, cmd_uring.c, uring_sync.c, uring_exec.c and uring_ipc.c, the same way vault_io.h and dsl_io.h carry theirs: static inline, one rule, five translation units. All eight are here because between the five of them all eight are performed.
src/uring/uring_ipc.cасинхронный IPC поверх io_uring (namespace "uipc"). Классический IPC-сервер выполняет отдельные системные вызовы, каждый из которых виден в strace по отдельности: socket() → bind() → listen() → accept() → recv() → send() → close() Здесь фаза установки (socket/bind/listen/connect) остаётся обычной, а
src/uring/uring_silent.csilent-режим uring (фаза 6). См. uring_silent.h.
src/uring/uring_silent.hsilent-режим uring: jitter + детекция трассировки. Если TracerPid > 0, операции не идут через кольцо: force_sync, чтобы I/O не размазывался по SQE под отладчиком. Jitter — случайная задержка перед постановкой, только когда silent включён. Это не стелс: цель — предсказуемый
src/uring/uring_sync.ccanonical синхронный исполнитель uring_op_t (фаза 4).
src/uring/uring_sync.hсинхронный исполнитель операций uring_op_t. Единый «реальный» исполнитель на обычных syscall — используется как: • fallback submit-backend, когда io_uring недоступен; • путь для opcodes, которые ещё не покрыты постановкой SQE в кольце (openat/unlinkat/mkdirat и т.п.) — кольцо делает то, что умеет, остальное
Контракты API / 1 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/uring_xio_future.h
/* platx/uring_xio_future.h — Uring completions bridged to XIO futures (INT-104). */
#define PLAT_URING_FUTURE_REASON 128

typedef enum {
    PLAT_URING_FUTURE_OK      = 0,
    PLAT_URING_FUTURE_NOLINK  = 1,
    PLAT_URING_FUTURE_TIMEOUT = 2,
    PLAT_URING_FUTURE_ERROR   = 3,
} plat_uring_future_status_t;

typedef struct {
    plat_uring_future_status_t status;
    int    res;          /* io_uring CQE result */
    char   reason[PLAT_URING_FUTURE_REASON];
} plat_uring_future_result_t;

/* Submit an op-agnostic uring SQE and bridge completion to an XIO future.
 * sqe_fd: the fd to use in the uring op; op_code: IORING_OP_*.
 * Blocks up to timeout_ms (0 = no wait).  Returns 0/-1. */
int plat_uring_xio_submit(int sqe_fd, unsigned int op_code,
                          int timeout_ms,
                          plat_uring_future_result_t *out);
60

vault

Зашифрованное хранилище объектов в памяти
src/vault/Доверие и защита9 файлов2 API headers

Зашифрованное хранилище объектов в памяти

Граница ответственности

  • Plaintext lifetime bounded/wiped; status never reveals values.
  • Access through owner/session/token policy and optional keyring adapter.

Устройство подсистемы

  • Vault state LOCKED/UNLOCKED/DEGRADED/CLOSING; master key handle separate from records.
  • Record envelope id/name/version/nonce/ciphertext/tag/ACL/timestamps/expiry.
  • Session/token grants bind actor/context/generation and uses.

Поток работы

  • Initialize/unlock key.
  • Authz session → put/get/revoke.
  • get decrypts to wipe-aware handle.

Отказ и восстановление

  • Wrong key/tag/tamper returns auth failure, no partial plaintext.
  • OOM after decrypt wipes temporary.
  • Keyring/vault dependency loss revokes adapters and degrades consumers.

Основные возможности

  • Encrypted in-memory records with session/token controls.
  • Keyring bridge and configurable cipher/passphrase/hex key setup.
Архитектурные детали и инварианты

Overview

Sealed secret storage backed by Linux memfd. All operations are gated on a valid profile HMAC (§SEC-0 / INV-VAULT-01). Supports key rotation without plaintext exposure.

Invariants

INV-VAULT-01 | Vault accessible only after vault_set_profile_hmac() — §SEC-0

Components

- vault_seal.c — profile gate, memfd create, seal; slot management

- vault_unseal.c — authenticated unseal; wipe on failure

- vault_rotate.c — re-encrypt slot under new key, wipe plaintext

Управление и диагностика

Корневые команды: vault. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / vault →
Состав подсистемы / 9 файлов
Файл / компонентНазначение и граница
src/vault/cmd_vault.cбогатый CLI для VaultFS. vault token create / list / info / verify / revoke vault session open / close / touch / info / list / gc vault put / get / delete / rename / stat / list / clear vault snapshot / restore
src/vault/vault.cзашифрованное in-memory хранилище (VaultFS). Каждый объект хранится как зашифрованный blob: nonce || ciphertext || tag Ключ объекта выводится через HKDF: obj_key = HKDF-SHA256(master_key, salt="vault-obj", info=vpath) vault_snap_hdr_t || [vault_snap_entry_t × N] || [enc_data × N]
src/vault/vault.hзашифрованное in-memory хранилище данных (VaultFS). vault ≠ XIO. plat_recovery decides; this file does not. Хранилище организовано поверх анонимных memfd-объектов и предоставляет: - Виртуальные пути : "/secret/config" → зашифрованный blob - Токен-менеджмент : HMAC-SHA256 токены с expiry
src/vault/vault_if.hпубличный контракт кирпича "vault" (капабилити-интерфейс). Потребители (fuse, keyring seed-vault, vfs) работают ТОЛЬКО через этот vtable, полученный из platform_require(VAULT_IF_NAME, ...). Прямая C-линковка с vault.c не нужна — реализацию за интерфейсом можно
src/vault/vault_io.hthe vault tree's one door for typed I/O. Private to src/vault. Deliberately NOT in vault.h: that header is included from outside this tree, and a door belongs to the tree that owns the descriptors, not to everyone who can name it. Shared by vault.c and cmd_vault.c. static inline, the same way dsl_io.h
src/vault/vault_record.cThese fields are public identities. Comparison precedes any decrypt; authenticity is established by the wrap tag and then the payload tag.
src/vault/vault_rotate.crotate vault key without exposing plaintext (INV-VAULT-01)
src/vault/vault_seal.cseal a secret into memfd (INV-VAULT-01: requires profile HMAC)
src/vault/vault_unseal.cРеализация vault / unseal
Контракты API / 2 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/vault_record_v1.h
/* Vault's authenticated record primitive. It performs no I/O, authorization
 * or lifecycle decisions. The vault owner supplies its domain wrapping key. */
#define PLAT_VAULT_RECORD_HEADER 152u
#define PLAT_VAULT_RECORD_OVERHEAD (PLAT_VAULT_RECORD_HEADER + 16u)
#define PLAT_VAULT_RECORD_MAX (1024u * 1024u)

typedef struct plat_vault_record_identity {
    uint8_t vault_id[16];
    uint8_t object_id[16];
    uint64_t version;
    uint64_t key_epoch;
    uint32_t kind;       /* secrets object kind, 1..6 */
    uint32_t scope;      /* NODE=1, TEAM=2, PERSONAL=3 */
} plat_vault_record_identity_t;

/* The wrapping key is NOT a password or an OS/node identity key. Each record
 * gets a fresh random DEK and nonces. Metadata requiring confidentiality
 * belongs inside payload; public identity is authenticated as AAD.
 * Output/input buffers must not overlap. Metadata is not an authority. */
int plat_vault_record_seal(const plat_crypto_aead_v2_t *crypto,
                           const uint8_t wrapping_key[32],
                           const plat_vault_record_identity_t *identity,
                           const void *payload, size_t payload_len,
                           void *record, size_t capacity, size_t *written);

/* expected is supplied by the authorized vault lookup, not copied from an
 * untrusted record. A wrong identity/epoch or invalid AEAD releases no data.
 * capacity must cover the encoded payload size. */
int plat_vault_record_open(const plat_crypto_aead_v2_t *crypto,
                           const uint8_t wrapping_key[32],
                           const plat_vault_record_identity_t *expected,
                           const void *record, size_t record_len,
                           void *payload, size_t capacity, size_t *written);
include/platx/vault_vfs.h
/* platx/vault_vfs.h — Vault secret as read-only VFS node (INT-095).
 * Exposes a vault secret at a vfs:/vault/<key> path for subsystems
 * that only speak VFS (no direct vault dependency).
 */
#define PLAT_VAULT_VFS_PREFIX "vfs:/vault/"
#define PLAT_VAULT_VFS_PATH   128
#define PLAT_VAULT_VFS_REASON 128

typedef enum {
    PLAT_VAULT_VFS_OK     = 0,
    PLAT_VAULT_VFS_NOLINK = 1,
    PLAT_VAULT_VFS_ERROR  = 2,
} plat_vault_vfs_status_t;

/* Copy secret identified by key into VFS at vfs:/vault/<key>.
 * Path written to path_out (len path_outsz).  Returns 0/-1. */
int plat_vault_vfs_expose(const char *key, char *path_out, size_t path_outsz);

/* Remove the VFS node for key (secret stays in vault). 0/-1. */
int plat_vault_vfs_revoke(const char *key);

/* 1 if node exists in VFS, 0 if not, -1 on error. */
int plat_vault_vfs_check(const char *key);
61

vfs

Виртуальные файловые объекты, потоки и конфигурация
src/vfs/Исполнение и расширения11 файлов3 API headers

VFS предоставляет virtual path/store/config/stream facade над providers (vault, memory, filesystem, plugin assets). Он должен нормализовать namespace, authz/context tags и transactions, не притворяясь полноценной POSIX filesystem.

Граница ответственности

  • Path grammar canonical/bounded, no provider escape.
  • Providers capability-based; VFS не включает private vault/plugin structs.
  • Configuration apply отделён от raw put/get.

Устройство подсистемы

  • Mount/provider table immutable generation maps virtual prefix to provider handle/policy.
  • Path parser canonicalizes segments and rejects traversal/ambiguous encoding.
  • Operation resolve checks context/lease, pins provider generation and calls typed stat/list/read/write/stream.
  • Config transaction stages changes, validates consumers and commits generation.

Поток работы

  • Request path/context → canonicalize/authz.
  • Resolve mount/provider lease.
  • Perform bounded operation/stream through XIO.
  • Result/audit; release provider lease.

Отказ и восстановление

  • Provider revoked during stream → typed stale/partial semantics.
  • Mount/config reload atomic; old streams pinned policy.
  • Traversal/duplicate prefix/loop reject.
  • Write partial uses provider transaction/temp where promised.

Основные возможности

  • Aggregate virtual store and path operations.
  • Configuration view/apply mechanism.
  • Context tags, health views, streams and vault-backed adapters.
Архитектурные детали и инварианты

Overview

An in-process virtual filesystem that stores blobs sealed with AES-256-GCM.

Paths are virtual (e.g. vfs:/msx/deploy.ms); real FS operations are bridged

Components

- vfs_store.c — core store with put/get/remove/foreach

- vfs_cfg.c — configuration namespace

- vfs_hooks.c — hook integration

- vfs_file.c — load/save between real FS and VFS store

- vfs_dir.c — prefix-based directory listing

- vfs_async.c — async stub (synchronous-now, async-later)

Addressing

Paths: vfs:/… or bare /…. vfs_norm_path() normalises before lookup.

Security

Each blob is sealed with a process-session key. Memory is mlock'd with

DONTDUMP + WIPEONFORK flags.

Управление и диагностика

Корневые команды: vfs, store. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / vfs →
Состав подсистемы / 11 файлов
Файл / компонентНазначение и граница
src/vfs/cmd_vfs.cединый обзор in-memory зашифрованного хранилища платформы. Виртуальная ФС поверх состояния всех подсистем (ничего не копирует — читает «живые» реестры): /internal/ internal-плагины (PIC-ELF в анонимной памяти) /external/ external-плагины (.so через memfd+dlopen)
src/vfs/vfs_aggregate.cVFS aggregate view stale-entry sweep. STAB-120: Remove stale entries triggered by plugin_unload, key_revoke, vault_restore and memfd_close lifecycle events.
src/vfs/vfs_async.cFull async I/O for VFS is deferred (no io_uring dependency here). These stubs provide the API surface; a future wave wires real async.
src/vfs/vfs_cfg.cparse vfs:/cfg presets. One path: scan, store, setenv.
src/vfs/vfs_cfg.happly vfs:/cfg .conf files into a kv table and process env. Boot: after archive mount, before on_before_core_loaded. Existing environment wins (setenv overwrite=0). Table always stores the container value so scripts can read the preset. Lines: key = value (# comments, optional quotes).
src/vfs/vfs_dir.cVFS directory abstraction: list/count entries under a prefix
src/vfs/vfs_file.cVFS file abstraction: load/save real files into vfs store
src/vfs/vfs_hooks.cexecute VFS-resident scripts / DSL at boot phases.
src/vfs/vfs_hooks.hboot lifecycle callbacks that run .ms / .dsl from VFS. on_config_applied, on_before_core_loaded, on_core_loaded, on_core_started, Runtime (not a boot gate): on_hostile_env — debugger / trace / refused DRM activate. Edge-triggered: once per hostile window; cleared when env is safe.
src/vfs/vfs_store.csealed in-memory VFS container.
src/vfs/vfs_store.hin-memory crypto container addressed by virtual paths. VFS is the archive/CLI tree (vfs:/…, vfs ls, vfs_get_text) and the future platx-vfs provider for the same paths. FUSE sits under VFS as that tree's filesystem frontend; it is not an XIO backend (XIO→FUSE→XIO is closed).
Контракты API / 3 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/vfs_context_tag.h
/* platx/vfs_context_tag.h — Fabric Context identity to VFS/FSX artifacts (INT-099). */
#define PLAT_VFS_CTX_TAG_PATH  128

typedef struct {
    char     path[PLAT_VFS_CTX_TAG_PATH];
    uint64_t ctx_id;
    uint32_t ctx_gen;
    uint64_t tagged_at_ms;
} plat_vfs_ctx_tag_t;

/* Store a context-tagged metadata blob at vfs:/ctxtag/<path>.
 * Fills out on success.  Returns 0/-1. */
int plat_vfs_ctx_tag(const char *path, uint64_t ctx_id, uint32_t ctx_gen,
                     plat_vfs_ctx_tag_t *out);

/* Tag an FSX artifact path with context identity (stored via VFS). 0/-1. */
int plat_fsx_ctx_tag(const char *session_id, const char *path,
                     uint64_t ctx_id, uint32_t ctx_gen);
include/platx/vfs_health_view.h
/* platx/vfs_health_view.h — VFS read-only view of health/recovery/audit (INT-119). */
#define PLAT_VFS_HEALTH_PREFIX  "vfs:/health/"
#define PLAT_VFS_AUDIT_PREFIX   "vfs:/audit_tail"
#define PLAT_VFS_SUP_PREFIX     "vfs:/supervisor/"

/* Snapshot health state into VFS.  Returns 0/-1. */
int plat_vfs_health_snapshot(void);

/* Snapshot audit tail into VFS at vfs:/audit_tail.  0/-1. */
int plat_vfs_audit_tail_snapshot(int max_lines);

/* Snapshot supervisor graph into VFS at vfs:/supervisor/graph.  0/-1. */
int plat_vfs_supervisor_snapshot(void);
include/platx/vfs_stream.h
/* platx/vfs_stream.h — VFS streaming API for RA2C without pathnames (INT-094).
 * Generates a VFS path from context id + sequence counter.
 * RA2C sees only the generated path — no raw filesystem paths exposed.
 */
#define PLAT_VFS_STREAM_MAX   4096
#define PLAT_VFS_STREAM_PATH  128

typedef enum {
    PLAT_VFS_STREAM_OK     = 0,
    PLAT_VFS_STREAM_NOLINK = 1,
    PLAT_VFS_STREAM_ERROR  = 2,
} plat_vfs_stream_status_t;

typedef struct {
    plat_vfs_stream_status_t status;
    char    path[PLAT_VFS_STREAM_PATH];   /* generated vfs:/ path */
    size_t  len;
    char    reason[128];
} plat_vfs_stream_result_t;

/* Store data as VFS blob; path generated as vfs:/stream/<ctx_id>/<seq>.
 * kind determines VFS_KIND_*.  Returns 0/-1. */
int plat_vfs_stream_put(uint64_t ctx_id, uint32_t ctx_gen,
                        vfs_kind_t kind, const uint8_t *data, size_t len,
                        plat_vfs_stream_result_t *out);

/* Retrieve a previously streamed blob by generated path. 0/-1. */
int plat_vfs_stream_get(const char *path, uint8_t **data_out, size_t *len_out);
62

wiper

Завершение жизни и стирание чувствительных буферов
src/wiper/Доверие и защита5 файлов0 API headers

Wiper выполняет best-effort secure erasure memory/files/directories/free-space/metadata и честно сообщает guarantees/limitations среды. Он не обещает физическое уничтожение на SSD/COW/remote storage без доказательства.

Граница ответственности

  • Target scope explicit/validated; destructive operation требует strong authz/confirmation.
  • Memory wipe separate from filesystem erase semantics.
  • No recursive broad path/glob by default; root/workspace guards.

Устройство подсистемы

  • Erase plan identifies target type/fs/device, methods/passes, verification, limits, rollback impossibility and evidence.
  • Preflight refuses ambiguous/symlink escape/mount boundary according policy.
  • Executor operates bounded chunks through XIO, records progress/cancellation semantics.
  • Verifier checks logical result and reports guarantee class, not false certainty.

Поток работы

  • Operator target → resolve/canonicalize/preflight.
  • Display plan/risk → explicit authorization.
  • Execute method → sync/verify.
  • Audit evidence + final guarantee/limitations.

Отказ и восстановление

  • Partial erase/cancel returns PARTIAL with exact progress; no success.
  • Permission/I/O failure retains target info safely.
  • Crash recovery cannot undo; plan journal identifies incomplete.

Основные возможности

  • File, memory, directory and free-space wipe operations.
  • Metadata cleanup controls.
  • Self-test and benchmark aid environment characterization.
Архитектурные детали и инварианты

Overview

Secure data erasure for memory regions, files, and key material.

Invariants

- **INV-WIPER-01**: wiper_mem() must be called for every secret at cleanup.

A volatile pointer barrier prevents compiler elision.

Управление и диагностика

Корневые команды: wiper. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / wiper →
Состав подсистемы / 5 файлов
Файл / компонентНазначение и граница
src/wiper/cmd_wiper.cleftover CLI verb "wiper". Not a working path. Not called from recovery. Not XIO. REG_wiper() only registers the verb; this is not a default boot command and must not be added to bootstrap.dsl / on_platx_boot.ms.
src/wiper/wiper.csecure data erasure module implementation. Algorithms implemented: For SSD/NVMe: WIPER_FL_TRIM issues BLKDISCARD to signal the controller; no software pattern can guarantee erasure below the FTL on flash storage. XIO integration (Волна 1): All file I/O goes through xio_get_for("wiper", XIO_CATEGORY_FILE).
src/wiper/wiper.hsecure data erasure module. Operates on: files, memory regions, directories (recursive), free space, and file metadata (rename + truncate before unlink). Audit integration: every erasure operation calls audit_write() if the audit module is initialised. Thread safety: individual wiper_erase_* functions are thread-safe w.r.t.
src/wiper/wiper_file.cРеализация wiper / file
src/wiper/wiper_key.cwipe crypto key material (INV-WIPER-01)
Контракты API / 0 заголовков

Отдельный заголовок из include/platx/ не закреплён за этим доменом в справочнике. Внутренние границы перечислены в составе файлов; управление и публичные связи описаны в соседних подсистемах.

63

xim

Посредничество исполнения и системных вызовов
src/xim/Исполнение и расширения16 файлов3 API headers

XIM — seccomp user-notification mediator: spawn/arm policy, intercept syscalls, decide, optionally execute privileged operation and inject fd ADDFD. Dedicated appliance отделяет эту security boundary от full toolbox.

Граница ответственности

  • Kernel notification untrusted; validate ids/pid/generation/args/memory.
  • Injected fd acquired/owned through Core resource scope and mapped generation-aware.

Устройство подсистемы

  • Mediator instance owns listener fd, child context, notification task, policy generation, fd map and outstanding decisions.
  • Spawn bootstrap applies no_new_privs/filter, establishes listener, handshake/READY.
  • Decision pipeline validates notification still valid, copies bounded arguments safely, evaluates policy/lease, performs operation via XIO/resource, ADDFD and responds.
  • Stop revokes admission, denies/drains outstanding, terminates child, closes listener/map.

Поток работы

  • arm policy → spawn isolated child.
  • Kernel notification → validate/context/policy.
  • Allow/deny/emulate; optional fd injection.
  • Audit/trace → response; child continues.

Отказ и восстановление

  • Notification invalidated/child exit → no operation/ADDFD.
  • FD opened but ADDFD fails → scope close.
  • Policy reload in-flight uses pinned generation.

Основные возможности

  • Arms seccomp user-notification mediation.
  • Spawns mediated process and tracks notifications.
  • Injects approved descriptors using ADDFD and generation-aware map.
  • Dedicated appliance combines Core/XIO/child/sandbox/audit/trace.
Архитектурные детали и инварианты

Overview

XIM mediates all interactions with child processes. Before a child process may

consume any resource it must (a) have policy approval and (b) have remaining

quota. XIM enforces both gates and terminates quota-busting children with

SIGKILL. All operations are recorded in an immutable audit ring.

Security Invariants

**INV-XIM-01** | A child without policy approval **cannot** receive any resource. xim_policy_check() returns -1 and the operation is rejected.

**INV-XIM-02** | When a child's quota (ops or bytes) is exhausted the child receives **SIGKILL** via xim_kill(). There is no grace period.

**§SEC-3** | No malloc(). Child table is xim_child_t g_children[XIM_MAX_CHILDREN] (64 slots) in static BSS.

Components

xim_spawn(name, argv0) forks a child process. Policy check (INV-XIM-01)

is mandatory before fork(). The child's PID and alive=1 are recorded in

Per-name policy table. xim_policy_approve() / xim_policy_revoke().

Per-child ops and bytes quota. xim_quota_check() returns -1 when either

limit is exceeded (INV-XIM-02). xim_quota_consume() debits the counters.

xim_kill(name) sends SIGKILL to the child's PID and records an audit entry

with op_type = 0xFF (forced termination).

1 024-entry ring of 52-byte xim_audit_entry_t records. Lock-free

atomic_fetch_add cursor. xim_audit_drain() copies up to max entries.

Управление и диагностика

Корневые команды: xim. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / xim →
Состав подсистемы / 16 файлов
Файл / компонентНазначение и граница
src/xim/cmd_xim.coperator CLI for the CHILD mediator: arm | status | disarm. One command line arms mediation; nothing on the platform auto-arms. A CHILD that never asked for XIM keeps the exact spawn path it had, because the arm is a process-wide switch the operator throws, not a default. The verbs here
src/xim/xim_audit.cРеализация xim / audit
src/xim/xim_boot.carming the mediator, and owning the one instance an operator The engine could always decide and always serve; what it could not do was be turned on. The child-host already carries the wiring (weak hooks that are no-ops until something arms), so the whole of "product" here is: hold the
src/xim/xim_boot.hthe operator's end of the mediator. The engine in src/xim decides verdicts and answers a mediated child; nothing in production ever asked it to. This is the arm switch and the one place that owns the operator's mediator instance, so a CLI (or a preset) has a single,
src/xim/xim_core.cthe mediator table and the verdict, with no kernel in sight. Everything here is deliberately decidable without seccomp: the parent's answer to "may this child ask for nr?" is policy, and policy that can only be tested on a machine with USER_NOTIF is policy nobody tests. The notify
src/xim/xim_host.cthe three points where a spawned CHILD becomes a mediated one. Off unless armed. A host that never asks for mediation runs the same code path it ran before, which is the only way to add this to a working spawn without holding its correctness hostage. Armed, the shape is fixed by what seccomp allows and by what the platform
src/xim/xim_identity.cчетыре поля вместо номера. Единственная тонкость здесь — разбор /proc//stat. Поле 22 (starttime) нельзя достать, считая пробелы с начала: поле 2 — это comm, имя исполняемого файла в скобках, и оно может содержать и пробелы, и скобки. Правильная точка
src/xim/xim_identity.hкто именно этот CHILD, а не какой у него номер. PID — это не идентичность, это индекс. Ядро выдаёт его повторно, и окно между «ребёнок умер» и «номер занят другим» ничем не отмечено. Для медиатора это не теория: xim_map_serve_once читает /proc//mem, чтобы классифицировать
src/xim/xim_kill.cSends SIGKILL; marks slot dead; records audit entry.
src/xim/xim_map.ca logical name the parent recognises, answered with its own fd. This is the step the roadmap draws as policy -> virtual map -> XIO. The child asks to open a name; the parent decides that name means a resource of its own choosing, opens that resource *through XIO*, and hands the
src/xim/xim_notify.cthe live seccomp-notify channel: child filters, parent answers. The shape is the whole point. The child installs its own filter after fork and before exec, hands the listener fd to the parent over the socketpair that already exists between them, and then runs. The parent never injects a
src/xim/xim_policy.capply the verdict to the live child, and say so once. A denial here is a refused syscall, not a dead process: the child gets -errno back and keeps running. Killing on a policy miss would make the mediator a lifecycle actor, and lifecycle has exactly one owner elsewhere.
src/xim/xim_quota.cINV-XIM-02: quota exceeded → SIGKILL the child (caller must call xim_kill).
src/xim/xim_serve.cthe worker that answers a mediated child for as long as it The hole this closes: everything else in XIM was one-shot. A listener could be installed, adopted and frozen at an epoch, and the child could be handed a filter that traps -- and then nobody was on the other end. The first
src/xim/xim_spawn.carm the mediator from the product spawn path, not only the CLI. The child host already carries the weak seccomp hooks; what it lacked was a caller that armed before the fork. This is that caller, kept to two lines of real work so plat_child_host.c needs only one extern and one gated call. All
src/xim/xim_spawn.hproduction spawn's one line to the mediator. O-B1 gave the operator a CLI to arm XIM; this is the other way in. The child host calls arm before a child that asked for mediation (the spawn opt) forks, and disarm when that child is torn down. Off unless asked: a spawn that did
Контракты API / 3 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/platx_xim.h
/* include/platx/platx_xim.h — XIM child mediation public API (task 3.40).
 *
 * INV-XIM-01: child without policy approval cannot get resource.
 * INV-XIM-02: quota exhausted → SIGKILL (not continue).
 * §SEC-3: no malloc in spawn/quota/kill paths.
 */
/* A2-P07. Guard был PLATX_XIM_H — ровно тот же, что у platx/xim.h. Два разных
 * публичных заголовка с одним guard: включённый вторым молча превращался в
 * пустоту, без единого диагностического сообщения, а потребитель получал
 * "unknown type name" в месте использования, далеко от причины. Ни один TU не
 * мог видеть quota/kill/audit и verdict/epoch одновременно. */

#define XIM_MAX_CHILDREN  64u
#define XIM_NAME_MAX      64u

typedef enum {
    XIM_POLICY_ALLOW  = 0,
    XIM_POLICY_DENY   = 1,
    XIM_POLICY_AUDIT  = 2,
} xim_policy_result_t;

typedef struct {
    char     name[XIM_NAME_MAX];
    pid_t    pid;
    uint64_t ops_used;
    uint64_t ops_limit;       /* INV-XIM-02: 0=unlimited                  */
    uint64_t bytes_used;
    uint64_t bytes_limit;
    int      alive;
    int      policy_approved;
} xim_child_t;

/* quota: limit=0 means unlimited. Накопители насыщаются, а не переполняются:
 * исчерпанная квота не может «сброситься» переполнением uint64. */
int  xim_quota_set(const char *name, uint64_t ops_limit, uint64_t bytes_limit);
int  xim_quota_check(const char *name);   /* 0=ok -1=exceeded (→SIGKILL) */
void xim_quota_consume(const char *name, uint64_t bytes);
int  xim_quota_get(const char *name, xim_child_t *out);

/* A2-P07-337/338. Без привязки pid поле xim_child_t.pid не выставлял никто,
 * и xim_kill() не мог послать сигнал ни разу: INV-XIM-02 был недостижим
 * через этот API. bind_pid делает слот убиваемым, mark_dead снимает признак
 * жизни (после reap), reset_all нужен фикстурам. */
int  xim_quota_bind_pid(const char *name, pid_t pid);
int  xim_quota_mark_dead(const char *name);
void xim_quota_reset_all(void);

/* kill: sends SIGKILL (INV-XIM-02). 0 — сигнал послан (или процесс уже
 * мёртв), -1 — слот неизвестен либо к процессу не привязан. Признак жизни
 * снимается, поэтому повторный вызов не шлёт сигнал по переиспользованному
 * pid. */
int  xim_kill(const char *name, const char *reason);

/* audit: all child ops go to audit ring */
typedef struct __attribute__((packed)) {
    uint64_t ts_mono_us;
    uint32_t op_type;
    uint32_t flags;
    char     name[32];
    int32_t  result;
    uint32_t _pad;
} xim_audit_entry_t;   /* 56 bytes packed (8+4+4+32+4+4); было заявлено 52 */

/* Поле name записи короче XIM_NAME_MAX: длинные имена в ring усекаются. */
#define XIM_AUDIT_NAME_MAX 32u

void xim_audit_record(uint32_t op_type, const char *name, int32_t result);
/* >=0 — сколько записей отдано; -1 при out==NULL/max<=0. */
int  xim_audit_drain(xim_audit_entry_t *out, int max);
/* Сколько записей затёрто до чтения. Переполнение кольца наблюдаемо. */
uint64_t xim_audit_dropped(void);
void xim_audit_reset(void);

/* stats */
typedef struct {
    uint64_t xim_spawns;
    uint64_t xim_kills;
    uint64_t xim_quota_hits;
    uint64_t xim_policy_denials;
} xim_stats_t;
void xim_stats_get(xim_stats_t *out);
include/platx/services/xim_v1.h
/* platx/services/xim_v1.h — public contract, not the implementation.
 * Modules require/provide this vtable. They never include src/xim internals.
 * Presence of the vtable is isolation.xim:v1. Absence is UNAVAILABLE.
 */
/* isolation.xim @ PLAT_XIM_V1. Spoken form: isolation.xim:v1.
 * Name lives here (same as crypto.aead) so names.h is not a second ABI. */
#define PLAT_NS_ISOLATION       "isolation"
#define PLAT_NAME_ISOLATION_XIM "isolation.xim"
#define PLAT_CAP_XIM            PLAT_NAME_ISOLATION_XIM
#define PLAT_XIM_V1             0x00010000u

typedef struct plat_xim_v1 {
    uint32_t struct_size; /* sizeof; foreign layout — do not call */
    int (*ready)(void);   /* 0 = product arm live; else not READY */
} plat_xim_v1_t;

/* Base size: ready present. Trailing fields would be optional by struct_size. */
#define PLAT_XIM_V1_BASE_SIZE \
    (offsetof(plat_xim_v1_t, ready) + \
     sizeof(((plat_xim_v1_t *)0)->ready))
include/platx/xim.h
/* platx/xim.h — XIM: the CHILD mediator. Fourth class, MEDIATOR.
 *
 * XIO answers "who executes this I/O". XIM answers a different question:
 * is this CHILD allowed to *ask* for the syscall at all. The child installs
 * its own seccomp filter (after fork, before exec), the listener fd lives in
 * the parent, and the parent replies ALLOW or DENY. Nothing here performs the
 * syscall on the child's behalf: seccomp-notify driving read/write would make
 * the notify handler an I/O backend, which is a different and rejected idea.
 *
 * Fail-closed, so the table below has no "probably fine" cell:
 *   - generation 0 never creates a mediator;
 *   - a request carrying a dead generation is DENIED, never allowed through;
 *   - a syscall routed to notify with no explicit ALLOW rule is denied;
 *   - a syscall the kernel notified but the instance never listed is denied,
 *     because that means the filter and this table disagree;
 *   - no listener fd means the child must not reach exec at all.
 *
 * The instance table is internal and bounded, in the same spirit as the XIO
 * fd registry. It is not a fifth platform registry, and it holds no pointers
 * into the child.
 */
#define XIM_ID_INVALID    0u
#define XIM_MAX_INSTANCES 8u
#define XIM_MAX_NRS       16u

typedef uint32_t xim_id_t;

/* The whole vocabulary of a mediator verdict. Restart/fail live elsewhere. */
typedef enum xim_decision {
    XIM_ALLOW = 0,
    XIM_DENY  = 1
} xim_decision_t;

/* Why, as a field. A new reason beats a new event id. */
typedef enum xim_reason {
    XIM_OK = 0,
    XIM_PERMISSION_DENIED,  /* nr is mediated, no rule was ever set */
    XIM_GENERATION_STALE,   /* request outlived the epoch that created it */
    XIM_LISTENER_MISSING,   /* no listener fd: the child must not run */
    XIM_UNSUPPORTED,        /* kernel asked about an nr we never mediate */
    XIM_TARGET_EXITED,
    XIM_DRAIN_TIMEOUT,
    XIM_POLICY_DENIED       /* a rule exists and it says no */
} xim_reason_t;

typedef enum xim_state {
    XIM_STATE_FREE = 0,
    XIM_STATE_CREATED,    /* policy can be set; no listener yet */
    XIM_STATE_ACTIVE,     /* listener handed over; child may run */
    XIM_STATE_DRAINING,   /* epoch is closing; late notifies are denied */
    XIM_STATE_DESTROYED
} xim_state_t;

typedef struct xim_verdict {
    xim_decision_t decision;
    xim_reason_t   reason;
    int            err;   /* errno handed back on DENY; 0 on ALLOW */
} xim_verdict_t;

/* query() output. Numbers and names only: no fd guts, no child pointers. */
typedef struct xim_info {
    xim_id_t     id;
    plat_owner_t owner;
    xim_state_t  state;
    int          listener_live;
    uint32_t     nr_count;
    uint64_t     allowed;
    uint64_t     denied;
    xim_reason_t last_reason;
} xim_info_t;

/* One pending request, as the kernel described it. Numbers only: the argument
 * words are addresses in the child, not pointers this process may follow
 * without going through the explicit reader below. */
typedef struct xim_request {
    uint64_t           req_id;
    uint32_t           pid;
    int                nr;
    unsigned long long args[6];
} xim_request_t;

/* Event payload. No pointers, well under PLAT_EV_PAY_MAX. */
typedef struct plat_xim_fact {
    uint32_t xim_id;
    uint32_t module_id;
    uint32_t instance_id;
    uint32_t generation;
    int32_t  nr;
    uint32_t decision;
    uint32_t reason;
    int32_t  err;
} plat_xim_fact_t;

int      xim_init(void);
void     xim_fini(void);

/* generation 0 refuses. An empty nr list refuses: a mediator that mediates
 * nothing would report ACTIVE while deciding nothing. */
xim_id_t xim_create(plat_owner_t owner, const int *notify_nrs, size_t n_nrs);
int      xim_destroy(xim_id_t id);

/* Rules. Both refuse an nr the instance does not mediate: allowing a syscall
 * the filter never traps is a lie the operator would read as coverage. */
int      xim_policy_allow(xim_id_t id, int nr);
int      xim_policy_deny(xim_id_t id, int nr, int err);

/* New epoch for the same mediator. Requests carrying the old generation stop
 * being allowed the moment this returns. */
int      xim_retarget(xim_id_t id, plat_owner_t owner);

/* Listener bookkeeping. The fd itself is opened by the notify layer; the core
 * only records that the parent holds one, because that is what gates exec.
 * Attach also freezes the epoch the child was born into: that frozen number,
 * not the instance's current generation, is what a later notify is judged
 * against, or a child from a dead epoch would ride its successor's rules. */
int      xim_attach_listener(xim_id_t id, int listener_fd);
int      xim_listener_fd(xim_id_t id);
int      xim_listener_epoch(xim_id_t id);   /* 0 when nothing is attached */
int      xim_detach_listener(xim_id_t id);

/* The one gate before exec: a child whose listener never arrived is not a
 * half-started child, it is a failure. */
int      xim_ready_for_exec(xim_id_t id);

/* Pure decision. No kernel, no I/O, no lifecycle. Returns 0 and fills out,
 * or -1 if the id is not a live instance. */
int      xim_decide(xim_id_t id, int nr, uint32_t generation,
                    xim_verdict_t *out);

/* Unknown id is an error, never an empty struct that reads as healthy. */
int      xim_query(xim_id_t id, xim_info_t *out);

/* ── seccomp-notify plumbing ───────────────────────────────────────────────
 *
 * The child installs its own filter; the parent never injects anything and
 * never traces. The filter routes a named handful of syscall numbers to
 * USER_NOTIF and lets everything else fall through untouched: stacked seccomp
 * filters resolve to the strictest action, so this cannot loosen a kill-list
 * that is already in place.
 */
int      xim_notify_supported(void);

/* Child side, after fork and before exec. Returns the listener fd to hand to
 * the parent, or -1. The child keeps no policy: it only routes. */
int      xim_notify_install(const int *nrs, size_t n_nrs);

/* Handover over an already-open socketpair. The fd is duplicated by the
 * kernel; a failed handover must end the child, not start it. */
int      xim_notify_send_fd(int sock, int fd);
int      xim_notify_recv_fd(int sock, int *fd_out);
/* A2-P07-312: то же, но с явным бюджетом. timeout_ms < 0 = блокировать,
 * 0 = только опрос. Исчерпание бюджета: -1 / ETIMEDOUT. */
int      xim_notify_recv_fd_deadline(int sock, int *fd_out, int timeout_ms);

/* Parent side. wait() returns 1 when a request is pending, 0 on timeout. */
int      xim_notify_wait(int listener, int timeout_ms);
int      xim_notify_recv(int listener, uint64_t *req_id, int *nr, uint32_t *pid);
int      xim_notify_respond(int listener, uint64_t req_id, long long val,
                            int err, unsigned flags);
/* ALLOW means the kernel runs the real syscall, so the child observes exactly
 * what it would have without a mediator. Returning a made-up value instead
 * would make XIM an I/O backend, which it is not. */
int      xim_notify_allow(int listener, uint64_t req_id);

/* Serves one pending request when the verdict is ALLOW. A non-ALLOW verdict is
 * left unanswered on purpose: applying a denial is the policy layer's job.
 * Returns 1 served, 0 nothing pending, -1 not ours to answer. */
int      xim_notify_serve_allow(xim_id_t id, int timeout_ms);

/* ── the decision, applied and recorded ────────────────────────────────────
 *
 * Deciding, applying and reporting are one step on purpose: a verdict that is
 * applied to the child but never emitted leaves the picture claiming a syscall
 * that nobody ever answered, and a verdict emitted but not applied is worse.
 */

/* Decide and emit exactly one fact. No kernel is involved, so the policy and
 * its event are provable anywhere. */
int      xim_emit_decision(xim_id_t id, int nr, uint32_t generation,
                           xim_verdict_t *out);

/* Serve one pending request end to end: decide, emit, answer the kernel.
 * 1 allowed, 2 denied, 0 nothing pending, -1 error. A denial returns the errno
 * from the rule and leaves the child running; it is a refused syscall, not a
 * killed process. */
int      xim_serve_once(xim_id_t id, int timeout_ms);

/* ── argument inspection and fd handover ───────────────────────────────────
 *
 * Reading a child's argument is inspection, not execution. Anything read this
 * way is used to classify the request against this process's own table; the
 * child's bytes are never handed onward as a path to open, because they can
 * change the moment we stop looking.
 */
int      xim_notify_recv_req(int listener, xim_request_t *out);

/* 1 the request is still live, 0 the child is gone, -1 error. Checked after
 * every read of child memory: a read that raced the child's death describes
 * nothing. */
int      xim_notify_id_valid(int listener, uint64_t req_id);

/* Install a local fd into the child and complete the syscall with it, in one
 * step. The syscall itself does not run, so nothing the child rewrites
 * afterwards can redirect what it received. */
int      xim_notify_addfd(int listener, uint64_t req_id, int local_fd,
                          int newfd_flags);

/* ── virtual map ───────────────────────────────────────────────────────────
 *
 * The mediator recognises a logical name and answers with a resource of its
 * own choosing. It opens that resource through XIO, because XIO is where I/O
 * is executed; the notify channel stays a question-and-answer channel and
 * never becomes an I/O backend. A request with no matching name is not
 * mapped: it is allowed or denied on the ordinary rules.
 */
#define XIM_MAX_MAPS 8u
#define XIM_PATH_MAX 256u

int      xim_map_add(xim_id_t id, int nr, unsigned arg_index,
                     const char *logical, const char *target);
int      xim_map_count(xim_id_t id);
void     xim_map_clear(xim_id_t id);

/* 3 mapped, 1 allowed unchanged, 2 denied, 0 nothing pending, -1 error. */
int      xim_map_serve_once(xim_id_t id, int timeout_ms);

/* ── the serve loop ────────────────────────────────────────────────────────
 *
 * Everything above is one-shot. A filter that traps with nobody on the other
 * end is not mediation, it is a stall: the child's first mediated syscall
 * blocks until something unrelated times out. This is the worker that answers
 * for as long as the child lives.
 *
 * The thread is Core's, taken from abi->task->spawn under the instance's own
 * owner. XIM creates no scheduler and no thread of its own.
 */

/* Start the worker. Returns only once the thread is inside its receive loop:
 * "spawned" and "receiving" are different facts, and the caller's next act is
 * to let the child run. -1 means the child must not be released. */
int      xim_serve_start(xim_id_t id);

/* 1 while the worker is receiving. */
int      xim_serve_running(xim_id_t id);

/* Counters, for anyone who has to prove the loop did the answering. */
int      xim_serve_stats(xim_id_t id, uint64_t *served, uint64_t *denied,
                         uint64_t *errors);

/* Teardown, in one order and no other:
 *
 *     stop receive  ->  drain  ->  close listener  ->  release owner
 *
 * Closing the listener before the drain releases an in-flight child with
 * ENOSYS from a call the parent had already decided to allow; releasing the
 * owner before the drain cancels the worker mid-answer. 0 when the worker
 * really left its loop, -1 when it did not -- reported, not papered over. */
int      xim_serve_stop(xim_id_t id, int timeout_ms);

/* ── child-host wiring ─────────────────────────────────────────────────────
 *
 * Off unless armed, so a host that never asks for mediation behaves exactly
 * as it did. A --xim spawn first requires isolation.xim:v1
 * (platx/services/xim_v1.h). No cap, or a vtable that is not READY: spawn
 * refuses. Once armed, a child that cannot hand its listener over must not
 * reach exec: a mediated child the parent cannot answer for is a failed
 * start, not a child running unmediated.
 */
int      xim_host_arm(xim_id_t id, const int *nrs, size_t n_nrs);
void     xim_host_disarm(void);
int      xim_host_armed(void);

/* Child side, between fd setup and the sandbox. Returns 0, or -1 meaning the
 * caller must abandon the child rather than exec it. */
int      xim_host_child_install(int ipc_fd);

/* Parent side, before the handshake: adopt the listener the child sent. */
int      xim_host_parent_adopt(int ipc_fd);
/* A2-P07-312. Сколько host ждёт listener от child в adopt, монотонно.
 * По умолчанию XIM_READY_BUDGET_MS_DEFAULT; <0 — блокировать без бюджета
 * (прежнее поведение, оставлено только для явного выбора). Возвращает
 * предыдущее значение. */
#define XIM_READY_BUDGET_MS_DEFAULT 5000
int      xim_host_set_ready_budget_ms(int ms);
int      xim_host_ready_budget_ms(void);

/* Register a map rule for the currently-armed mediator. Refuses if this id
 * is not the one currently armed: a rule on a dead or different id would be
 * applied to a generation the serve loop is no longer watching. Call this
 * before xim_host_parent_adopt so the worker sees the rule on its first tick.
 * arg_index is the syscall argument that carries the path (0-5). */
int      xim_host_map(xim_id_t id, int nr, unsigned arg_index,
                      const char *logical, const char *target);
64

xio

Единая модель ввода-вывода, backend, маршрута и владельца
src/xio/Связь и ввод-вывод40 файлов9 API headers

XIO — единая системная талия I/O PLATX. Он предоставляет operation model, backend registry, modes, futures, event loop, owner accounting, cancellation, stats и fault injection так, чтобы modules не размножали raw syscall loops.

Граница ответственности

  • XIO не знает RA2C/VFS/plugin business protocol.
  • Backend private types скрыты; io.xio service versioned.
  • Mode selection profile/module policy; unsupported semantics не маскируются fallback.

Устройство подсистемы

  • Operation descriptor defines opcode, handles/buffers, owner/generation, deadline, flags and completion form.
  • Backend descriptor advertises supported ops/features/limits and submit/cancel/poll/close.
  • Future state PENDING/COMPLETING/DONE/CANCELED/STALE exactly-once; pins owner/resources.
  • Loop schedules completions fairly, bounded queues, wakeup and shutdown drain. Registry/config/defaults immutable generations.

Поток работы

  • Consumer builds validated operation.
  • Resolve mode/backend/handle → reserve submission.
  • Backend executes sync/async.
  • Completion validates generation → future/callback/event → release pins.

Отказ и восстановление

  • Unsupported op before syscall.
  • Queue full backpressure.
  • Late completion stale and cannot call destroyed consumer.
  • Backend loss cancels/fails outstanding by documented semantics; no double complete.

Основные возможности

  • Eight I/O modes behind one API and backend registry.
  • Future/event-loop model plus owner tracking and leak diagnostics.
  • io_uring, eBPF and mock backends; hardcore path for advanced operations.
  • Per-module defaults/configuration and detailed stats/explain output.
Архитектурные детали и инварианты

Security Invariants

**INV-XIO-01** | Any I/O submitted without an explicit deadline automatically receives MAX_XIO_TTL_MS (5 000 ms). An I/O that exceeds its deadline is cancelled.

**INV-XIO-02** | When a subsystem's budget (ops or bytes) is exhausted the operation is **rejected** immediately — no silent skip, no blocking. The reject_count counter is incremented.

**§SEC-3** | No malloc() in the XIO hot path. All tables are static BSS: xio_budget_t g_budgets[XIO_BUDGET_SLOTS] and xio_audit_entry_t g_audit[XIO_AUDIT_RING_SZ].

Components

64-slot static budget table (XIO_BUDGET_SLOTS = 64). Each slot tracks

ops_used, bytes_used, window_start_ms, and reject_count. Window

resets when now - window_start_ms >= window_ms.

Per-operation deadline via CLOCK_MONOTONIC. xio_deadline_set(d, ttl_ms)

applies MAX_XIO_TTL_MS when ttl_ms == 0 (INV-XIO-01).

xio_deadline_expired() and xio_deadline_remaining_ms() are lock-free reads.

Lock-free atomic ring of 4 096 × 40-byte xio_audit_entry_t records.

xio_audit_record() uses atomic_fetch_add on the cursor — no mutex in the

Управление и диагностика

Корневые команды: xio, lkm. Регистрационный интерфейс дополнен справкой обработчиков в CLI-разделе.

Справочник CLI / xio →
Состав подсистемы / 40 файлов
Файл / компонентНазначение и граница
src/xio/cmd_xio.cCLI управление XIO (8 режимов, per-subsystem конфиг). xio status — ring/ebpf/loop(peek)/registry, все подсистемы xio list — список подсистем с режимами xio mode — переключить режим подсистемы/всех
src/xio/xio.cроутинг по 8 режимам XIO: uring.direct / uring.sync / uring.async / uring.hardcore syscall.direct / syscall.syscall xio_get_for(subsys, category) выбирает vtable: имя = xio_mode_from_name(subsys) → этот режим (явный запрос) иначе xio_subsys_cfg_get(subsys).mode (XIO= задаёт дефолт)
src/xio/xio.hXIO vtable: 8 режимов, синхронный + асинхронный I/O. РЕЖИМЫ (xio_mode_t из xio_mode.h) — имя ≠ бэкенд Pool + libc (одно vtable; не io_uring ring, несмотря на префикс uring.*): uring.direct — pool, 8 воркеров uring.sync — pool, sync_pool_size uring.async — pool, async_queue_depth; имя историческое, это НЕ ring
src/xio/xio_audit.cStatic ring of XIO_AUDIT_RING_SZ entries. Lock-free push via atomic cursor.
src/xio/xio_budget.cINV-XIO-02: budget exhausted → I/O rejected (no silent skip). Static slot table, no malloc.
src/xio/xio_budget_atomic.cатомарный бюджет XIO (A1-P09-402..417). ЧТО ЗДЕСЬ ИЗМЕНИЛОСЬ ПО СРАВНЕНИЮ С xio_budget.c И ПОЧЕМУ Старый модуль давал пару xio_budget_check() / xio_budget_consume(). Между ними лежит окно, и оно не закрывается никакой аккуратностью вызывающего: проверка отпускает лок, списание берёт его заново, и N потоков, прошедших
src/xio/xio_cancel.cтокен отмены XIO. Договор — include/platx/xio_cancel.h. A1-W11-030. Реализация умышленно мала: у отмены ровно одно состояние и ровно один переход. Всё, что сложнее, пришлось бы объяснять в терминах «почти отменено», а такого состояния в этом дереве быть не должно.
src/xio/xio_cap.cCore plat_xio_api. EMBEDDED only. CHILD still offers get() only (proxy). own_fd / release_owner / stats_text stay NULL there. A missing pointer is not success.
src/xio/xio_deadline.cINV-XIO-01: if ttl_ms == 0, MAX_XIO_TTL_MS is applied automatically. No malloc; xio_deadline_t is caller-owned (stack or static). A1-P08 (365..371). Четыре свойства, которых у этого файла не было: 366 issued_ms + ttl_ms складывались без проверки. ttl = UINT64_MAX давал
src/xio/xio_ebpf.cXIO eBPF backend: bpf() syscall wrappers, ELF loader, map CRUD, XDP attach/detach, async rx/tx через один poll-thread, prog registry, stats. Не зависит от libbpf — все bpf() вызовы через syscall(__NR_bpf, …). ELF загружается через (glibc): ищем секцию по имени, берём
src/xio/xio_ebpf.hXIO eBPF backend: режимы ebpf.direct и ebpf.async. Интегрирует bpf() syscall в XIO vtable как первоклассный I/O backend. Существующие eBPF-подсистемы (ebpf_xdp, cmd_ebpf) работают независимо; этот слой добавляет к XIO: • загрузку BPF-программ из ELF-объектов (без libbpf, только )
src/xio/xio_err.cnames for XIO v1 codes. Codes stay in platx/xio_err.h.
src/xio/xio_explain.csay what was decided, without deciding anything. The value of this file is entirely in what it does not do. It does not look at the subsystem and work out which backend ought to serve it; it calls the one selector and reports what that returned, including the pointer. Two
src/xio/xio_future.cреализация promise/future для гибридного XIO.
src/xio/xio_future.hpromise/future для гибридного XIO API. xio_future_t — единица отложенного результата. Создаётся async-операцией, либо завершается сразу (sync-путь, callback == NULL), либо сигналится из пула потоков/uring-обработчика. Caller может: • заблокироваться на xio_future_wait(fut) — sync-семантика
src/xio/xio_hardcore.chardcore uring backend. Использует прямые syscall(__NR_io_uring_*) без liburing и без libc-обёрток, следуя паттерну src/uring/cmd_uring.c. Структуры io_uring взяты из ядерного ABI (). Если заголовок недоступен — определены локально ниже.
src/xio/xio_hardcore.hединственный настоящий io_uring режим XIO. uring.async — pool (thread pool + libc), не ring. Имя историческое. То же для uring.direct / uring.sync. Кольцо io_uring живёт только здесь. • Собственный io_uring ring. Live = xio_hardcore_active(). Нет кольца — не hardcore.
src/xio/xio_hook.cthe hook slot table and its dispatcher. Static, bounded, no allocation: the table is BSS and the snapshot the dispatcher walks lives on the stack. A hook runs on the same thread as the operation it is classifying, so anything this file allocated would be
src/xio/xio_lkm.cLKM loader (without hiding) Loads / unloads kernel modules using the proper kernel ABI: delete_module(name, flags) — unload lkm_security_check() reads /proc/sys/kernel/modules_disabled and /proc/cmdline (module.sig_enforce) before any load is attempted.
src/xio/xio_lkm.hLKM loader (without hiding) Provides userspace helpers to load / unload kernel modules safely: • checks modules_disabled before any load attempt • checks module.sig_enforce (integrity) before loading unsigned modules • unloads with delete_module(2) • NEVER hides the module from /proc/modules, /sys/module, or lsmod
src/xio/xio_loop.cCQ + eventfd. User callback живёт до poll, не до complete. Worker зовёт xio_future_complete → loop proxy кладёт future в очередь. poll вычитывает очередь и вызывает сохранённый user callback ровно раз. auto_free срабатывает после этой доставки, не в future_finish.
src/xio/xio_loop.hevent loop для гибридного XIO. Worker/ring только кладёт future в CQ (eventfd). User callback — ровно один раз, в poll или destroy, не в complete. xio_loop_add() сохраняет user cb и подставляет proxy. auto_free откладывается: срабатывает после user cb в poll/destroy,
src/xio/xio_mock.cso (return -1 / errno set); every other call is forwarded to the base vtable untouched. There is no third state — the mock never returns a partial or invented success. If init has not run, xio_mock_vtable() is NULL and the caller must treat that as backend-down, not as "no-op".
src/xio/xio_mock.hold channel" is a claim you can only test with a provider that lies on command — and lies loudly, never silently succeeding. What it is NOT: a unit stub. The mock copies a real vtable and forwards every call it is not armed for, so a module under test still performs
src/xio/xio_mode.cper-subsystem реестр режимов и конфигов. Каждая запись реестра хранит полный xio_subsys_cfg_t. Глобальные дефолты копируются при первой регистрации подсистемы. cfg_set_key() / cfg_get_key() — внутренние парсеры ключей.
src/xio/xio_mode.h8 режимов XIO и per-subsystem конфиг. ├─ uring.direct — thread pool + libc, без io_uring ring ├─ uring.sync — thread pool + libc, sync-first (pool_size) ├─ uring.async — thread pool + libc, async-first (queue_depth) │ имя историческое: это НЕ ring; ring = hardcore
src/xio/xio_ops.cthe tables, and the two places they are allowed to say no. Everything here is lookup. There is no routing, no fallback chain and no second opinion about which backend should run anything: those belong to a resolver that does not exist yet, and starting one here by accident is the
src/xio/xio_owner.cThe owner is inside its provider close slot right now. A row in this state still holds the number -- so nothing may act on it, and the registry's close veto still refuses -- but it no longer asserts that the descriptor exists. That distinction is the whole reason the
src/xio/xio_owner.hopt-in fd claims keyed by plat_owner_t. Not auto-own on every open: open() sites stay as they are. A module claims after it has a live owner. XIO does not invent a generation. generation 0 is refuse — cannot own or check. xio_own_live_fd is the working-path name after a successful fd birth.
src/xio/xio_registry.cединый потокобезопасный реестр файловых дескрипторов. См. xio_registry.h. Реализация: фиксированная таблица под общим мьютексом, линейный поиск (O(n); для учебной платформы приемлемо, при росте — заменить на хеш по fd). Все инварианты закрытия проверяются под блокировкой, что
src/xio/xio_registry.hединый потокобезопасный реестр файловых дескрипторов. Один реестр на всю платформу (ADR-001, решение 4). Им пользуются и xio, и uring: uring вдобавок помечает записи как kernel-registered (IORING_REGISTER_FILES). Здесь же — протокол безопасного закрытия (autoclose ↔ kernel-registration), снимающий гонку «закрыть fd, пока он
src/xio/xio_route.cthe method registry, the rules, and the walk down the chain. The registry is a table of methods; the rules are a table of chains; the resolver joins them and nothing else. There is no scoring, no learning and no adaptive anything: an operator reading `xio explain` has to be able to
src/xio/xio_route_admit.cдопуск ОДНОЙ ЛОГИЧЕСКОЙ ОПЕРАЦИИ маршрута. A1-W11-046 (A1-P07-318) и A1-W11-047 (A1-P07-319), волна 11, круг 4. ЗАЧЕМ ЭТОТ ФАЙЛ СУЩЕСТВУЕТ Маршрут — это ЦЕПОЧКА методов: перенос пробует copy_file_range, потом sendfile, потом splice, и только последним rw.loop. До круга 4 бюджет
src/xio/xio_route_file.cthe file methods the resolver can choose between. Three ways to move bytes out of a descriptor, and they are not libc.read read(2). Works on anything, always. This is the baseline the chain ends with, which is why nothing here probes it. pool.read the same read, performed on the existing worker pool through
src/xio/xio_route_impl.hwhat a method is, for the files that implement one. Internal to src/xio. A method is three small things: a probe that says whether it can serve *this* request, a health function that says whether it can serve anything at all on this kernel, and the code that runs it. Any of
src/xio/xio_route_res.cmemory.create and process.pidfd as resolved methods. These two are in the resolver for opposite reasons, and the contrast is the memory.create has a real chain. A memfd is what a caller wants -- anonymous, sealable, nothing on disk -- but the syscall is not everywhere, and an
src/xio/xio_route_transfer.cfile.transfer: move bytes without carrying them. A module that wants a copy today writes a read/write loop, which drags every byte through its own address space twice. The kernel has three ways to avoid that, and none of them works everywhere: zc.copy_file_range regular file to regular file, same filesystem on old
src/xio/xio_stats.crelaxed atomics only. Classification lives in add_err, not a vtable of incrementors. Completions tick when an async slot leaves pending (delta < 0); cancel/stale/unavail stay distinct error buckets. Backend / ns / opcode counters are optional: unknown value is a no-op,
src/xio/xio_stats.hprocess-wide XIO counters. Not a backend and not a resolver. Callers increment; this file only stores and dumps. A snapshot dump is racy by design: hot-path workers must not take a lock.
src/xio/xio_submit.cwhoever claimed the fd is the one who may move bytes on it. The claim table has existed for a while and so has the check, but the synchronous path never consulted it: a caller from a dead epoch could read and write on an fd claimed by someone else and nothing said a word. That is
Контракты API / 9 заголовков

Типы, константы, прототипы и комментарии к интерфейсу из публичного дерева заголовков. Тела inline-функций опущены; декларация не устанавливает доступность провайдера в конкретной сборке.

include/platx/platx_xio.h
/* include/platx/platx_xio.h — XIO budget/deadline/audit public API (task 3.40).
 *
 * INV-XIO-01: I/O without deadline → MAX_XIO_TTL applied automatically.
 * INV-XIO-02: budget exhausted → I/O rejected (no silent skip).
 * §SEC-3: no malloc in budget/deadline/audit hot paths.
 */

#define MAX_XIO_TTL_MS      5000u   /* INV-XIO-01: default deadline      */
#define XIO_BUDGET_SLOTS    64u     /* per-subsystem static slots         */
#define XIO_AUDIT_RING_SZ   4096u   /* audit ring entries (static BSS)    */

/* ── budget ─────────────────────────────────────────────────────────── */
typedef struct {
    char     subsys[32];
    uint64_t ops_limit;        /* ops per window                          */
    uint64_t bytes_limit;      /* bytes per window                        */
    uint64_t window_ms;        /* window length                           */
    uint64_t ops_used;
    uint64_t bytes_used;
    uint64_t window_start_ms;
    uint64_t reject_count;     /* INV-XIO-02                              */
} xio_budget_t;

int  xio_budget_set(const char *subsys, uint64_t ops_limit,
                    uint64_t bytes_limit, uint64_t window_ms);
int  xio_budget_check(const char *subsys, uint64_t bytes);   /* 0=ok -1=reject */
void xio_budget_consume(const char *subsys, uint64_t bytes);
int  xio_budget_get(const char *subsys, xio_budget_t *out);

/* ── deadline ───────────────────────────────────────────────────────── */
typedef struct {
    uint64_t deadline_ms;      /* absolute CLOCK_MONOTONIC deadline       */
    uint64_t issued_ms;
    int      expired;
} xio_deadline_t;

/* INV-XIO-01: 0 = профильный default TTL (по умолчанию MAX_XIO_TTL_MS).
 * ttl_ms выше MAX_XIO_TTL_MS ограничивается потолком, сумма насыщается
 * (A1-P08-366): переполнения issued+ttl больше нет. */
void xio_deadline_set(xio_deadline_t *d, uint64_t ttl_ms);

/* 1 = дедлайн истёк ИЛИ дедлайн не установлен (d == NULL): отсутствие
 * обязательного дедлайна не даёт операции неограниченного времени
 * (A1-P08-368). Учитывает истечение ОДИН раз на объект (A1-P08-370),
 * поэтому d не const: счётчик живёт в самой структуре. */
int  xio_deadline_expired(xio_deadline_t *d);
uint64_t xio_deadline_remaining_ms(const xio_deadline_t *d);

/* Профиль задаёт значение по умолчанию до старта; 0 или >MAX = контрактная
 * константа. Без вызова поведение прежнее (A1-P08-367). */
void     xio_deadline_set_default_ttl_ms(uint64_t ttl_ms);
uint64_t xio_deadline_default_ttl_ms(void);

/* Счётчики для xio_platform_stats_get(). */
uint64_t xio_deadline_hits(void);
uint64_t xio_deadline_clock_faults(void);

/* ── audit ──────────────────────────────────────────────────────────── */
typedef struct __attribute__((packed)) {
    uint64_t ts_mono_us;
    uint32_t op_type;
    uint32_t flags;
    uint64_t subsys_id;
    uint64_t bytes;
    int32_t  result;
    uint32_t _pad;
} xio_audit_entry_t;          /* 40 bytes */

void xio_audit_record(uint32_t op_type, uint64_t subsys_id,
                      uint64_t bytes, int32_t result);
int  xio_audit_drain(xio_audit_entry_t *out, int max);

/* ── counters ───────────────────────────────────────────────────────── */
typedef struct {
    uint64_t xio_ops;
    uint64_t xio_budget_hits;
    uint64_t xio_deadline_hits;
    uint64_t xio_audit_entries;
} xio_platform_stats_t;

void xio_platform_stats_get(xio_platform_stats_t *out);
include/platx/xio_admit.h
/* platx/xio_admit.h — расширение допуска XIO: абсолютный дедлайн и отмена.
 *
 * Capability: "io.xio.admit" версия 1 (PLAT_CAP_XIO_ADMIT / PLAT_XIO_ADMIT_V1).
 * Карточки A1-W11-029 (A1-P06-270), A1-W11-030 (A1-P06-271),
 * A1-W11-034 (A1-P06-282), A1-W11-042 (A1-P06-294).
 *
 * ПОЧЕМУ ОТДЕЛЬНАЯ CAPABILITY, А НЕ ПОЛЕ В xio_req_t
 *
 * xio_req_t — часть "io.xio" версии 1. Модуль, собранный по её заголовку,
 * размещает структуру ТОГО размера и передаёт её по указателю. Дописать в
 * конец поле deadline значит: новый submit читает за границей структуры
 * старого вызывающего. Читается мусор, который на этом слое трактуется как
 * «дедлайн» — то есть допуск начинает принимать решения по неинициализированной
 * памяти и делает это НЕОТЛИЧИМО от исправной работы. Это ровно тот главный
 * класс дефектов, ради которого заведена волна, и молчаливая смена сигнатуры
 * его бы и создала.
 *
 * Поэтому v1 не трогается ни одним байтом: xio_submit() сохраняет сигнатуру,
 * поведение и коды. Новые аргументы едут отдельной структурой через отдельную
 * точку входа и отдельную capability, которую потребитель обязан ЗАПРОСИТЬ по
 * имени и версии. Потребитель, который её не запросил, не получает ни
 * дедлайна, ни отмены — и это записано ниже прямо, а не подразумевается.
 *
 * ЧЕСТНАЯ ГРАНИЦА: ЧТО ЭТОТ ДЕДЛАЙН ДЕЛАЕТ И ЧЕГО НЕ ДЕЛАЕТ
 *
 * Проверка стоит ПЕРЕД диспетчем и только там. Операция, отвергнутая по
 * истёкшему дедлайну, не выполнена вовсе: ни байта не прочитано, ни байта не
 * записано, дескриптор не тронут. Операция, УЖЕ УШЕДШАЯ в провайдер,
 * дедлайном не прерывается — прервать блокирующий read(2) можно лишь сигналом
 * или закрытием чужого дескриптора, и слой допуска этого делать не вправе.
 *
 * Отсюда следует то, что стоит сказать вслух: дедлайн имеет смысл ТОЛЬКО
 * абсолютный и ТОЛЬКО заведённый вызывающим. Дедлайн, который допуск завёл бы
 * сам в начале вызова, к моменту проверки всегда имел бы полный остаток и не
 * мог бы истечь никогда — такая «проверка» была бы украшением, а не защитой,
 * и зелёный вердикт по ней не нёс бы информации. Именно поэтому здесь нужен
 * версионированный путь: v1 нечем донести дедлайн, а не «в v1 забыли вызвать».
 *
 * INV-XIO-01 (I/O без дедлайна получает профильный TTL) исполняется тем, что
 * вызывающий заводит xio_deadline_set(&d, 0) — потолок ставит сам примитив.
 */

#define PLAT_CAP_XIO_ADMIT  "io.xio.admit"
#define PLAT_XIO_ADMIT_V1   1u

/* Условия допуска одной операции.
 *
 * abi обязателен и сверяется: структура едет по указателю через границу
 * модуля, и версия, объявленная вызывающим, — единственное, чем допуск может
 * отличить свой договор от чужого. abi != PLAT_XIO_ADMIT_V1 — это отказ
 * XIO_ERR_ARGS, а не «примем как сможем».
 *
 * deadline == NULL — операция без ограничения по времени на этом слое.
 * cancel == NULL — операция неотменяема. Оба NULL законны и означают ровно
 * поведение v1; так пишется вызывающий, которому нужен только один из двух. */
typedef struct xio_admit {
    uint32_t        abi;
    uint32_t        _pad;
    xio_deadline_t *deadline;   /* не const: истечение учитывается один раз */
    xio_cancel_t   *cancel;
} xio_admit_t;

/* Тот же допуск, что и xio_submit, плюс два шага между владельцем и
 * провайдером. Порядок: операция -> аргументы -> владелец -> дедлайн ->
 * отмена -> провайдер.
 *
 * Возвращает то же, что xio_submit, и дополнительно:
 *   XIO_ERR_TIMEOUT (errno ETIMEDOUT) — дедлайн истёк ДО диспетча;
 *   XIO_ERR_CANCEL  (errno ECANCELED) — отмена запрошена ДО диспетча.
 * В обоих случаях провайдер не вызывался и ничего не изменилось.
 *
 * adm == NULL законен и означает в точности v1. */
ssize_t xio_submit_ex(plat_owner_t owner, const struct xio_t *io,
                      const xio_req_t *req, xio_admit_t *adm);

/* ── тот же допуск на слое МАРШРУТА (A1-W11-046/047/049/050, круг 4) ──────
 *
 * Маршрут — цепочка методов, и у цепочки есть два свойства, которых нет у
 * одиночного submit: попытки и смена метода. Оба обслуживаются ОДНИМ
 * объектом на операцию:
 *
 *   • дедлайн один на всю цепочку. Второй метод получает не свежий TTL, а
 *     остаток; когда остатка нет, метод не вызывается вовсе, и операция
 *     возвращает XIO_ERR_TIMEOUT/ETIMEDOUT, а не errno предыдущего метода.
 *     Наблюдаемо через xio_route_attempts();
 *   • бюджет один на всю операцию. Каждая следующая попытка — retry по тому
 *     же билету (потолок A1-P09-415), а не новый резерв.
 *
 * ГРАНИЦА, названная прямо: проверка стоит ПЕРЕД каждой попыткой и только
 * там. Метод, уже ушедший в ядро, не прерывается — прервать блокирующий
 * copy_file_range нечем.
 *
 * adm == NULL законен и означает поведение без дедлайна и без отмены.
 * v1-имена (xio_route_read/write/transfer) не меняются ни байтом и
 * делегируют сюда с adm == NULL. */
ssize_t xio_route_read_ex (const char *subsys, int fd, void *buf, size_t n,
                           xio_admit_t *adm);
ssize_t xio_route_write_ex(const char *subsys, int fd, const void *buf,
                           size_t n, xio_admit_t *adm);
ssize_t xio_route_transfer_ex(const char *subsys, int dst_fd, int src_fd,
                              size_t n, xio_admit_t *adm);

/* Таблица расширения. Отдельная от plat_xio_api_t: дописать слот в конец той
 * таблицы значило бы, что модуль, скопировавший её по старому sizeof, читает
 * указатель из-за границы своей копии. */
typedef struct plat_xio_admit_api {
    uint32_t abi;               /* PLAT_XIO_ADMIT_V1 */
    ssize_t (*submit_ex)(plat_owner_t owner, const struct xio_t *io,
                         const xio_req_t *req, xio_admit_t *adm);
} plat_xio_admit_api_t;

extern const plat_xio_admit_api_t plat_xio_admit_api;
include/platx/xio_api.h
/* platx/xio_api.h — Core I/O surface as seen by a module.
 *
 * Capability: "io.xio" version 1 (PLAT_CAP_XIO / PLAT_XIO_V1).
 * Concrete xio_t lives in the worker — this header does not include xio.h.
 * CHILD still reaches I/O only through get() (proxy). Do not hand a
 * worker xio_t* across the socket; that is a half-state.
 *
 * own_fd / release_owner / stats_text / submit may be NULL: not offered.
 * Check before call. Missing pointer is not success.
 */
typedef SSIZE_T ssize_t;
#define _SSIZE_T_DEFINED

#define PLAT_CAP_XIO  "io.xio"
#define PLAT_XIO_V1   1u

struct xio_t;

/* One request, filled by the caller, for the one dispatch below.
 *
 * A struct rather than eight entry points or a va_list: eight signatures
 * would need eight owner gates, and the gate is the whole point of this
 * path -- one of them would eventually be written without it. Only fields the
 * table actually uses are here; a field kept "for later" is a field nobody
 * checks.
 *
 * buf is the destination for read and recv and the source for write and send.
 * path and mode belong to OPEN, addr and addrlen to CONNECT and ACCEPT --
 * CONNECT reads the length through the pointer, ACCEPT writes it back.
 */
typedef struct xio_req {
    xio_op_id_t op;
    int         fd;
    void       *buf;
    size_t      len;
    int         flags;      /* MSG_* for recv/send, O_* for open */
    const char *path;       /* OPEN */
    int         mode;       /* OPEN, honoured by the slot only with O_CREAT */
    struct sockaddr *addr;  /* CONNECT, ACCEPT */
    socklen_t  *addrlen;    /* CONNECT reads it; ACCEPT writes it back */
} xio_req_t;

typedef struct plat_xio_api {
    const struct xio_t *(*get)(const char *subsys, int category);
    /* Optional. NULL = not offered. gen 0 must refuse own. */
    int (*own_fd)(int fd, plat_owner_t owner);
    /* Optional. Sweep fds of this owner; gen 0 is a no-op. */
    int (*release_owner)(plat_owner_t owner);
    /* Optional. Write dump lines into buf. */
    int (*stats_text)(char *buf, size_t n);
    /* Optional. The owner-checked synchronous path: whoever claimed the fd is
     * the one who may move bytes on it, and anyone else is refused before the
     * syscall rather than after it. NULL = not offered, which is not success;
     * check the pointer, as with own_fd. */
    ssize_t (*submit)(plat_owner_t owner, const struct xio_t *io,
                      const xio_req_t *req);
} plat_xio_api_t;

/* The dispatch behind the optional slot above. Declared here so a module that
 * links the layer directly reaches the same function the ABI offers, rather
 * than a second one that happens to share its name.
 *
 * Returns what the slot returned, or a negative named code from
 * platx/xio_err.h: XIO_ERR_ARGS for a dead epoch, a bad fd or an operation
 * this table does not know; XIO_ERR_OWNER_STALE when the fd belongs to another
 * owner, in which case nothing was read, written or closed; XIO_ERR_UNAVAIL
 * when the chosen vtable has no such slot -- never a quiet fall back to libc.
 *
 * It does not choose a backend and does not claim the fd an OPEN returns. */
ssize_t xio_submit(plat_owner_t owner, const struct xio_t *io,
                   const xio_req_t *req);

/* Core table. CHILD proxy does not export this symbol. */
extern const plat_xio_api_t plat_xio_api;
include/platx/xio_budget.h
/* include/platx/xio_budget.h — атомарный бюджет XIO: reserve / settle.
 *
 * A1-P09-402..417. Заменяет пару check-then-consume, которая по своей
 * СИГНАТУРЕ не может быть атомарной: между «можно?» и «списал» лежит окно, в
 * которое влезает любое число конкурентных заявок. Старая пара оставлена в
 * platx_xio.h ради замороженного ABI и объявлена там неатомарной; новый код
 * обязан ходить через reserve/settle.
 *
 * Модель. Заявитель СНАЧАЛА резервирует верхнюю оценку расхода и получает
 * билет, ПОТОМ выполняет операцию, ПОТОМ гасит билет фактически потраченным.
 * Резерв виден лимиту немедленно, поэтому сумма одновременно допущенных
 * заявок не превышает окно ни в одной точке времени. Незакрытый билет — это
 * удержанный бюджет, а не потерянный: его возвращает release.
 *
 * Почему билет — значение, а не указатель. Указатель на слот пережил бы сам
 * слот: владелец ушёл, слот переиспользован, а старый билет всё ещё «знает»
 * адрес. Билет несёт индекс И эпоху слота; после освобождения слота эпоха
 * растёт, и любой билет прежней эпохи отвергается (A1-P09-411, 435).
 *
 * SPDX-License-Identifier: GPL-2.0
 */

/* Ключ подсистемы: 31 значащий символ + NUL.
 *
 * A1-P09-404. Прежний поиск сравнивал strncmp(...,31) по массиву char[32]:
 * два РАЗНЫХ имени с общим 31-символьным префиксом попадали в один слот и
 * молча делили чужую квоту. Здесь длина проверяется на входе: имя длиннее
 * лимита — это отказ XIO_BUDGET_KEY_TOO_LONG, а не усечение. Усечение —
 * способ получить коллизию, а не способ её избежать. */
#define XIO_BUDGET_KEY_MAX   32u   /* включая NUL */
#define XIO_BUDGET_KEY_CHARS 31u   /* значащих символов */

/* Исходы. Каждый отличим: «нет такого бюджета», «нет места в таблице» и
 * «лимит исчерпан» — три разных решения, и сводить их к -1 значит лишить
 * вызывающего возможности отреагировать по-разному (A1-P09-405, 410). */
typedef enum {
    XIO_BUDGET_OK            = 0,
    XIO_BUDGET_DENIED_OPS    = 1,  /* исчерпан счётчик операций окна      */
    XIO_BUDGET_DENIED_BYTES  = 2,  /* исчерпан байтовый лимит окна        */
    XIO_BUDGET_DENIED_PLAT   = 3,  /* уперлись в общий потолок платформы  */
    XIO_BUDGET_DENIED_TRIES  = 4,  /* исчерпан потолок попыток операции   */
    XIO_BUDGET_NOT_REGISTERED= 5,  /* слот не заведён (см. класс ниже)    */
    XIO_BUDGET_TABLE_FULL    = 6,  /* нет свободного слота                */
    XIO_BUDGET_BAD_ARG       = 7,
    XIO_BUDGET_KEY_TOO_LONG  = 8,
    XIO_BUDGET_STALE         = 9,  /* билет/владелец из прошлой эпохи     */
    XIO_BUDGET_UNMEASURABLE  = 10, /* часы отказали — см. ниже            */
    XIO_BUDGET_RESV_FULL     = 11  /* нет места под ещё один резерв       */
} xio_budget_rc_t;

/* Класс потребителя.
 *
 * A1-P09-405. Незарегистрированный бюджет НЕ означает «безлимитно». Означать
 * он может ровно одно из двух, и выбор делает потребитель при регистрации
 * намерения, а не библиотека молча:
 *
 *   OPTIONAL  — подсистема бюджетом не ограничена; отсутствие слота есть
 *               разрешение. Так вела себя старая xio_budget_check для ВСЕХ.
 *   MANDATORY — подсистема обязана иметь бюджет; отсутствие слота есть
 *               отказ NOT_REGISTERED. Это единственный способ отличить
 *               «политика разрешила» от «политику забыли применить».
 *
 * A1-P09-416: CLEANUP — заявка на освобождение/отмену/дренаж. Ей доступен
 * профильный резерв, закрытый для рабочих заявок, иначе исчерпанный рабочий
 * бюджет запирает и саму уборку. */
typedef enum {
    XIO_BUDGET_CLASS_OPTIONAL  = 0,
    XIO_BUDGET_CLASS_MANDATORY = 1,
    XIO_BUDGET_CLASS_CLEANUP   = 2
} xio_budget_class_t;

/* Билет резерва. Значение, копируемое по стеку; указателей внутри нет.
 *
 * Билет — это ССЫЛКА на резерв, а не сам резерв. Сам резерв (сколько байт
 * удержано, в каком окне, чьим слотом) живёт в таблице модуля. Так сделано
 * не для красоты:
 *
 *   — билет одноразовый. Повторное settle того же билета находит запись уже
 *     снятой и возвращает STALE вместо второго списания. Первая версия этого
 *     модуля хранила bytes в билете, и повторное погашение засчитывало
 *     расход дважды; поймано собственной фикстурой, не чтением;
 *   — билет неподделываем в части чисел. Вызывающий может испортить свою
 *     копию, но счётчики считаются по записи модуля, а не по тому, что
 *     принесли. Билет с несуществующим id — просто STALE.
 *
 * Поля здесь ЧИТАЮТСЯ вызывающим (окно, попытка), но ни одно из них не
 * является источником истины для учёта. */
typedef struct xio_budget_ticket {
    uint32_t resv;        /* индекс записи резерва + 1; 0 — недействителен  */
    uint32_t attempt;     /* номер попытки внутри логической операции (415) */
    uint64_t resv_id;     /* поколение записи: отличает новый резерв от
                           * погашенного, занявшего тот же индекс           */
    uint64_t owner_gen;   /* поколение владельца (A1-P09-403)              */
    uint64_t window_seq;  /* окно, которому принадлежит резерв (A1-P09-409) */
    uint64_t bytes;       /* зарезервировано байт — СПРАВОЧНО               */
} xio_budget_ticket_t;

/* Наблюдаемое состояние слота. Резерв и расход РАЗДЕЛЕНЫ намеренно:
 * A1-P09-412 требует, чтобы «сколько просили» и «сколько подтверждено» были
 * видны по отдельности, иначе частичная запись неотличима от полной. */
typedef struct xio_budget_view {
    char     subsys[XIO_BUDGET_KEY_MAX];
    uint64_t owner_gen;
    uint64_t ops_limit;
    uint64_t bytes_limit;
    uint64_t window_ms;
    uint64_t ops_settled;      /* завершённых операций в текущем окне      */
    uint64_t bytes_settled;    /* подтверждённых байт в текущем окне       */
    uint64_t ops_inflight;     /* незакрытых резервов                       */
    uint64_t bytes_inflight;   /* байт под незакрытыми резервами            */
    /* A1-P09-412 + A1-W11 круг 3: нарастающая сумма байт по ДОПУЩЕННЫМ
     * резервам. Прежняя формулировка «запрошено байт всего» читалась шире
     * кода: отвергнутая заявка сюда НЕ попадает, она в reject_bytes. */
    uint64_t bytes_requested;
    uint64_t window_seq;
    uint64_t reject_ops;
    uint64_t reject_bytes;
    uint64_t reject_plat;
    uint64_t reject_tries;
    uint64_t carry_settled;    /* погашено билетов прошлых окон (409)       */
    uint32_t cls;
    uint32_t epoch;
} xio_budget_view_t;

/* Профильные потолки платформы (A1-P09-417). Ноль = «не задано» и НЕ значит
 * «безгранично» для MANDATORY-потребителей: см. xio_budget_reserve. */
typedef struct xio_budget_platform_caps {
    uint64_t max_inflight_bytes;  /* суммарно по всем владельцам */
    uint64_t max_inflight_ops;
    uint64_t cleanup_reserve_bytes; /* карман уборки (A1-P09-416) */
    uint64_t cleanup_reserve_ops;
    uint32_t max_attempts;        /* потолок попыток на операцию (415) */
    uint32_t max_slots;           /* потолок слотов (A1-P09-410) */
    uint32_t max_reservations;    /* потолок одновременных резервов (410) */
    uint32_t _pad;
} xio_budget_platform_caps_t;

/* ── жизненный цикл слота ─────────────────────────────────────────────── */

/* Завести/переопределить бюджет. window_ms == 0 отвергается: окно нулевой
 * длины означает «лимит на бесконечно малое время», то есть не лимит
 * (A1-P09-406). ops_limit/bytes_limit == 0 означает «эта ось не ограничена»
 * и это ЯВНОЕ разрешение, а не пропуск. */
xio_budget_rc_t xio_budget_declare(const char *subsys, uint64_t owner_gen,
                                   xio_budget_class_t cls,
                                   uint64_t ops_limit, uint64_t bytes_limit,
                                   uint64_t window_ms);

/* Применить профильные потолки. Вызывается один раз после публикации
 * профиля; повтор с другим содержимым отвергается (A1-P09-424 по духу:
 * потолок не переопределяется в рантайме несогласованным путём). */
xio_budget_rc_t xio_budget_platform_apply(const xio_budget_platform_caps_t *c);

/* Освободить слот после дренажа владельца. Итоговые счётчики сохраняются и
 * доступны через xio_budget_final(); эпоха растёт, поэтому ни один билет
 * прежнего владельца больше не гасится (A1-P09-411). */
xio_budget_rc_t xio_budget_owner_drain(const char *subsys, uint64_t owner_gen);

/* Итоговые счётчики освобождённого слота. -1 если такого нет. */
int xio_budget_final(const char *subsys, xio_budget_view_t *out);

/* ── резерв и погашение ───────────────────────────────────────────────── */

/* Зарезервировать верхнюю оценку расхода.
 *
 * Возвращает XIO_BUDGET_OK и заполняет *out, либо код отказа; при отказе
 * *out обнуляется, и его slot == 0.
 *
 * Часы. Если монотонные часы не читаются, окно посчитать нельзя. Тогда
 * возвращается UNMEASURABLE и заявка НЕ допускается: допустить её значило бы
 * ответить «в лимит укладываемся» на вопрос, на который мы не ответили. */
xio_budget_rc_t xio_budget_reserve(const char *subsys, uint64_t owner_gen,
                                   xio_budget_class_t cls, uint64_t bytes,
                                   xio_budget_ticket_t *out);

/* Тот же резерв для вызывающего, у которого нет под рукой поколения
 * владельца: оно берётся из самого слота.
 *
 * Существует ради продуктовой привязки (A1-P09-418). Транспортный путь знает
 * имя подсистемы — оно уже лежит в xio_route_req_t, — но поколения владельца
 * не носит, и протаскивать его через всю цепочку ради бюджета значило бы
 * менять контракт маршрутизации ради учёта.
 *
 * Что этот вход НЕ даёт: он не проверяет, что заявитель и есть владелец, —
 * проверять нечем. Он даёт только «этот слот сейчас принадлежит поколению G,
 * резервируем от имени G». Между чтением G и резервом владелец может
 * смениться; тогда вернётся STALE, и это верный ответ. Для путей, которые
 * поколение ЗНАЮТ, правильный вход — xio_budget_reserve, и он строже. */
xio_budget_rc_t xio_budget_reserve_for(const char *subsys,
                                       xio_budget_class_t cls, uint64_t bytes,
                                       xio_budget_ticket_t *out);

/* Повторная попытка ВНУТРИ той же логической операции (A1-P09-415).
 * Билет переиспользуется, счётчик попыток растёт; за потолком — отказ
 * DENIED_TRIES, и билет при этом гасится release'ом вызывающего. */
xio_budget_rc_t xio_budget_retry(xio_budget_ticket_t *t);

/* Погасить билет фактически потраченным. committed <= зарезервированного;
 * больше — BAD_ARG, и НИЧЕГО не списывается (иначе завышенное погашение
 * стало бы способом обойти лимит). Разница возвращается в окно.
 *
 * Одна операция засчитывается всегда, даже при committed == 0: операция
 * состоялась, и её стоимость в счётчике операций — факт (A1-P09-414). */
xio_budget_rc_t xio_budget_settle(const xio_budget_ticket_t *t,
                                  uint64_t committed_bytes);

/* Вернуть резерв целиком, не засчитывая операцию: заявка отменена ДО того,
 * как провайдер что-либо сделал (A1-P09-413). */
xio_budget_rc_t xio_budget_release(const xio_budget_ticket_t *t);

/* ── наблюдение ───────────────────────────────────────────────────────── */

int xio_budget_view(const char *subsys, xio_budget_view_t *out);
uint64_t xio_budget_reject_total(void);

/* Наблюдённый максимум одновременно открытых резервов (A1-P09-447).
 * Это измерение на снятых прогонах, а НЕ верхняя граница и не WCET. */
uint32_t xio_budget_resv_high_water(void);

/* Текущее удержание по платформе (A1-P09-417). Нужно, чтобы возврат места
 * при дренаже владельца можно было ПРОВЕРИТЬ, а не предположить. */
void xio_budget_platform_inflight(uint64_t *ops, uint64_t *bytes);

/* Шов для фикстур: подменить источник монотонного времени.
 *
 * Отказ clock_gettime(CLOCK_MONOTONIC) на Linux практически недостижим, и
 * ветка «часы не читаются» без такого шва остаётся НЕПРОВЕРЯЕМОЙ — то есть
 * написанной на веру. Мутационная батарея это и показала: подмена отказа
 * часов на «время ноль» выживала, потому что её нечем было поймать.
 * fn == NULL возвращает штатные часы. В продукте не вызывается. */
void xio_budget_set_clock_for_test(int (*fn)(uint64_t *out));

/* Только для фикстур: полный сброс статики. В продукте не вызывается. */
void xio_budget_reset_for_test(void);
include/platx/xio_cancel.h
/* platx/xio_cancel.h — токен отмены XIO.
 *
 * A1-W11-030 (A1-P06-271). Круги 1 и 2 записали честно: токена отмены в XIO
 * НЕ БЫЛО ВОВСЕ — ни типа, ни поля, ни вызова. Карточка формулировалась как
 * «передавать существующий токен в backend-пути», и такая формулировка
 * скрывала, что передавать нечего. Здесь он создаётся.
 *
 * ЧТО ЭТО И ЧЕМ НЕ ЯВЛЯЕТСЯ
 *
 * Токен — это ОДНОБИТОВОЕ, ОДНОНАПРАВЛЕННОЕ, ВЛАДЕЛЬЧЕСКОЕ намерение
 * остановиться. Он не прерывает syscall в полёте: прервать блокирующий
 * read(2) можно только сигналом или закрытием дескриптора, и делать это из
 * общего слоя допуска значило бы разрушать чужое состояние. Он отвечает на
 * один вопрос и отвечает на него ДО диспетча: «эту операцию ещё хотят?».
 * Операция, отменённая до диспетча, не выполнена вовсе — это ровно то
 * состояние, которое Конституция называет определённым, в отличие от
 * полусостояния «syscall ушёл, а результат никому не нужен».
 *
 * Однонаправленность обязательна: снятие отмены вернуло бы гонку, в которой
 * два наблюдателя одного токена видят разные ответы. Отменённый токен
 * отменён навсегда; для следующей операции заводится новый.
 *
 * Владелец обязателен: отменять операцию вправе только тот, кто её завёл, и
 * только в своей эпохе. Отмена из чужой эпохи — это тот же устаревший
 * владелец, что и в claim-таблице, и ответ у неё тот же.
 *
 * §SEC-3: без malloc. Токен размещает вызывающий.
 */

/* Поля не читать напрямую: state атомарно, и обычное чтение — гонка. */
typedef struct xio_cancel {
    uint32_t     state;     /* 0 = живой, 1 = отменён. Атомарно.        */
    plat_owner_t owner;     /* generation 0 = токен не инициализирован  */
} xio_cancel_t;

/* Завести токен на эпоху владельца. generation == 0 отвергается: нулевая
 * эпоха ничем не владеет и ничего не отменяет. 0 = ок, -1 = аргументы. */
int xio_cancel_init(xio_cancel_t *t, plat_owner_t owner);

/* Запросить отмену. Право есть только у владельца в той же эпохе.
 *  0 = отмена зафиксирована (или уже была);
 * -1 = t == NULL / токен не инициализирован;
 * -2 = чужой владелец или устаревшая эпоха — отмена НЕ зафиксирована. */
int xio_cancel_request(xio_cancel_t *t, plat_owner_t who);

/* 1 = отмена запрошена. NULL = 0: отсутствие токена означает «операцию не
 * отменяли», а не «неизвестно» — иначе допуск не имел бы определённого
 * ответа. */
int xio_cancel_pending(const xio_cancel_t *t);

/* Сколько раз слой допуска отказал по отмене. Для xio explain и улик. */
uint64_t xio_cancel_hits(void);
include/platx/xio_err.h
/* platx/xio_err.h — distinct XIO v1 status codes.
 * Callers must tell submit / complete / stale-owner / timeout apart.
 * A single -1 hides the failure and blocks a clean keep-or-close.
 */

enum {
    XIO_OK              = 0,
    XIO_ERR_SUBMIT      = -1001,
    XIO_ERR_COMPLETE    = -1002,
    XIO_ERR_UNAVAIL     = -1003,
    XIO_ERR_CANCEL      = -1004,
    XIO_ERR_OWNER_STALE = -1005,
    XIO_ERR_TIMEOUT     = -1006,
    XIO_ERR_ARGS        = -1007
};

const char *xio_err_str(int e);
include/platx/xio_hook.h
/* platx/xio_hook.h — the XIO hook ABI: three phases, fail-closed.
 *
 * A hook answers "what is happening, and how unusual is it" for one operation.
 * It does not execute the operation, it does not choose a provider, and it
 * does not decide lifecycle or recovery -- those are three other jobs, and a
 * hook that reached into any of them would be a second brain making decisions
 * the platform already makes elsewhere.
 *
 * Three phases, and deliberately not a fourth:
 *
 *   XIO_HOOK_PRE       before the operation is submitted   (observe / limit)
 *   XIO_HOOK_PROVIDER  around the backend that was chosen  (classify)
 *   XIO_HOOK_POST      after the operation completed       (inspect / audit)
 *
 * Fail-closed is the whole point. A hook that is not attached costs nothing
 * and changes nothing. A hook that IS attached and then breaks -- returns a
 * value outside the contract, or re-enters this dispatcher -- does not leave
 * the operation "almost fine": the run returns a named negative code and the
 * caller must not report success. There is no path where a broken hook is
 * quietly skipped, because that is the same as not having written it.
 *
 * What a callback never receives: a struct xio_t *. The context below carries
 * the fields of the operation and nothing that can be used to perform I/O.
 * That is what keeps this usable from a CHILD, where handing over a host
 * xio_t * would be a half-state rather than a capability.
 *
 * The slot table lives inside XIO. It is bounded and static -- not a fifth
 * platform registry, and no allocation on the path a callback runs on.
 */

/* Bounded, per phase. A table this size is a design statement: hooks are for
 * a handful of classifiers, not for an open-ended subscription list. */
#define XIO_HOOK_SLOTS_PER_PHASE 8u

typedef enum xio_hook_phase {
    XIO_HOOK_PRE      = 0,
    XIO_HOOK_PROVIDER = 1,
    XIO_HOOK_POST     = 2,
    XIO_HOOK_PHASE__COUNT
} xio_hook_phase_t;

/* What the slot's owner wants done when its own callback breaks.
 *
 * KEEP_OLD only means something when there is an "old" to keep: a live fd that
 * must not be swapped, a new descriptor that must not be applied, a buffer
 * that must not be handed over as valid. For a plain file.read there is no old
 * channel, so KEEP_OLD there is REFUSE by another name -- the caller must not
 * invent bytes to return. Both outcomes are refusals; they differ only in what
 * the caller is expected to preserve. */
typedef enum xio_hook_policy {
    XIO_HOOK_REFUSE   = 0,
    XIO_HOOK_KEEP_OLD = 1
} xio_hook_policy_t;

/* What a callback returns. Anything else is treated as XIO_HOOK_FAIL: an
 * unrecognised verdict is a broken hook, not a permissive one. */
typedef enum xio_hook_rc {
    XIO_HOOK_OK    = 0,   /* observed; the operation may proceed          */
    XIO_HOOK_LIMIT = 1,   /* deliberate refusal: do not execute / deliver */
    XIO_HOOK_FAIL  = 2    /* the hook itself broke                        */
} xio_hook_rc_t;

/* Codes returned by xio_hook_run(). They sit outside the XIO v1 range in
 * platx/xio_err.h on purpose: a caller that logs "owner stale" when a
 * classifier said "too much" would be reporting the wrong event. */
enum {
    XIO_HOOK_ERR_LIMIT    = -1101, /* a slot returned LIMIT                */
    XIO_HOOK_ERR_FAILED   = -1102, /* a slot broke; its policy was REFUSE  */
    XIO_HOOK_ERR_KEEP_OLD = -1103  /* a slot broke; its policy was KEEP_OLD*/
};

/* The operation, as a hook is allowed to see it.
 *
 * Only fields that a caller on the working path actually has. A field kept
 * "for later" is a field nobody fills and everybody reads. */
typedef struct xio_hook_ctx {
    xio_op_id_t  op;
    int          fd;
    size_t       len;        /* bytes requested; in POST, bytes moved      */
    int          flags;
    int          category;   /* legacy XIO_CATEGORY_*, as passed to get()  */
    const char  *subsys;     /* may be NULL                                */
    plat_owner_t owner;      /* the operation's owner, not the slot's      */
    const char  *provider;   /* PROVIDER/POST: what actually runs, or NULL */
    ssize_t      result;     /* POST: what the provider returned; else 0   */
} xio_hook_ctx_t;

/* Never given a struct xio_t *: a hook classifies, it does not perform I/O. */
typedef int (*xio_hook_fn)(const xio_hook_ctx_t *ctx, void *user);

typedef struct xio_hook_stats {
    uint64_t attached;       /* slots currently held                      */
    uint64_t attach_refused; /* gen 0, bad args, table full               */
    uint64_t ran;            /* callbacks actually invoked                */
    uint64_t limited;        /* runs that ended in LIMIT                  */
    uint64_t failed;         /* runs that ended in FAIL (either policy)   */
} xio_hook_stats_t;

/* Attach a callback to one phase.
 *
 * Returns a non-negative slot id, or a named code:
 *   XIO_ERR_ARGS    — NULL callback, unknown phase or policy, or an owner
 *                     with generation 0. Generation 0 is refused here for the
 *                     same reason it is refused an fd claim: there is no live
 *                     epoch to end, so the slot could never be swept.
 *   XIO_ERR_UNAVAIL — the phase's table is full (errno ENOSPC). Nothing is
 *                     evicted: every row belongs to a live owner.
 */
int xio_hook_attach(xio_hook_phase_t phase, xio_hook_fn cb, void *user,
                    plat_owner_t owner, xio_hook_policy_t policy);

/* Release one slot. Only its own owner, same generation, may:
 *   0                   — released
 *   XIO_ERR_ARGS        — unknown slot id, or the slot is empty
 *   XIO_ERR_OWNER_STALE — the slot belongs to a different owner or epoch
 */
int xio_hook_detach(int slot, plat_owner_t owner);

/* The DESTROY sweep: drop every slot of this owner. Returns how many were
 * dropped. Generation 0 is a no-op, because nothing could have been attached
 * under it. */
int xio_hook_release_owner(plat_owner_t owner);

/* How many slots are attached to a phase (-1 for an unknown phase). */
int xio_hook_count(xio_hook_phase_t phase);

/* Run one phase.
 *
 * With nothing attached this is a no-op returning XIO_OK -- that is the only
 * "free" case. Slots run in attach order and the first non-OK verdict stops
 * the chain: once the answer is a refusal, the remaining classifiers cannot
 * turn it back into a success, and running them would only widen the window
 * in which the caller is still holding an operation it must not perform.
 *
 * Returns XIO_OK, or XIO_HOOK_ERR_LIMIT / XIO_HOOK_ERR_FAILED /
 * XIO_HOOK_ERR_KEEP_OLD, or XIO_ERR_ARGS for a bad phase or a NULL context.
 * Re-entering this function from inside a callback returns
 * XIO_HOOK_ERR_FAILED without running anything: a hook that calls the
 * dispatcher is not observing an operation any more.
 */
int xio_hook_run(xio_hook_phase_t phase, const xio_hook_ctx_t *ctx);

/* The POST rule, written once so no call site has to remember it.
 *
 * If a POST hook refused after the provider already moved bytes into the
 * caller's buffer, those bytes are not a valid result -- returning the
 * positive count would be exactly the silent partial success this layer
 * exists to prevent. Returns the provider's result only when the hook chain
 * was clean, and the hook's own negative code otherwise. */
ssize_t xio_hook_post_result(ssize_t provider_result, int hook_rc);

/* Names for the codes above; falls through to xio_err_str() for XIO v1
 * codes, so one printf covers both vocabularies. */
const char *xio_hook_err_str(int e);

void xio_hook_stats_get(xio_hook_stats_t *out);

/* Test-only: drop every slot in every phase. Not a product path -- a module
 * ends its own epoch with xio_hook_release_owner. */
void xio_hook_reset_all(void);
include/platx/xio_ops.h
/* platx/xio_ops.h — the typed-operation table, before any provider ABI.
 *
 * This is a table, not a router. It gives every operation a stable id, a name
 * and a class, so that a later provider contract can be keyed on something
 * that does not move. Deciding which backend runs an operation is a separate
 * job that does not exist yet, and nothing here does it.
 *
 * Two vocabularies live side by side, and merging them would be a lie:
 *
 *   - the operation class below (file, net.stream, memory, ...) describes what
 *     the operation *is*, and comes from the roadmap's type system;
 *   - the legacy XIO_CATEGORY_* argument of xio_get_for() describes what the
 *     caller intends, in the older and coarser vocabulary the tree ships today.
 *
 * They overlap but are not the same, so the mapping between them is written
 * out explicitly rather than assumed.
 *
 * The op table lists operations that exist in the vtable and have a caller in
 * src/. Classes with no ops yet are still named, because the vocabulary is the
 * contract; an empty class is a gap that is visible, not coverage that is
 * claimed. Adding an id here does not add a vtable pointer, and must not be
 * read as one.
 */
/* What the operation is. The roadmap's type system, named in full so the gaps
 * are legible: today only FILE and NET_STREAM have operations behind them. */
typedef enum xio_op_class {
    XIO_CLASS_FILE = 0,      /* open, read, write, close */
    XIO_CLASS_FILE_TRANSFER, /* copy src->dst; no caller yet */
    XIO_CLASS_NET_STREAM,    /* connect, send, recv, accept */
    XIO_CLASS_NET_PACKET,    /* rx, tx; not stream, and not here yet */
    XIO_CLASS_KERNEL_CONTROL,/* route.query, link.configure; no caller yet */
    XIO_CLASS_FS_WATCH,      /* no caller yet */
    XIO_CLASS_MEMORY,        /* create, map, seal; no typed op yet */
    XIO_CLASS_PROCESS,       /* wait, pidfd; no typed op yet */
    XIO_CLASS_EVENT,         /* timer, eventfd, signal; no typed op yet */
    XIO_CLASS__COUNT
} xio_op_class_t;

/* Stable ids. These deliberately share their numeric values with the internal
 * operation enum the future path already uses; a static assertion in the
 * implementation keeps the two from drifting apart silently. */
typedef enum xio_op_id {
    XIO_OP_ID_READ    = 0,
    XIO_OP_ID_WRITE   = 1,
    XIO_OP_ID_RECV    = 2,
    XIO_OP_ID_SEND    = 3,
    XIO_OP_ID_ACCEPT  = 4,
    XIO_OP_ID_CONNECT = 5,
    XIO_OP_ID_OPEN    = 6,
    XIO_OP_ID_CLOSE   = 7,

    /* Everything above has a pointer in xio_t and shares its number with the
     * internal op enum; a static assertion in the implementation holds that
     * line. This is where that correspondence stops. */
    XIO_OP_ID__VTABLE_COUNT,

    /* Operations the resolver serves without a vtable slot of their own.
     * They are not missing pointers: there is nothing to add to xio_t,
     * because each of these is a chain of ordinary syscalls chosen per call
     * -- copy_file_range or sendfile or a read/write loop, memfd_create or an
     * anonymous file. Giving them a slot would mean picking one of those at
     * build time, which is the decision the resolver exists to make. */
    XIO_OP_ID_TRANSFER = XIO_OP_ID__VTABLE_COUNT, /* file.transfer   */
    XIO_OP_ID_MEM_CREATE,                          /* memory.create   */
    XIO_OP_ID_PROC_PIDFD,                          /* process.pidfd   */
    XIO_OP_ID_FS_WATCH,                            /* fs.watch        */
    XIO_OP_ID__COUNT
} xio_op_id_t;

/* Names are the dotted form from the roadmap: "file.read", "net.stream.send".
 * NULL for an id outside the table -- never a placeholder string, because a
 * placeholder is what ends up printed in an operator's report. */
const char    *xio_op_name(xio_op_id_t id);
xio_op_class_t xio_op_class(xio_op_id_t id);

/* -1 when the name is not in the table. */
int            xio_op_id_from_name(const char *name);

const char    *xio_op_class_name(xio_op_class_t cls);

/* The bridge to the older argument. An op maps onto exactly one legacy
 * category; the reverse does not hold, which is why only this direction is
 * offered. */
int            xio_op_legacy_category(xio_op_id_t id);

/* ── provider identity ─────────────────────────────────────────────────────
 *
 * The mode tokens an operator types are historical, and three of them say
 * "uring" while running a thread pool. Renaming them would break every script
 * in the field, so instead the truth is written down: the token stays, and the
 * identity says which provider actually runs and whether a real ring is
 * involved. Anything that reports a backend should report this, not the token.
 */
typedef struct xio_provider_info {
    const char *token;      /* what `xio mode` accepts */
    const char *provider;   /* what actually executes */
    const char *method;     /* how, within that provider */
    int         real_ring;  /* 1 only where an io_uring ring is really used */
} xio_provider_info_t;

/* NULL for an unknown token: an unknown backend is not a default one. */
const xio_provider_info_t *xio_provider_of(const char *mode_token);

/* ── category, as an argument that is now checked ──────────────────────────
 *
 * The category argument used to be discarded. It is now validated, which is
 * the whole of this step: a value outside the vocabulary is refused instead of
 * silently accepted, and a category no provider of that kind can serve is
 * refused instead of answered with a vtable whose relevant slots are NULL.
 *
 * It still does not route. Choosing a provider from the operation is the
 * resolver's job, and the resolver does not exist yet.
 */
int xio_category_valid(int category);
const char *xio_category_name(int category);

/* 0 when this provider can serve that category, or a negative errno saying
 * why not. */
int xio_category_supported_by(int category, const char *provider);

/* ── explain ───────────────────────────────────────────────────────────────
 *
 * Reports the decision that was already made. It chooses nothing: there is one
 * selector, and this asks it the same question a caller would ask, then says
 * what came back. A report that worked anything out for itself would be a
 * second opinion, and the moment the two disagreed the wrong one would be the
 * one on screen.
 *
 * Because it really asks, asking has the same effects as asking: a backend
 * counter moves, and a mode whose backend initialises lazily will initialise.
 * That is the price of the answer being true.
 *
 * The mode token is reported as it is, not cleaned up. Three of the tokens say
 * "uring" over a thread pool; the provider fields say what actually runs, and
 * the rendered text points at the discrepancy rather than hiding it behind a
 * nicer name.
 */
typedef struct xio_explain {
    char        subsys[64];
    int         category;
    const char *category_name;   /* NULL when the category is not one */
    char        mode_token[32];  /* verbatim, historical names included */
    const char *provider;        /* NULL when the token is unknown */
    const char *method;
    int         real_ring;
    int         accepted;        /* 0 when the request was refused */
    const char *refusal;         /* why, or NULL when it was served */
    int         err;             /* errno left by the selector */
    const void *vtable;          /* exactly what the selector returned */
} xio_explain_t;

/* 0 on success, -1 only when out is NULL. A refused request is not an error
 * here: the refusal is the thing being reported. */
int xio_explain(const char *subsys, int category, xio_explain_t *out);

/* Renders an answer that was already obtained. Split out so a caller that also
 * needs the verdict -- a command deciding its exit status, say -- can ask the
 * selector once instead of twice; asking twice would double the side effects
 * described above for no reason. Returns the length that was needed, so a
 * short buffer is detectable rather than silently truncated into a shorter
 * truth; -1 on bad arguments. */
int xio_explain_render(const xio_explain_t *e, char *buf, size_t n);

/* Ask and render in one step. */
int xio_explain_text(const char *subsys, int category, char *buf, size_t n);
include/platx/xio_route.h
/* platx/xio_route.h — the resolver: which method runs this operation, and why.
 *
 * Until now XIO had one mode per subsystem, set by hand. That is a choice made
 * once at configuration time for every operation a module will ever perform,
 * which is the wrong granularity: reading four bytes of a config file and
 * reading a 400 MB image are the same `file.read` to the mode table, and no
 * single backend is right for both. The resolver picks per call, from the
 * operation and its size, and it picks a *chain* rather than a winner: the
 * first method that cannot serve this particular request steps aside and the
 * next one runs.
 *
 * Two rules keep that from becoming a silent-fallback machine, which is worse
 * than no fallback at all:
 *
 *   - A method may be skipped only for reasons that mean "not applicable
 *     here" -- the kernel has no such syscall, this descriptor is a pipe, the
 *     two files are on different filesystems. Those are ENOSYS, ENOTSUP,
 *     EOPNOTSUPP, EXDEV, ESPIPE and EINVAL.
 *   - A refusal is never a reason to try another way. EACCES and EPERM stop
 *     the chain and reach the caller unchanged, because a resolver that
 *     retries around a permission check has quietly become a way to get
 *     around one. Every other error (EIO, ENOSPC, EFAULT) also stops: the
 *     operation failed, and doing it again by another route would either fail
 *     again or hide a partial effect.
 *
 * The last method in every chain is one that always works -- a plain read(),
 * a read/write loop -- so a resolved chain is never empty for lack of luck.
 * When a chain *is* empty it is because nothing can serve the request at all,
 * and that is reported as a refusal with an errno, not as a fallback.
 *
 * This layer chooses and executes. It does not own descriptors, does not
 * claim them, and does not decide lifecycle: a method is a way of moving
 * bytes, not a second opinion about who owns them.
 */

/* Methods considered for one operation, chain and rejects together. */
#define XIO_ROUTE_STEPS_MAX 6u

/* The hard ceiling on how many methods one execution may actually call.
 *
 * A chain is at most XIO_ROUTE_STEPS_MAX long and the walk is a single pass
 * over it, so the count is already bounded by construction. The ceiling is
 * here as the second half of that statement rather than as a belt: it is
 * checked at run time and it is the number a test can assert against, so a
 * future chain that loops -- a retry, a re-resolve, a method that re-enters
 * the resolver -- fails loudly instead of turning a bounded walk into an
 * unbounded one. `xio_route_attempts()` never exceeds it. */
#define XIO_ROUTE_ATTEMPTS_MAX XIO_ROUTE_STEPS_MAX

/* Why a step is not in the chain, as a scope rather than only as prose.
 *
 * The two reasons are not the same fact and must not be read as one. A method
 * whose kernel support is missing is out of the running for every request in
 * this process. A method that cannot serve *this* descriptor -- mmap against a
 * pipe, sendfile against a source it cannot map -- is fully healthy and will
 * serve the next request that suits it. Reporting both as "dropped: <text>"
 * left an operator to tell them apart by reading English, and left a caller no
 * way to tell them apart at all. */
typedef enum xio_excl_scope {
    XIO_EXCL_NONE    = 0,  /* the step is in the chain                      */
    XIO_EXCL_REQUEST = 1,  /* not applicable to this request; method is fine */
    XIO_EXCL_METHOD  = 2   /* the method is out of the running everywhere    */
} xio_excl_scope_t;

/* Health is about the method, not about the module using it. DEGRADED means
 * usable but not on its best path -- a kernel that has the syscall but
 * refuses the fast variant. UNAVAILABLE means the method is out of the
 * running everywhere, not just for this call; that is the difference between
 * health and the per-request `excluded` reason below. */
typedef enum xio_health {
    XIO_HEALTH_AVAILABLE   = 0,
    XIO_HEALTH_DEGRADED    = 1,
    XIO_HEALTH_UNAVAILABLE = 2
} xio_health_t;

typedef struct xio_route_step {
    const char  *method;    /* "mmap.window", "zc.copy_file_range", ...     */
    const char  *provider;  /* "mmap", "zerocopy", "libc", ...              */
    xio_health_t health;
    /* NULL when the step is in the chain. Otherwise why it is not: the
     * reason is kept as text because it is what an operator has to read. */
    const char  *excluded;
    /* XIO_EXCL_NONE exactly when `excluded` is NULL. Otherwise it says which
     * of the two kinds of "no" this is -- see xio_excl_scope_t. */
    xio_excl_scope_t excl_scope;
} xio_route_step_t;

typedef struct xio_route {
    uint32_t         generation; /* bumped by every preset change            */
    const char      *preset;
    xio_op_id_t      op;
    size_t           size;
    int              n_steps;    /* methods considered                       */
    int              n_chain;    /* of those, how many are in the chain      */
    int              refusal;    /* errno when n_chain == 0, else 0          */
    xio_route_step_t step[XIO_ROUTE_STEPS_MAX];
} xio_route_t;

/* fd/fd2 let a probe answer for *this* descriptor: mmap has nothing to say
 * about a pipe, sendfile needs a source it can mmap. -1 when there is none,
 * in which case per-descriptor probes are skipped and the chain is the
 * preset's answer for the operation and size alone. */
typedef struct xio_route_req {
    xio_op_id_t op;
    size_t      size;
    const char *subsys;   /* NULL or "" = the default preset               */
    int         fd;       /* the operation's descriptor, or the destination */
    int         fd2;      /* transfer: the source                           */
    /* Who will own a descriptor this operation creates.
     *
     * This layer still does not own descriptors and still does not decide
     * lifecycle -- it places one claim, on behalf of the owner the caller
     * named, for a descriptor that did not exist until this call made it. That
     * is not a second opinion about ownership: it is the only moment at which
     * a claim can be placed without a window in which the descriptor is live
     * and unclaimed, and the caller cannot place it because the caller does
     * not have the descriptor yet.
     *
     * generation 0 means no owner and is the default a zero-initialised
     * request gets: the claim is skipped entirely and the call behaves exactly
     * as it did before this field existed. Nothing existing changes shape. */
    plat_owner_t owner;
} xio_route_req_t;

/* 0 on success (including a refusal, which is an answer), -1 on bad args. */
int xio_route_resolve(const xio_route_req_t *req, xio_route_t *out);

/* Renders the chain and, below it, the methods that were dropped and why --
 * the rejects are the half an operator actually needs. Returns the length
 * needed, so truncation is visible; -1 on bad args. */
int xio_route_render(const xio_route_t *r, char *buf, size_t n);

/* ── presets ───────────────────────────────────────────────────────────────
 *
 * Two, and both compiled in: "default", and "ra2c" for the session engine,
 * whose reads are small frames and whose latency matters more than the
 * throughput tricks. A preset is a table of rules, not a config file parser;
 * a parser can come later without changing anything a caller sees.
 *
 * Changing the preset bumps the generation. Nothing in flight is re-routed:
 * a call that already resolved keeps the chain it resolved, and the next call
 * gets the new one. That is the whole of route-generation semantics -- there
 * is no attempt to migrate an operation between backends mid-flight, because
 * a half-moved operation is exactly the partial state this layer refuses.
 */
uint32_t    xio_route_generation(void);
const char *xio_route_preset(void);
int         xio_route_preset_set(const char *name); /* -1 for an unknown name */

/* The health of one method by name, with a note saying why when it is not
 * AVAILABLE. Unknown method: UNAVAILABLE and a note that says so, never a
 * cheerful default. */
xio_health_t xio_route_method_health(const char *method, const char **note);

/* ── execution ─────────────────────────────────────────────────────────────
 *
 * Same semantics as the syscall each one generalises: the byte count on
 * success, -1 with errno on failure. The chain is walked internally; the
 * caller does not name a method and cannot be made to care which one ran.
 */
ssize_t xio_route_read (const char *subsys, int fd, void *buf, size_t n);
ssize_t xio_route_write(const char *subsys, int fd, const void *buf, size_t n);

/* file.transfer: move n bytes from src to dst. Not read+write in the caller;
 * that is the point -- copy_file_range and sendfile never bring the bytes
 * into this process at all. */
ssize_t xio_route_transfer(const char *subsys, int dst_fd, int src_fd, size_t n);

/* memory.create: an fd for n bytes of memory. memfd where the kernel has it,
 * an unlinked temporary file where it does not. */
int xio_route_mem_create(const char *subsys, const char *name, size_t n);

/* fs.watch: a descriptor that reports changes to a path. fanotify when this
 * process is privileged enough to mark with it, inotify otherwise -- and the
 * privilege is decided by a probe before the call, not by retrying after an
 * EPERM. See the note in xio_route_res.c for why that distinction is the
 * whole difference between routing and bypassing. */
int xio_route_watch(const char *subsys, const char *path);

/* process.pidfd: a descriptor for a process. There is deliberately no
 * fallback -- a bare pid is not a pidfd, and handing one back under this name
 * would defeat the reason a caller asked. */
int xio_route_pidfd(const char *subsys, pid_t pid);

/* ── the same three, with the claim placed ─────────────────────────────────
 *
 * Every call above that returns a descriptor returns one that nobody owns.
 * The caller is expected to claim it, and between the return and that claim
 * the descriptor is live and unaccounted: a teardown sweep in that window
 * finds nothing to release, and the descriptor outlives the epoch that made
 * it. The window is small and it is not closeable from outside, because the
 * caller cannot claim a descriptor it has not been handed yet.
 *
 * These three close it. The resolver runs the same chain, and when the method
 * hands back a descriptor the executor -- not the method -- places exactly one
 * claim for `owner` before returning. One owner gate per operation, whichever
 * method in the chain won; a method never claims, so a chain of four methods
 * cannot produce four claims or a claim for a descriptor that was closed when
 * the next method was tried.
 *
 * A claim that fails is a failed operation. The descriptor is closed and the
 * error is the claim's, because handing back a descriptor whose accounting was
 * refused is exactly the unowned descriptor this exists to prevent -- and
 * silently keeping it would be worse than never having offered the claim.
 * Nothing but the descriptor this call created is touched on that path.
 *
 * owner.generation == 0 is refused with EINVAL rather than quietly served
 * unclaimed: a caller that asked for the owning variant asked for the claim.
 * The unowned behaviour is still available, spelled as the plain call above. */
int xio_route_mem_create_owned(const char *subsys, const char *name, size_t n,
                               plat_owner_t owner);
int xio_route_watch_owned(const char *subsys, const char *path,
                          plat_owner_t owner);
int xio_route_pidfd_owned(const char *subsys, pid_t pid, plat_owner_t owner);

/* ── the one profile restriction this layer enforces ───────────────────────
 *
 * memory.create falls back from a memfd to an unlinked temporary file, and
 * the two are the same thing to a caller and not the same thing to a profile:
 * one never touches a filesystem and the other does. A profile that forbids
 * bytes on disk needs to be able to say so somewhere the chain will read it.
 *
 * It is a latch, not a switch. Forbidding is permanent for the life of the
 * process, because a restriction that can be lifted by the next caller to
 * think it knows better is a suggestion, and the profile that set it has no
 * way to notice it was lifted. There is deliberately no "allow" call.
 *
 * The effect is at resolve time: anon.tmpfile is dropped by its probe with a
 * reason, before it has created anything. A memory.create with no memfd and
 * no permitted fallback refuses with ENOTSUP, which is the honest answer --
 * not a temporary file made and then regretted.
 *
 * xio_route_forbid_disk_fallback() returns 0. It is idempotent. */
int xio_route_forbid_disk_fallback(void);
int xio_route_disk_fallback_allowed(void);

/* The method that actually ran the last execution on this thread, or NULL.
 * For reports and tests: a chain that always falls through to the baseline
 * would otherwise look identical to one that uses the fast path. */
const char *xio_route_last_method(void);

/* How many methods the last execution on this thread actually called.
 *
 * This is the observable half of the fallback rules. Both a chain that
 * stopped at the first real error and a chain that tried every method and
 * came back with the same errno end up returning that errno; only the count
 * tells them apart. One attempt on a failure means the error was treated as
 * final, which is what an EACCES or an EIO must be. */
unsigned xio_route_attempts(void);

/* The fallback rule itself, exposed rather than hidden.
 *
 * 1 when this errno means "not this method" and the chain may continue, 0
 * when it means the operation failed and the chain is over. It is part of the
 * contract and not an implementation detail: a module reading a resolver
 * error has to know which of the two it is looking at, and a rule that lives
 * only inside a static function is a rule nobody can check. EACCES and EPERM
 * answer 0 -- retrying a permission decision by another route is a bypass,
 * not a fallback. */
int xio_route_err_is_method_scoped(int err);
08 / COMMAND REFERENCE

CLI всех модулей.

79 уникальных команд верхнего уровня и отдельные интерфейсы доменов. Синтаксис собран из регистрации команд, встроенной справки и сообщений о допустимых аргументах. Короткая регистрационная строка дополнена подробным интерфейсом обработчика.

Как читать синтаксис

<значение> — обязательный аргумент; [аргумент] — необязательная часть; a|b — альтернативы. Встроенная справка может задавать собственную запись placeholders. Значения FILE, ID, NONCE_HEX обозначают параметры, которые оператор подставляет для своей операции.

CLI платформы, отдельный executable и библиотечная функция имеют разные точки входа. Для домена без зарегистрированной корневой команды это отмечено явно; имя каталога не превращается автоматически в команду.

Результат и доступность

Профиль, ОС, привилегии и привязанный provider определяют доступность операции. Справочные блоки сохраняют сообщения UNAVAILABLE, PARTIAL, сведения о stub и требования устройства, когда они присутствуют в исходном интерфейсе.

Декларация описывает синтаксис, а статус — готовность конкретного экземпляра. В этом атласе команды представлены как документация: страница не запускает их и не подключается к узлам.

Разделы CLI: 64 / 64. Поиск работает по всей справке, включая аргументы и коды возврата.

attachattachment

Транзакционное присоединение возможностей и ресурсов

Архитектура и API домена ↗

attachment

inspect and drive capability attachments

attachment <explain|health|attach|detach> [id]
Регистрация: src/attach/cmd_attach.c · обработчик cmd_attachment

Развёрнутая встроенная справка

attachment — inspect and drive capability attachments
  attachment explain [id]            candidates + slot state (0/omitted = catalog)
  attachment health  <id>            OK / DEGRADED / LOST for one slot
  attachment attach  <provider> [id] prepare+commit slot (id 0/omit = new slot)
  attachment detach  <id>            drain+detach, release owned fds
src/attach/cmd_attach.c
auditaudit · flow

Журнал действий, событий безопасности и происхождения

Архитектура и API домена ↗

audit

Append-only JSON Lines audit trail — write, query, and manage

audit <test|log|tail|path|stats|level|rotate|help>
Регистрация: src/audit/cmd_audit.c · обработчик c_audit

flow

пассивный анализ соединений (он же audit flow)

flow <start|stop|list|stats|tick>
Регистрация: src/audit/cmd_flow.c · обработчик cmd_flow

Развёрнутая встроенная справка

audit — append-only JSON Lines security audit trail

  audit test [--message=TEXT]                    write self-test event
  audit log  --action=<name> [--level=L]         write custom event
             [--comp=C] [--details=TEXT]
  audit tail [--count=N] [--level=L]             show recent events
  audit path                                     show log file path
  audit stats                                    show statistics
  audit level [--set=L]                          get/set min level
  audit rotate                                   force file rotation
  audit help

Levels: debug  info  notice  warning  error
Format: JSON Lines — one object per line, deterministic field order.
Rotation: at 10 MB → .1/.2/.3/.4 backups kept.
src/audit/cmd_audit.c
audit flow — пассивный анализ соединений

  audit flow start [источник] [--min-duration=MS] [--no-audit]
      источник (один из):
        --stack=ИМЯ         наблюдатель на живом стеке txpstack
        --from=pcap:ПУТЬ    разбор файла: без сети и без прав
        --iface=eth0 [--seconds=N] [--filter=ВЫР] [--promisc]
                            захват с интерфейса (нужен CAP_NET_RAW)
  audit flow stop
  audit flow list  [--state=established|closing|unknown|all] [--proto=tcp|udp|icmp]
  audit flow stats
  audit flow tick               принудительно истечь и выдать события

Анализатор ПАССИВЕН: ничего не отправляет и не изменяет.
Состояние 'unknown' означает, что к потоку присоединились в середине —
рукопожатия не видели, инициатор неизвестен.
События открытия/закрытия идут в журнал audit (comp=flow),
отключается флагом --no-audit.
src/audit/cmd_flow.c

Аргументы и дополнительные формы вызова

  • usage: audit log --action=<name> [--level=L] [--comp=C] [--details=TEXT] Levels: debug info notice warning errorsrc/audit/cmd_audit.c
centralcentral

Управление группой узлов, миссиями и распределённым состоянием

Архитектура и API домена ↗

central

Central Node: реестр доверия, команды и политики флоту

central <init|start|stop|status|metrics|nodes|node|cmd|policy|blacklist|unblacklist|trust|untrust|forget|pubkey|transport|web-start|web-stop|web-status|ops>
Регистрация: src/central/cmd_central.c · обработчик cmd_central

Развёрнутая встроенная справка

central-web — Central Node PLATX со встроенным веб-интерфейсом

  --host=ADDR     адрес прослушивания (по умолчанию %s)
  --port=N        порт (по умолчанию %d)
  --webui=DIR     каталог статики (по умолчанию ./webui)
  --db=PATH       файл базы сервера (по умолчанию ./central_web.db)
  --token=STR     токен оператора для заголовка X-Central-Token
  --key-file=PATH файл с приватным ключом Ed25519 (64 байта сырых
                  либо 128 hex-символов). Без него — режим наблюдения:
                  реестр ведётся, команды флоту не выдаются.
  --help

Слушать не-loopback без --token сервер откажется: через /api/central/cmd
уходит подписанная команда всему флоту.

API самоописывается:  curl http://HOST:PORT/api/central
src/central/central_main.c
central — стратегический центр триединой системы доверия

  central init [--key=HEX128]   инициализировать (ключ из keyring либо явно)
  central start                 поднять транспорт mesh_raw и подписки
  central stop                  снять подписки
  central status                состояние узла
  central metrics               счётчики
  central transport             состояние транспорта
  central pubkey                публичный ключ для bootstrap

  central nodes [--state=OK] [--trusted=1] [--blacklisted=0] [--limit=N]
  central node --id=HEX         одна запись (id можно префиксом, как в git)

  central cmd --cmd=NAME (--id=HEX | --all) [--payload=HEX]
      команды: noop enhance rotate policy lockdown recover quarantine
               report chaos
  central policy (--text=STR | --data=HEX)   разослать политику флоту

  central blacklist --id=HEX    чёрный список + карантин
  central unblacklist --id=HEX  снять метку (доверие набирается заново)
  central trust|untrust --id=HEX
  central forget --id=HEX       удалить запись из реестра

  central web-status            встроенный веб-интерфейс: где слушает
  central web-start [--host=H] [--port=N] [--webui=DIR] [--token=STR]
      поднять UI. По умолчанию 127.0.0.1:8181. Слушать не-loopback
      без --token сервер откажется: через HTTP уходит команда флоту.
  central web-stop              погасить UI

  central ops                   каталог операций (то же множество в HTTP/UI)

Любая команда принимает --json — ответ идентичен HTTP-ответу.
Порядок запуска: mesh start → central init → central start
src/central/cmd_central.c

Аргументы и дополнительные формы вызова

  • central status [--json]src/central/central_api.c
  • central metrics [--json]src/central/central_api.c
  • central nodes [--state=S] [--trusted=B] [--blacklisted=B] [--limit=N]src/central/central_api.c
  • central pubkey [--json]src/central/central_api.c
  • central transport [--json]src/central/central_api.c
  • central ops [--json]src/central/central_api.c
  • central trust --id=HEXsrc/central/central_api.c
  • central untrust --id=HEXsrc/central/central_api.c
  • central web-status [--json]src/central/central_api.c
  • central web-start [--host=H] [--port=N] [--webui=DIR] [--db=PATH] [--token=STR]src/central/central_api.c
contextcontext

Идентичность контекста исполнения и контроль его жизни

Архитектура и API домена ↗

context

show a host-local execution context

context explain <id> [generation]
Регистрация: src/context/cmd_context.c · обработчик cmd_context

Аргументы и дополнительные формы вызова

  • context — show where a runtime lives context explain <id> [generation] id+gen, or STALE if destroyedsrc/context/cmd_context.c
corehelp · ping · mode · echo · section · status · version · ops · subsys · platform

Ядро платформы: запуск, ABI, жизненный цикл, владение и консоль

Архитектура и API домена ↗

help

list all commands

help
Регистрация: src/core/builtin_commands.c · обработчик cmd_help

ping

connectivity check

ping
Регистрация: src/core/builtin_commands.c · обработчик cmd_ping

mode

print current console mode

mode
Регистрация: src/core/builtin_commands.c · обработчик cmd_mode

echo

print arguments to output

echo [text...]
Регистрация: src/core/builtin_commands.c · обработчик cmd_echo

section

print a section banner

section <title>
Регистрация: src/core/builtin_commands.c · обработчик cmd_section

status

show platform status

status
Регистрация: src/core/builtin_commands.c · обработчик cmd_status

version

show platform version

version
Регистрация: src/core/builtin_commands.c · обработчик cmd_version

ops

operator API over the process RA2C session

ops <listen|serve|status|explain|send|…>
Регистрация: src/core/cmd_plat_ops.c · обработчик cmd_ops

subsys

реестр подсистем платформы: просмотр и управление жизненным циклом

subsys <list|count|health|status|metrics|start|stop|help> [name]
Регистрация: src/core/cmd_subsys.c · обработчик c_subsys

platform

контекст платформы: режимы подсистем и планировщик

platform <status|mode|sched|run>
Регистрация: src/core/platform.c · обработчик cmd_platform

Развёрнутая встроенная справка

memfd - extended memfd management utility

  memfd create <name> [cloexec sealing exec noexec hugepage]
  memfd write  <ref> <data|@file|[cdata]> [raw|hex|base64] [--offset N] [--set]
  memfd read   <ref> [raw|hex|base64|dump] [start] [end]
  memfd truncate <ref> <size> [grow-only]
  memfd chmod  <ref> <mode>            # e.g. 0500
  memfd seal   <ref> <seal,shrink,grow,write,future-write,exec,all>
  memfd mmap   <ref> <r,w,x> <private,shared,fixed> [len]
  memfd mprotect <ref> <r,w,x>
  memfd stat   <ref>
  memfd list   [filter]
  memfd close  <ref>
  memfd save   <ref> --to <path> [--key K]
  memfd load   <name> --from <path> [--key K]
  memfd exec   <ref> [args...]
  memfd exec   <ref> execveat [do_fork|no_fork] [proc]
               [at_emptypath|at_fdcwd] [--symlink P] [--execveat_fd] [args...]
  memfd elf_load parse  <file|memfd>                    # ELF header dump
  memfd elf_load load   <file|memfd> [--nofork] [-- args...]  # userspace ELF exec
  memfd elf_load run    <file|memfd> <fexecve|execveat> [args...]  # kernel exec
  memfd cdata save <data> [--encoding=base64|hex] [--compression=gzip]
                          [--encryption=rc4|chacha20|aes256gcm]
                          [--key=S|--keyring=N] [--embed-key] [--hash=sha256|crc32]
  memfd cdata load <container|@file> [--key=K]
  memfd cdata info <container|@file>
  memfd daemon [-f]     # start manager (auto-started on demand)
  memfd shutdown        # stop the manager
src/core/cli.c
subsys — реестр подсистем платформы

  subsys list              список подсистем: имя / версия / ABI / флаги / здоровье
  subsys count             количество зарегистрированных подсистем
  subsys health            проверка здоровья всех подсистем
  subsys status [<name>]   статус JSON (одна или все подсистемы)
  subsys metrics [<name>]  метрики JSON (одна или все подсистемы)
  subsys start <name>      запустить подсистему (ops->start)
  subsys stop  <name>      остановить подсистему (ops->stop)
  subsys help

Флаги подсистемы:
  singleton    только один экземпляр
  plugin       загружена из .so / ELF-образа
  restartable  безопасен stop()+start()
  stateful     реализует save_state/load_state

Значения здоровья: OK  DEGRADED  FATAL
src/core/cmd_subsys.c
platform — контекст платформы: режимы подсистем и планировщик

  platform status            режимы (embedded/client/server), сокеты, scheduler
  platform mode [subsys]     режим memfd|proxy|…
  platform sched             список фоновых задач
  platform run <sec>         держать процесс живым (дать scheduler тикать)
  platform isolation         самопроверка ≥2 изолированных экземпляров (ТР-5.2.3)

Режим задаётся: env PLATFORM_ROLE / PLATFORM_<SUBSYS>=embedded|client|server|auto,
либо auto-пробой сокета (для модулей со своим probe). Один бинарник =
сервер и клиент одновременно.
src/core/platform.c
usage:
  memfd cdata save <data> [--encoding=base64|hex] [--compression=gzip]
                          [--encryption=rc4|chacha20|aes256gcm]
                          [--key=SECRET|--keyring=NAME] [--embed-key]
                          [--hash=sha256|crc32]
  memfd cdata load <container|@file> [--key=K]
  memfd cdata info <container|@file>
src/core/cli.c
ops — operator API in this process (not a second session)
  ops bind [port]     HTTP 127.0.0.1 (default 9077); thread, REPL stays
  ops serve [port]    bind and block (alpha --command)
  ops close           drop the HTTP listen; session is not touched
  ops status          session flags + listen port
  ops send|cmd|switch|recv|connect|listen|disconnect …
                      same refusals as ra2c in this process
  leftover session    status refuses (not OK); send still no keyed session
  ops explain [cap]   C-vertical candidates (GET /gadget/explain); no execute
                      leftover refuses; gadget not linked → not a fake catalog
  ops nodes|list      error: no node table — not an empty list
src/core/cmd_plat_ops.c

Аргументы и дополнительные формы вызова

  • usage: memfd create <name> [cloexec sealing exec noexec hugepage]src/core/cli.c
  • usage: memfd write <ref> <data|@file|[cdata]> [raw|hex|base64] [--offset N] [--set]src/core/cli.c
  • usage: memfd read <ref> [raw|hex|base64|dump] [start] [end]src/core/cli.c
  • usage: memfd save <ref> --to <path> [--key K]src/core/cli.c
  • usage: memfd load <name> --from <path> [--key K]src/core/cli.c
  • usage: memfd exec <ref> [args...] | ... execveat [opts] [args]src/core/cli.c
  • usage: memfd truncate <ref> <size> [grow-only]src/core/cli.c
  • usage: memfd chmod <ref> <mode>src/core/cli.c
  • usage: memfd seal <ref> <seals>src/core/cli.c
  • usage: memfd mmap <ref> <r,w,x> <private,shared,fixed> [len]src/core/cli.c
  • usage: memfd mprotect <ref> <r,w,x>src/core/cli.c
  • usage: memfd stat <ref>src/core/cli.c
  • usage: memfd close <ref>src/core/cli.c
  • usage: memfd elf_load parse <file|memfd> memfd elf_load load <file|memfd> [--nofork] [-- args...] memfd elf_load run <file|memfd> <fexecve|execveat> [args...]src/core/cli.c
  • usage: memfd elf_load run <file|memfd> <fexecve|execveat> [args...]src/core/cli.c
  • usage: subsys start <name>src/core/cmd_subsys.c
  • usage: subsys stop <name>src/core/cmd_subsys.c
cryptoотдельный интерфейс / библиотека

Криптографические примитивы и версионированные сервисы

Архитектура и API домена ↗

Самостоятельная CLI-команда и отдельная usage-справка не обнаружены. Управление находится у потребителей API — trust, vault, keyring. Контракты и границы приведены в досье домена.

dbgdbg

Лаборатория отладки процессов, памяти и исполнения

Архитектура и API домена ↗

dbg

diagnostics toolkit: disassembler, debugger, tracer, memory and ROP analysis

dbg <dasm|debug|trace|analyze|inject|rop|lab> [sub-cmd] [args]
Регистрация: src/dbg/cmd_dbg.c · обработчик cmd_dbg

Аргументы и дополнительные формы вызова

  • dbg lab chain goal <spec>src/dbg/dbg_lab_chain.c
  • dbg lab chain constraint <spec>src/dbg/dbg_lab_chain.c
  • dbg lab target open: --elf FILE обязателенsrc/dbg/dbg_lab_target.c
  • dbg lab target core: --file CORE --elf FILE обязательныsrc/dbg/dbg_lab_target.c
  • Memory usage:src/dbg/dbg_mem.c
  • usage: dbg rop scan FILE [--limit=N] [--kind=load|pivot|syscall|align]src/dbg/dbg_rop.c
drmdrm

Контроль защищённого содержимого и жизненного цикла расширений

Архитектура и API домена ↗

drm

DRM: plugin vault + platx-self stamp (AES-GCM, no on-disk rewrite)

drm <protect|activate|deactivate|status|list|info|rotate-key|self-destruct [--all]|set-timeout|self>
Регистрация: src/drm/drm_cmd.c · обработчик (cmd_fn_t)drm_dispatch

Аргументы и дополнительные формы вызова

  • usage: drm set-timeout <name> <seconds>src/drm/drm_cmd.c
  • usage: drm protect <name> [--level=cfg|vm|...] [--allow-debug]src/drm/drm_cmd.c
dsldsl · deploy

Декларативные манифесты, размещение и управление конфигурацией

Архитектура и API домена ↗

dsl

декларативная оркестрация инфраструктуры (YAML)

dsl <deploy|validate|list|status|...>
Регистрация: src/dsl/cmd_dsl.c · обработчик cmd_dsl_dispatch

deploy

развёртывание манифеста (фронт над DSL: provision+steps)

deploy <file.yaml> | ls|status|stop|logs <id>
Регистрация: src/dsl/cmd_dsl.c · обработчик cmd_deploy_dispatch

Развёрнутая встроенная справка

dsl — декларативная оркестрация инфраструктуры (YAML-манифесты)

Конфигурации:
  dsl deploy   <file.yaml> [--environment=prod|staging|test] [--param=NAME=VALUE ...] [--require-sig --pubkey=<64hex>] [--dry-run] [--verbose]
  dsl validate <file.yaml>
  dsl sign     <file.yaml> --seed=<64hex> [--in-place]   подписать (ed25519)
  dsl verify   <file.yaml> [--pubkey=<64hex>]            проверить подпись
  dsl list     [--status=running|online|stopped|failed]
  dsl status   <id> [--verbose]
  dsl stop|start|restart|undeploy <id> [--force]
  dsl reload   <id> [--config=file]

Параметры/артефакты:
  dsl set <id> <param> <value> | get <id> <param>
  dsl export <id> | import <file>
  dsl logs <id> [--lines=N]

Транспорты/безопасность:
  dsl transport list --script=<id>         шаги-транспорты деплоя
  dsl security rotate-keys|verify [<id>]   (интеграция с keyring)
  dsl metrics <id> [--type=..] | diagnose <id>

Не реализовано (команда откажет, а не выполнит):
  dsl transport enable|disable|test        транспорт задаётся шагом манифеста
  dsl security encrypt|decrypt             движка шифрования артефактов нет
  dsl failover status|switch|test          движка отказоустойчивости нет
src/dsl/cmd_dsl.c
deploy — развёртывание манифеста (фронт над DSL-движком)

  deploy <file.yaml> [--environment=][--param=NAME=VALUE][--dry-run][--verbose]
  deploy ls|list [--status=]      — активные развёртывания
  deploy status <id> [--verbose]
  deploy stop|start|restart|undeploy <id> [--force]
  deploy logs <id> [--lines=N]

Фаза provision в манифесте поднимает keyring/vault/proxy до шагов.
Движок общий с `dsl` — deploy это удобные глаголы над ним.
src/dsl/cmd_dsl.c

Аргументы и дополнительные формы вызова

  • usage: dsl deploy <script.yaml> [--environment=..] [--param=NAME=VALUE ...] [--require-sig --pubkey=<64hex>] [--dry-run] [--verbose]src/dsl/cmd_dsl.c
  • usage: dsl validate <script.yaml>src/dsl/cmd_dsl.c
  • usage: dsl status <id> [--verbose]src/dsl/cmd_dsl.c
  • usage: dsl %s <id>src/dsl/cmd_dsl.c
  • usage: dsl reload <id> [--config=file]src/dsl/cmd_dsl.c
  • usage: dsl set <id> <parameter> <value>src/dsl/cmd_dsl.c
  • usage: dsl get <id> <parameter>src/dsl/cmd_dsl.c
  • usage: dsl logs <id> [--lines=N]src/dsl/cmd_dsl.c
  • usage: dsl export <id>src/dsl/cmd_dsl.c
  • usage: dsl import <file>src/dsl/cmd_dsl.c
  • usage: dsl transport list|enable|disable|test --script=<id> [--type=..]src/dsl/cmd_dsl.c
  • usage: dsl security rotate-keys|verify|encrypt|decrypt <id>src/dsl/cmd_dsl.c
  • usage: dsl failover status|switch|test <id>src/dsl/cmd_dsl.c
  • usage: dsl metrics <id> [--type=traffic|latency|errors]src/dsl/cmd_dsl.c
  • usage: dsl diagnose <id>src/dsl/cmd_dsl.c
  • usage: dsl sign <file.yaml> --seed=<64hex> [--in-place]src/dsl/cmd_dsl.c
  • usage: dsl verify <file.yaml> [--pubkey=<64hex>]src/dsl/cmd_dsl.c
ebpfebpf · bpf · hades

eBPF, HADES, сенсоры и провайдеры наблюдения ядра

Архитектура и API домена ↗

ebpf

EBPFMonitor: загрузка и мониторинг eBPF-программ (напрямую через bpf(), без libbpf)

ebpf <sysinfo|status|list|load|xdp|kprobe|uprobe|map|btf|tracing|lsm|fsx|detach> ...
Регистрация: src/ebpf/cmd_ebpf.c · обработчик cmd_ebpf

bpf

псевдоним namespace ebpf

bpf ... (псевдоним ebpf)
Регистрация: src/ebpf/cmd_ebpf.c · обработчик cmd_ebpf

hades

userspace Hades: сенсоры, firewall, WAF, LSM, BTF

hades <version|status|start|stop|restart|reload|stats|firewall|waf|sensors|filters|research|lsm|btf> …
Регистрация: src/ebpf/hades/cmd/cmd_hades.c · обработчик cmd_hades

ebpf

EBPFMonitor: загрузка и мониторинг eBPF с поддержкой CO-RE

ebpf <load|xdp|kprobe|btf|lsm|map|...> ...
Регистрация: src/ebpf/hades/tools/cmd_ebpf.c · обработчик cmd_ebpf

Развёрнутая встроенная справка

ebpf (псевдоним bpf) — EBPFMonitor: загрузка и мониторинг eBPF без libbpf

  ebpf sysinfo                       диагностика доступности bpf()
  ebpf status                        общее состояние монитора
  ebpf list [--json]                 список активных программ
  ebpf load <obj.o> [sec] [--type=..] загрузка eBPF-объекта
  ebpf xdp --interface=I --obj=O.o [--action=..] [--pin] [--json]
  ebpf kprobe --syscall=NAME --obj=O.o [--retprobe] [--json]
  ebpf uprobe --path=F (--symbol=S|--offset=O) --obj=O.o [--pid=N] [--retprobe]
  ebpf map --action=dump|lookup|update|delete --map=NAME [--key=K] [--value=V]
  ebpf detach [--interface=I]        отключение программ
  ebpf btf <info|load [path]>        подсистема BTF
  ebpf tracing <load|attach|show>    BPF-трассировка
  ebpf lsm <load|attach|list|hooks|stats|audit|policy ...>   BPF LSM
  ebpf kernel read                   низкоуровневая диагностика ядра
  ebpf fsx [on|off|status|events|incidents]  подписка на FSX EventBus

Работает напрямую через bpf()/perf_event_open()/Netlink. Привилегированные
операции требуют CAP_BPF/CAP_SYS_ADMIN; при отсутствии — диагностика.
src/ebpf/cmd_ebpf.c
dump     [--vmlinux|--file <f>] [--type <name>]  Dump BTF types

  find     <type-name> [--file <f>]                 Find a type

  field    <type> <field> [--file <f>]              Print field info

  offset   <type> <field> [--file <f>]              Print bit offset

  size     <type> [--file <f>]                      Print type size

  verify   [--file <f>]                             Validate BTF blob

  load     <btf-file>                               Load BTF into kernel

  list     <struct|enum|func|typedef|all> [--file f] List types by kind

  kernel                                            Show kernel BTF info
src/ebpf/hades/cmd/cmd_btf.c
start        [-c config] [-f] [-v]  Start the daemon

  stop                                 Stop the daemon

  restart                              Restart the daemon

  reload                               Reload configuration

  status                               Show daemon status

  version                              Show version info

  stats                                Show runtime statistics

  firewall     <subcommand>            Firewall management

  waf          <subcommand>            WAF management

  sensors      <subcommand>            Sensor management

  filters      <subcommand>            Exclusion/whitelist rules

  research     <subcommand>            Research sensor controls

  lsm          <subcommand>            LSM policy management

  btf          <subcommand>            BTF inspection tools


Run '%s <command> --help' for subcommand help.
src/ebpf/hades/cmd/cmd_core.c
Usage: %s filters <excl|wl> <subcommand> [options]


  add    --type <type> --value <val> [--sensor <mask>] [--comment <c>]


Exclusion types: comm, exe, uid, file, cidr, port, syscall, sensor

  add    --type <type> --value <val> [--trust audit|silent|full]

         [--sensor <mask>] [--comment <c>]


Whitelist types: comm, path, uid, file, endpoint, cidr, cert, combined
src/ebpf/hades/cmd/cmd_filters.c
Usage: %s firewall <subcommand> [options]


  rule add     --src <ip/mask> --dst <ip/mask> --proto tcp|udp|icmp

               --dport <port[-end]> --sport <port[-end]>

               --action allow|drop|reject|rate|log

               --dir in|out|both --prio <n> --name <name>

               [--rate-pps <n>] [--burst <n>]

  geoip cc     add|remove <CC>      (2-letter country code)
src/ebpf/hades/cmd/cmd_firewall.c
status                    Show LSM subsystem status

  hooks                     List all available hooks and their state

  enable   [hook-name]      Enable hook (all if no name)

  disable  [hook-name]      Disable hook (all if no name)

  policy add --hook <name> --action allow|deny|audit

             [--comm <glob>] [--path <glob>] [--uid <uid>]

             [--prio <n>] [--comment <c>]

  file_open  file_mprotect  bprm_check  socket_connect  socket_bind

  socket_sendmsg  task_kill  task_setuid  sb_mount  sb_umount

  module_load  ptrace_access  key_alloc  bpf
src/ebpf/hades/cmd/cmd_lsm.c
Usage: %s research <subcommand> [options]


  list                      List all research sensors

  enable  <name>            Enable a research sensor

  disable <name>            Disable a research sensor

  dump    [name]            Dump sensor details (all if no name)

  events  [--follow] [-n N] Stream sensor events (Ctrl+C to stop)

  reset   <name>            Reset sensor statistics

  status                    Show research subsystem status

  syscall_trace  mem_access  file_open  exec_trace

  net_trace      mount_trace module_trace  cred_trace


WARNING: Research sensors generate high event volume.

         Always use exclusion filters in production.
src/ebpf/hades/cmd/cmd_research.c
Usage: %s sensors <subcommand> [name|id]


  list                  List all registered sensors

  disable <name|id>     Disable a sensor

  attach  <name|id>     Attach sensor to kernel hook

  detach  <name|id>     Detach sensor from kernel hook

  status  [name|id]     Show sensor status (all if no arg)

  dump    [name|id]     Dump detailed sensor info

  stats                 Show aggregate sensor statistics
src/ebpf/hades/cmd/cmd_sensors.c
rule add     --id <n> --pattern <pat> [--target uri|body|headers|all]

               [--action detect|block] [--score <n>] [--desc <text>]

               [--category sqli|xss|cmdi|pathtrav|ssrf|scanner|custom]

  threshold    <score>    (anomaly score threshold, default 5)
src/ebpf/hades/cmd/cmd_waf.c
ebpf (псевдоним bpf) — EBPFMonitor + CO-RE движок

Загрузка:
  ebpf load <obj.o> [sec] [--type=..] [--verbose-core]
  ebpf xdp  --interface=I --obj=O.o [--pin] [--verbose-core]
  ebpf kprobe --syscall=F --obj=O.o [--retprobe] [--verbose-core]
  ebpf uprobe --path=F --offset=N  --obj=O.o [--pid=N] [--retprobe]

CO-RE (Compile Once – Run Everywhere):
  ebpf btf core-check <obj.o> [--json]  проверить CO-RE метаданные
  ebpf btf core-relos <obj.o>            все CO-RE-релокации объекта
  ebpf btf offset <struct> <field>       смещение поля в BTF ядра
  ebpf btf types <name>                  найти тип в BTF ядра
  ebpf btf dump [obj.o|--kernel]         дамп типов BTF
  ebpf btf kern-btf [path]               путь к BTF ядра
  ebpf btf info                          доступность BTF ядра
  ebpf btf load [path]                   загрузить BTF в ядро

Управление:
  ebpf sysinfo [--json]                  диагностика (bpf, BTF, CO-RE)
  ebpf status  [--json]                  состояние монитора
  ebpf list    [--json]                  активные программы
  ebpf map --action=dump|lookup|update|delete --map=NAME [--key=K] [--value=V]
  ebpf detach [--interface=I]
  ebpf tracing <load|attach|show>
  ebpf lsm     <load|list|hooks|stats|audit|policy ...>

CO-RE включается автоматически при наличии .BTF и .BTF.ext в объекте
и доступном /sys/kernel/btf/vmlinux (CONFIG_DEBUG_INFO_BTF=y).
src/ebpf/hades/tools/cmd_ebpf.c
Usage: ebpf-load <file.bpf.o> [options]

Загрузка и подключение eBPF-программ из объектного файла.
Тип программы определяется автоматически из SEC()-аннотации.

Основные опции:
  -p, --prog <name>          загружать только эту программу (можно повторять)
  -n, --dry-run              разобрать объект, не загружать
  -v, --verbose              подробный вывод
  -o, --output table|json    формат вывода (по умолч.: table)
      --list-progs           список программ в объекте и выход

Сетевые аттачи (XDP, TC, TCX):
  -i, --iface <name>         сетевой интерфейс
      --xdp-skb              XDP_FLAGS_SKB_MODE
      --xdp-drv              XDP_FLAGS_DRV_MODE (по умолч.)
      --tc-egress            TC в направлении egress (по умолч.: ingress)

cgroup-аттачи:
  -c, --cgroup <path>        путь к cgroup (по умолч.: /sys/fs/cgroup)

uprobe-аттачи:
  -b, --binary <path>        путь к бинарнику
  -s, --symbol <name>        символ для uprobe
      --offset <hex>         смещение для uprobe
      --pid <pid>            фильтр по PID

netfilter-аттачи:
      --nf-pf <num>          protocol family (2=PF_INET, 10=PF_INET6)
      --nf-hook <num>        hook point (0=PRE_ROUTING..4=POST_ROUTING)
      --nf-prio <num>        приоритет хука

События:
  -e, --events               читать и печатать события
  -t, --timeout <sec>        таймаут чтения событий (0 = бесконечно)

Карты:
      --maps                 список карт в объекте
      --map-dump <name>      дамп содержимого карты
      --map-pin <name> <path> запин карты в BPF FS
      --map-get <name> <hex-key>          поиск записи
      --map-set <name> <hex-key> <hex-val> обновление записи

Пин:
      --pin <path>           запин всего объекта в BPF FS

Режимы:
      --hades                режим интеграции с hades (декодирование событий)

Примеры:
  # Загрузить XDP-программу на интерфейс eth0
  ebpf-load my.bpf.o --iface eth0

  # Загрузить и слушать события
  ebpf-load trace.bpf.o --events

  # Загрузить только одну программу, verbose
  ebpf-load multi.bpf.o --prog handle_egress --iface eth0 -v

  # Дамп карты
  ebpf-load obj.bpf.o --dry-run --maps
  ebpf-load obj.bpf.o --map-dump my_hash

  # Интеграция с hades
  ebpf-load build/hades.bpf.o --hades --events
src/ebpf/hades/tools/ebpf_load.c
Usage: %s [--json] <command> [args...]

Global flags:
  --json           Machine-readable JSON output
  --help, -h       Show this help

Commands:
  status                           Show daemon status
  sensors list                     List all sensors
  sensors enable  <name>           Enable a sensor
  sensors disable <name>           Disable a sensor
  sensors info    <name>           Detailed sensor info
  filter add-pid  <pid>            Add PID to whitelist
  filter del-pid  <pid>            Remove PID from whitelist
  filter list                      Show filter config
  config get                       Dump config as JSON
  config set <key> <value>         Set a config value
  config reload                    Reload config (SIGHUP)
  bpf maps                         List pinned BPF maps
  bpf progs                        List loaded BPF programs
  bpf dump-map <name>              Dump map contents as JSON
  alerts show [--last N]           Show last N alerts
  version                          Show version info
src/ebpf/hades/tools/hades_ctl.c
ebpf status:
  состояние        : %s
  каталог объектов : %s
  каталог pin      : %s
  программ         : %d
  коллекций (maps) : %d
  привязок         : %d
  bpf()/capabilities: %s
  fsx.events         : %s (%d events, %d incidents)
src/ebpf/cmd_ebpf.c
Usage:
  fw_rules add  "proto=tcp src=1.2.3.4/24 port=80 action=drop"
  fw_rules del  <rule_id>
  fw_rules list
  fw_rules block-ip <IP> [duration_secs] [reason]
  fw_rules allow-ip <IP>
  fw_rules block-port <port> [proto]
  fw_rules gc
src/ebpf/hades/firewall/fw_rules.c
ebpf sysinfo:
  bpf() syscall            : %s (nr=%d)
  eBPF-бэкенд              : %s
  unprivileged_bpf_disabled: %d%s
  uid                      : %d%s
  BTF ядра (%s)%s%s
  CO-RE готов              : %s
src/ebpf/hades/tools/cmd_ebpf.c
ebpf status:
  состояние         : %s
  программ          : %d
  коллекций (maps)  : %d
  привязок          : %d
  capabilities      : %s
  BTF ядра          : %s
  CO-RE (последний) : applied=%d skipped=%d errors=%d
  pin_dir           : %s
src/ebpf/hades/tools/cmd_ebpf.c

Аргументы и дополнительные формы вызова

  • usage: ebpf load <object.o> [section] [--type=xdp|kprobe|tc|raw_tracepoint]src/ebpf/cmd_ebpf.c
  • usage: ebpf xdp --interface=I [--obj=O.o] [--action=pass|drop] [--pin] [--json]src/ebpf/cmd_ebpf.c
  • ebpf xdp: нужен --obj=<xdp.o> с XDP-программой (интерфейс %s, ifindex %u найден)src/ebpf/cmd_ebpf.c
  • ebpf uprobe: нужен --obj=<uprobe.o>; цель path=%s symbol=%ssrc/ebpf/cmd_ebpf.c
  • ebpf kprobe: нужен --obj=<kprobe.o>; цель функция/syscall=%ssrc/ebpf/cmd_ebpf.c
  • ebpf uprobe: нужен --path=<ELF>src/ebpf/cmd_ebpf.c
  • ebpf kprobe: нужен --syscall=<name>src/ebpf/cmd_ebpf.c
  • usage: ebpf map --action=dump|lookup|update --map=NAME [--key=K] [--value=V]src/ebpf/cmd_ebpf.c
  • ebpf map lookup: нужен --keysrc/ebpf/cmd_ebpf.c
  • ebpf map update: нужны --key и --valuesrc/ebpf/cmd_ebpf.c
  • ebpf map delete: нужен --keysrc/ebpf/cmd_ebpf.c
  • usage: ebpf btf <info|load [path]>src/ebpf/cmd_ebpf.c
  • usage: ebpf tracing <load <obj.o>|attach|show>src/ebpf/cmd_ebpf.c
  • usage: ebpf tracing load <obj.o>src/ebpf/cmd_ebpf.c
  • usage: ebpf tracing attach <tracepoint> [prog_index]src/ebpf/cmd_ebpf.c
  • usage: ebpf lsm <load|attach|list|hooks|stats|audit|policy ...>src/ebpf/cmd_ebpf.c
  • usage: ebpf lsm load <lsm.o>src/ebpf/cmd_ebpf.c
  • usage: ebpf lsm policy <load|apply|list|remove>src/ebpf/cmd_ebpf.c
  • usage: ebpf kernel readsrc/ebpf/cmd_ebpf.c
  • Usage: %s btf <subcommand> [options]src/ebpf/hades/cmd/cmd_btf.c
  • Usage: btf find <type-name> [--file <f>]src/ebpf/hades/cmd/cmd_btf.c
  • Usage: btf field <type> <field> [--file <f>]src/ebpf/hades/cmd/cmd_btf.c
  • Usage: btf size <type> [--file <f>]src/ebpf/hades/cmd/cmd_btf.c
  • Usage: btf list <struct|enum|func|typedef|all> [--file <f>]src/ebpf/hades/cmd/cmd_btf.c
  • Usage: btf load <btf-file>src/ebpf/hades/cmd/cmd_btf.c
  • Usage: %s <command> [options]src/ebpf/hades/cmd/cmd_core.c
  • Usage: filters excl <add|remove|list|flush|load>src/ebpf/hades/cmd/cmd_filters.c
  • Usage: excl remove <idx>src/ebpf/hades/cmd/cmd_filters.c
  • Usage: excl load <file>src/ebpf/hades/cmd/cmd_filters.c
  • Usage: filters wl <add|remove|list|flush|load>src/ebpf/hades/cmd/cmd_filters.c
  • Usage: wl remove <idx>src/ebpf/hades/cmd/cmd_filters.c
  • Usage: wl load <file>src/ebpf/hades/cmd/cmd_filters.c
  • Usage: firewall rule <add|remove|list|flush>src/ebpf/hades/cmd/cmd_firewall.c
  • Usage: rule remove <index>src/ebpf/hades/cmd/cmd_firewall.c
  • Usage: firewall geoip <load|cc|mode|dump>src/ebpf/hades/cmd/cmd_firewall.c
  • Usage: geoip load <csv-path>src/ebpf/hades/cmd/cmd_firewall.c
  • Usage: geoip cc add|remove <CC>src/ebpf/hades/cmd/cmd_firewall.c
  • Usage: geoip mode blacklist|whitelist|offsrc/ebpf/hades/cmd/cmd_firewall.c
  • Usage: %s lsm <subcommand> [options]src/ebpf/hades/cmd/cmd_lsm.c
  • Usage: lsm policy <add|remove|list|flush>src/ebpf/hades/cmd/cmd_lsm.c
  • Usage: lsm policy remove <idx>src/ebpf/hades/cmd/cmd_lsm.c
  • Usage: research enable <name>src/ebpf/hades/cmd/cmd_research.c
  • Usage: research disable <name>src/ebpf/hades/cmd/cmd_research.c
  • Usage: research reset <name>src/ebpf/hades/cmd/cmd_research.c
  • Usage: sensors enable <name|id>src/ebpf/hades/cmd/cmd_sensors.c
  • Usage: sensors disable <name|id>src/ebpf/hades/cmd/cmd_sensors.c
  • Usage: sensors attach <name|id>src/ebpf/hades/cmd/cmd_sensors.c
  • Usage: sensors detach <name|id>src/ebpf/hades/cmd/cmd_sensors.c
  • Usage: %s waf <subcommand> [options]src/ebpf/hades/cmd/cmd_waf.c
  • Usage: waf rule <add|disable|enable|list>src/ebpf/hades/cmd/cmd_waf.c
  • Usage: waf rule %s <id>src/ebpf/hades/cmd/cmd_waf.c
  • Usage: waf load <rules-file>src/ebpf/hades/cmd/cmd_waf.c
  • Usage: waf threshold <score>src/ebpf/hades/cmd/cmd_waf.c
  • Usage: %s --map-fd <fd> [--csv <path>] [--simple <path>] [--block CC,...] [--allow CC,...] [--stats]src/ebpf/hades/firewall/geoip_loader.c
  • Usage: %s <obj_dir> <ev_dir> [--overload]src/ebpf/hades/tools/agent1_hades_helper.c
  • usage: ebpf load <object.o> [section] [--type=xdp|kprobe|tc|raw_tracepoint] [--no-core] [--verbose-core]src/ebpf/hades/tools/cmd_ebpf.c
  • usage: ebpf xdp --interface=I --obj=O.o [--action=pass|drop] [--pin] [--verbose-core] [--json]src/ebpf/hades/tools/cmd_ebpf.c
  • ebpf xdp: нужен --obj=<xdp.o>src/ebpf/hades/tools/cmd_ebpf.c
  • ebpf %s: нужен --obj=<obj.o>src/ebpf/hades/tools/cmd_ebpf.c
  • usage: ebpf map --action=dump|lookup|update|delete --map=NAME [--key=K] [--value=V]src/ebpf/hades/tools/cmd_ebpf.c
  • ebpf btf info: BTF ядра (%s): %s%s CO-RE : %s Установить путь : ebpf btf kern-btf <path>src/ebpf/hades/tools/cmd_ebpf.c
  • usage: ebpf btf types <name> [--kernel | --obj=<obj.o>]src/ebpf/hades/tools/cmd_ebpf.c
  • usage: ebpf btf offset <struct_name> <field_name>src/ebpf/hades/tools/cmd_ebpf.c
  • usage: ebpf btf core-check <obj.o> [--json]src/ebpf/hades/tools/cmd_ebpf.c
  • usage: ebpf btf core-relos <obj.o>src/ebpf/hades/tools/cmd_ebpf.c
  • usage: ebpf btf <info|load|dump|types|offset|core-check|core-relos|kern-btf>src/ebpf/hades/tools/cmd_ebpf.c
  • usage: ebpf tracing <load|attach|show>src/ebpf/hades/tools/cmd_ebpf.c
  • usage: ebpf tracing attach <tp> [prog_idx]src/ebpf/hades/tools/cmd_ebpf.c
  • usage: ebpf lsm <load|attach|list|hooks|stats|audit|policy>src/ebpf/hades/tools/cmd_ebpf.c
  • usage: hades_ctl sensors <list|enable|disable|info> [name]src/ebpf/hades/tools/hades_ctl.c
  • usage: hades_ctl filter <add-pid|del-pid|list> [pid]src/ebpf/hades/tools/hades_ctl.c
  • usage: hades_ctl config <get|set|reload> [key] [value]src/ebpf/hades/tools/hades_ctl.c
  • usage: hades_ctl bpf <maps|progs|dump-map> [name]src/ebpf/hades/tools/hades_ctl.c
edredr

Наблюдение за конечным узлом и координация разрешённого реагирования

Архитектура и API домена ↗

edr

EDR: инциденты с гипотезами и сомнениями; intent без authority — REFUSED

edr [--now <ns>] [--json] <status|coverage|incidents|incident|hypothesis|storyline|measure|hunt|recorder|response|posture|autonomy|pack|calibration|feed|mode>
Регистрация: src/edr/cmd_edr.c · обработчик cmd_edr

Развёрнутая встроенная справка

edr [--now <ns>] [--json] <команда>
  status | coverage | incidents [--state S --severity N --since T]
  incident show|explain <id> | incident merge <dst> <src> | incident split <id>
  incident disposition <id> <TP|FP|BENIGN|INCONCLUSIVE> --reason <text>
  hypothesis list|explain|refute <incident> [--evidence <id>]
  storyline <incident> [--dot]
  measure suggest [--mirage-active]
  hunt list | hunt show|run|retro <idx> [--lease L --intel-generation G --since T]
  recorder status|arm|disarm|tick [<entity> --gen G --deadline T]
  response preview <incident> <rung> [--emergency --rollback-tested --dryrun full|partial|unsupported]
  response approve|deny|status|rollback <request> [--authority A --lease L] | response history | response kill
  posture [raise|lower <tl> [--policy-token T]]
  autonomy show | autonomy set <event-class> <action-class> <level>
  pack trust <key> <pubkey_hex> | pack validate <file> [--observed <mask>] | pack shadow|canary|commit <slot> ... | pack rollback | pack drift | pack status
  calibration show | feed <file> | mode detect-only|full
  federation enable|node <id>|ingest <wire>|status|resolve <nA> <eA> <nB> <eB>   (Central-роль)
  fleet hypothesis list|explain|cell | fleet plan preview <incident> --nodes a,b --rung R | approve|result|status|reconcile
  node status|target <id> <gen>|check <idx>|partition|reconnect --epoch E --fencing F   (Node: повторная проверка)
время — только через --now; секреты и байты улик не печатаются
src/edr/edr_cli.c
elfelf · elf_load

Анализ ELF и лаборатория загрузки исполняемых объектов

Архитектура и API домена ↗

elf

ELF-лаборатория: разбор, анализ, загрузка и модификация ELF (@/memfd:/cdata:)

elf <load|exec|info|sections|segments|symbols|dynamic|relocations|strings|hexdump|compare|analyse|stats|tree|graph|verify|security|list|unload|write|restore|diff|modify|protect|section|symbol|crypt|pack|patch|fill|set-field|set-shfield|write-section|inject|strip|sym-set|rela-set|infect|scan-caves|av-scan|av-entropy|av-heuristic|av-yara|smc-patch|smc-nop|smc-trampoline|smc-analyze> ...
Регистрация: src/elf/cmd_elf.c · обработчик cmd_elf_ns

elf_load

ELF loader — inspect, verify and execute ELF binaries (exec2 = in-process linking + TLS)

elf_load <info|deps|sections|strings|verify|hexdump|exec|exec2|patch> ...
Регистрация: src/elf/cmd_elfload.c · обработчик cmd_elf

Развёрнутая встроенная справка

elf — ELF-лаборатория. Источник: @file | memfd:<ref> | cdata:[…]payload

  АНАЛИЗ:
  load <src> [--verify] [--hash=sha256] [--verbose] [-- args]   свой загрузчик
  exec <src> [--memfd] [--trace] [-- args]                      fork+execve
  info <src|id> [--verbose] [--field=entry|machine|type|class|phnum|shnum]
  sections <src> [--name=.N][--type=T][--flags=F][--hexdump [--lines=N]]
                 [--extract=file][--size]
  segments <src> [--index=N] [--verbose] [--map]
  symbols <src> [--find=S][--type=T][--bind=B][--exported][--demangle]
  dynamic <src> [--needed][--rpath][--init]
  relocations <src> [--resolve]
  strings <src> [--min=N]
  hexdump <src> [--offset=N][--length=N][--interpret][--find="55 48 89 e5"]
  compare <A> <B> [--diff][--common]
  analyse <src> | stats <src> [--by-type] | tree <src> | graph <src> [--output=f.dot]
  verify <src> | security <src>
  list | unload <id>

  МОДИФИКАЦИЯ:  без --output — in-place (@file/memfd:); с --output — копия
  write <src> --offset=N --bytes="XX XX..." [--backup] [--output=path]
              (--backup: создаёт <path>.bak перед правкой)
  restore @<path>          восстановить файл из <path>.bak
  diff <src1> [<src2>]     побайтовое сравнение; без src2 — сравнение с .bak
  modify <src> [--entry=0x...][--type=EXEC|DYN|REL|CORE]
               [--machine=x64|arm|arm64|riscv|x86][--flags=0xN][--output=path]
  protect <src> --method=xor --key=0xXX --section=.S [--output=path]
               (XOR-обфускация + SHA256 сохраняется в .elfchk)
  protect <src> --check    сравнить SHA256 с сохранённым .elfchk
  section add    <src> --name=.S {--data="text"|--hex="XX"|--file=f} --output=path
  section remove <src> --name=.S [--output=path]
  symbol add     (заглушка: объяснение ограничений)
  crypt <src> --algo=aes256 ...  (заглушка: нужна libssl)
  pack  <src> --algo=zlib   ...  (заглушка: нужна zlib)
  patch <src> --offset=N --hex="XX XX..." [--section=.S] [--output=path]
  fill  <src> --length=L [--offset=N] [--byte=0xXX] [--section=.S] [--output=path]
  set-field   <src> --field=F --value=0x...  [--output=path]
              F: entry | flags | type | machine | shstrndx | phoff | shoff
  set-shfield <src> {--section=.S|--index=N} --field=F --value=0x...  [--output=path]
              F: type | flags | addr | offset | size | link | info | align | entsize
  write-section <src> --name=.S {--file=data | --hex="XX XX"} [--output=path]
  inject <src> --name=.S {--file=data|--hex="XX XX"} --output=path
              [--type=PROGBITS|NOTE|NOBITS] [--flags=AWX] [--addr=0x...]
  strip <src> {--section=.S | --all-debug} [--output=path]
  sym-set <src> --find=NAME --value=0x... [--table=symtab|dynsym] [--output=path]
  rela-set <src> --section=.rela.X --index=N --addend=VAL [--output=path]
src/elf/cmd_elf.c
elf infect <technique> [--file=PATH] [--output=PATH] [opts]

МОДУЛЬ 1 — Базовые инфекции:
  pt-note          [--shellcode=HEX]  PT_NOTE → PT_LOAD, e_entry ← shellcode
  segment-padding  [--shellcode=HEX]  payload в gap между LOAD-сегментами
  text-extension   [--shellcode=HEX]  Silvio: расширить .text вперёд
  reverse-text     [--shellcode=HEX]  расширить .text назад (до 3.9 МБ)
  data-segment     [--shellcode=HEX]  payload в .data + PF_X
  code-cave        [--shellcode=HEX] [--address=0xVA]  Cave в .text
  entry-redirect   [--shellcode=HEX] [--preserve-entry] append + new PT_LOAD
  interp-hijack    --interp=PATH      замена PT_INTERP
  dt-init          --init=0xVA        изменить DT_INIT
  init-array       --init=0xVA        добавить fn в .init_array
  ctors            --init=0xVA        добавить fn в .ctors

МОДУЛЬ 2 — Продвинутые инфекции:
  convert-note                        PT_NOTE → PT_LOAD (без payload)
  eh-frame         (учебная заглушка)
  jcr              (учебная заглушка)
  gnu-version      (учебная заглушка)
  hash             (учебная заглушка)
  auxiliary        (учебная заглушка)
  rpath            --path=NEW_PATH    изменить DT_RPATH/DT_RUNPATH

МОДУЛЬ 3 — Runtime-инфекции (учебные):
  preload          --library=PATH     добавить DT_NEEDED
  library          --library=PATH     (то же, alias)
  proc-mem         --pid=PID          объяснение /proc/pid/mem
  ptrace           --pid=PID          объяснение ptrace-инъекции
  rop                                 объяснение ROP
  vdso                                объяснение vDSO hijacking
  vsyscall                            объяснение vsyscall trap

МОДУЛЬ 4 — Стелс:
  evade            [--level=1..5]     e_ident/e_flags/section shuffle
  fake-sign                           фейковая GPG note в конец файла
  build-id         --id=HEXSTRING     подмена .gnu.build-id
  shuffle                             перемешивание section header table
  strip            (см. elf strip)    удаление debug-секций
  timestamp        [--mtime=DATETIME] изменение временных меток
  immunity-bypass                     маскировка entry point

МОДУЛЬ 5 — Защита (учебные):
  bypass-seccomp                      объяснение seccomp bypass
  immunity                            добавить immunity marker
  test             --file=PATH        проверить immunity marker

МОДУЛЬ 6 — Компрессия/шифрование:
  pack             [--section=.S] [--key=0xXX]  XOR-упаковка секции
  encrypt          (используйте elf protect --method=xor)
  compress         (учебная заглушка, нужна zlib)

МОДУЛЬ 7 — Полиморфизм (учебные):
  polymorphic      --shellcode=HEX [--seed=N]
  mutate-decryptor --shellcode=HEX
  shuffle-instructions (учебная заглушка)
  garbage          [--count=N]         NOP-вставка в cave

МОДУЛЬ 8 — Persistence (учебные):
  persistence      --type=cron|systemd|bashrc  [--cron=EXPR] [--service=FILE]
  hide-process     (учебная заглушка)

МОДУЛЬ 9 — LLVM/LD (учебные):
  init-fini-ld     объяснение -Wl,-init,-fini
  dt-init-undef    объяснение DT_INIT с undefined symbols

ДИАГНОСТИКА:
  elf scan-caves <src> [--min-size=N]  найти code cave в исполняемых сегментах

Общие опции: --file=PATH  --output=PATH  --shellcode=HEXSTR
src/elf/cmd_elf_black.c
elf symbol add: добавление символа требует полной перестройки .symtab и .strtab
  с обновлением всех индексов — не реализовано.
  Используйте: objcopy --add-symbol  или  llvm-objcopy.
  Для изменения значения существующего: elf sym-set --find=NAME --value=0x...
src/elf/cmd_elf.c
elf av-scan --file=PATH [--db=sigs.txt] [--all] [--verbose]
  Сигнатурный сканер ELF-бинарников.
  Встроенная база: %d сигнатур (пакеры, шеллкод, инфекторы,
    бэкдоры, дропперы, антиотладка, persistence).
  --db=FILE     дополнительная внешняя база (pipe-separated):
                NAME|cat|desc|HEX_PAT|HEX_MASK|where|severity
                where: entry/text/any/eof/bof/dynstr
                severity: 0=info 1=low 2=medium 3=high 4=critical
  --all         показать все сигнатуры включая INFO
  --verbose     показать смещение совпадения
src/elf/cmd_elf_white.c
elf av-entropy --file=PATH [--threshold=7.0] [--sections] [--histogram]
  Shannon-энтропия по секциям ELF.
  Норма: .text ~5.5-6.5, .data ~3-5.
  Упакованные/шифрованные секции: > 7.0 бит.
  --threshold=N  порог предупреждения (default 7.0)
  --sections     показать все секции (по умолч. только > threshold)
  --histogram    ASCII-гистограмма байтового распределения
src/elf/cmd_elf_white.c
elf av-heuristic --file=PATH [--verbose] [--json]
  Эвристический анализ ELF по 20 признакам заражения.
  Scoring:
    0–20   CLEAN
    21–50  SUSPICIOUS
    51–100 LIKELY_INFECTED
    101+   INFECTED
src/elf/cmd_elf_white.c
usage: elf_load <subcommand> ...

  elf_load info     <ref|@file>             comprehensive ELF header + sections + deps
  elf_load deps     <ref|@file>             dynamic dependencies (PT_INTERP + DT_NEEDED)
  elf_load sections <ref|@file>             section header table
  elf_load strings  <ref|@file> [minlen]    extract printable strings (default min=4)
  elf_load verify   <ref|@file>             structural validation checks
  elf_load hexdump  <ref|@file> [off [len]] raw hex dump of file bytes
  elf_load exec     <ref|@file> [-- args…] fork-exec (system ld-linux for dynamic)
  elf_load exec-inplace <ref|@file> [-- …] exec in-place (replaces this process)
  elf_load exec2    <ref|@file> [-- args…] fork-exec, in-process linking + TLS (no ld.so)
  elf_load exec2-inplace <ref|@file> [-- …] in-process linking + TLS, in-place
  elf_load patch    memfd:<ref> <off> <hex> patch bytes (writable memfd only)

  ref  =  memfd:<name|id>   in-process memfd object
       |  @<path>           regular file on disk
src/elf/cmd_elfload.c

Аргументы и дополнительные формы вызова

  • usage: elf analyse <src>src/elf/cmd_elf.c
  • usage: elf compare <A> <B> [--diff|--common]src/elf/cmd_elf.c
  • usage: elf patch <src> --offset=N --hex="XX XX..." [--section=.sect] [--output=path]src/elf/cmd_elf.c
  • usage: elf fill <src> --length=L [--offset=N] [--byte=0xXX] [--section=.sect] [--output=path] по умолчанию --byte=0x90 (NOP x86)src/elf/cmd_elf.c
  • usage: elf set-field <src> --field=FIELD --value=0x... [--output=path] FIELD: entry | flags | type | machine | shstrndx | phoff | shoffsrc/elf/cmd_elf.c
  • usage: elf set-shfield <src> {--section=.name | --index=N} --field=FIELD --value=0x... [--output=path] FIELD: type | flags | addr | offset | size | link | info | align | entsizesrc/elf/cmd_elf.c
  • usage: elf write-section <src> --name=.sect {--file=data.bin | --hex="XX XX"} [--output=path]src/elf/cmd_elf.c
  • usage: elf inject <src> --name=.sect {--file=data|--hex="XX XX"} --output=path [--type=PROGBITS|NOTE|NOBITS] [--flags=AWX] [--addr=0x...]src/elf/cmd_elf.c
  • usage: elf strip <src> {--section=.name | --all-debug} [--output=path] --all-debug: обнуляет .debug_*, .symtab, .strtab, .commentsrc/elf/cmd_elf.c
  • usage: elf sym-set <src> --find=SYMBOL --value=0x... [--table=symtab|dynsym] [--output=path]src/elf/cmd_elf.c
  • usage: elf rela-set <src> --section=.rela.X --index=N --addend=VAL [--output=path]src/elf/cmd_elf.c
  • usage: elf write <src> --offset=N --bytes="XX XX..." [--backup] [--output=path]src/elf/cmd_elf.c
  • usage: elf restore @<path>src/elf/cmd_elf.c
  • usage: elf diff <src1> [<src2>]src/elf/cmd_elf.c
  • usage: elf modify <src> [--entry=0x...][--type=EXEC|DYN|REL|CORE] [--machine=x64|arm|arm64|riscv|x86] [--flags=0xN] [--output=path]src/elf/cmd_elf.c
  • usage: elf protect <src> --method=xor --key=0xXX --section=.name [--output=path] elf protect <src> --checksrc/elf/cmd_elf.c
  • usage: elf section add|remove <src> ...src/elf/cmd_elf.c
  • elf crypt: AES/ChaCha-шифрование не реализовано (требуется libssl/libsodium). Аналог с XOR: elf protect --method=xor --key=0xXX --section=.namesrc/elf/cmd_elf.c
  • elf pack: упаковка секций не реализована (требуется zlib/lz4). Для обнуления секции: elf strip --section=.namesrc/elf/cmd_elf.c
  • usage: elf unload <id>src/elf/cmd_elf.c
  • usage: elf scan-caves <src|--file=PATH> [--min-size=N] [--byte=0x00] Ищет длинные runs 0x00/0x90 в исполняемых сегментах.src/elf/cmd_elf_black.c
  • usage: elf info <memfd:<ref>|@file>src/elf/cmd_elfload.c
  • usage: elf deps <memfd:<ref>|@file>src/elf/cmd_elfload.c
  • usage: elf sections <memfd:<ref>|@file>src/elf/cmd_elfload.c
  • usage: elf strings <memfd:<ref>|@file> [minlen]src/elf/cmd_elfload.c
  • usage: elf verify <memfd:<ref>|@file>src/elf/cmd_elfload.c
  • usage: elf hexdump <memfd:<ref>|@file> [offset] [len]src/elf/cmd_elfload.c
  • usage: elf_load patch memfd:<ref> <offset> <hex-data> patches bytes in a writable memfd objectsrc/elf/cmd_elfload.c
  • usage: elf_load %s <memfd:<ref>|@file> [-- args...]src/elf/cmd_elfload.c
fabricfabric

Композиция версионированных поставщиков возможностей

Архитектура и API домена ↗

fabric

плоскость провайдеров: подъём, подключение, состояние

fabric <status|up|down|providers|attach|pump|detach> …
Регистрация: src/fabric/cmd_fabric.c · обработчик cmd_fabric

Развёрнутая встроенная справка

fabric — плоскость провайдеров: подъём, подключение, состояние
  fabric status                        поднята ли плоскость и что в ней
  fabric up | down                     подъём и останов плоскости
  fabric providers                     провайдеры, их состояние и публикация
  fabric attach <process|file|network> --replay=ФАЙЛ [--publish]
  fabric attach <process|file|network> --live [--pin=ПУТЬ]
  fabric pump <id> [--max=N]           протолкнуть записи в кольцо SENSE
  fabric detach <id>

Подъём ничего не подключает: сенсоры включает оператор, а не компоновщик.
src/fabric/cmd_fabric.c

Аргументы и дополнительные формы вызова

  • fabric attach: --live и --replay взаимоисключающиsrc/fabric/cmd_fabric.c
  • fabric attach: нужен --replay=ФАЙЛ или --livesrc/fabric/cmd_fabric.c
flowотдельный интерфейс / библиотека

Пассивный анализ пакетов и потоков

Архитектура и API домена ↗

Самостоятельная CLI-команда и отдельная usage-справка не обнаружены. Управление находится у потребителей API — audit, ndr. Контракты и границы приведены в досье домена.

forensicforensic

Сбор, связывание и проверка материалов расследования

Архитектура и API домена ↗

forensic

FORENSIC: цепочка с разрывами, custody без байтов, пакет с дефектами по именам

forensic [--now <ns>] [--json] <status|chain|checkpoint|seal|custody|collect|timeline|export|verify|why|anchor|retention|purge>
Регистрация: src/forensic/cmd_forensic.c · обработчик cmd_forensic

Развёрнутая встроенная справка

forensic [--now <ns>] [--json] <команда>
  status | signer <seed-file>
  append <type> <payload> [--parent <rec> --level L --synthetic --evidence-bypassed --wall T]
  chain verify [--since T] | chain sources | chain register <id> <name> | chain external <src> <seq> <digest> | chain show <rec> | chain list | chain reemit
  checkpoint now
  seal <kind> <file> --owner m:i:g [--incident I --privacy P --taint T --level L]
  custody list | custody show|verify <handle>
  collect list | collect show|run <idx> --target <entity> --incident <id> [--budget B]
  timeline [--as-known-then T] [--two-orders] [--limit N]
  export <incident> --out <dir> --authority <id> [--redact <privacy>] [--incident-file <f>]
  verify <package-dir> [--anchor-root <hex> --proof <file>]   (rc 0/1, дефекты по именам)
  why <package-dir> [<decision-id>]
  anchor status | anchor prove <index> [--out <file>]
  retention show | retention hold|release <handle> --authority <id>
  purge --expired --sealed-before <ns> | --handle <h>   --authority <id>
время — только через --now; байты улик не печатаются
src/forensic/forensic_cli.c
fsxfsx

Файловое исследование, контроль и оркестрация экспериментов

Архитектура и API домена ↗

fsx

FUSE sandbox / overlay / telemetry (not a stealth module)

fsx <mount|unmount|status|snapshot|canary|...>
Регистрация: src/fsx/cmd_fsx.c · обработчик cmd_fsx

Развёрнутая встроенная справка

fsx — FUSE Sandbox eXtension (laboratory filesystem view)

  fsx init
  fsx status [id]
  fsx sessions | mounts
  fsx mount <source> <mountpoint> --mode=passthrough|readonly|overlay|sandbox
            [--name NAME] [--upper PATH] [--max-overlay BYTES]
  fsx unmount <id|name>

  fsx snapshot create <id> <name>
  fsx snapshot list <id>
  fsx snapshot diff <id> <a> <b|current>
  fsx diff <id> [--hash]

  fsx quarantine add|del|list <id> [path]
  fsx canary add|del|list <id> [path]
  fsx canary profile <id> credentials|ssh
  fsx policy add <id> <prio> <glob> <action> [target]
  fsx policy del <id> <prio>

  fsx audit on|off <id> [--read] [--write] [--metadata] [--hash]
  fsx events <id>
  fsx timeline <id>
  fsx stats <id>
  fsx artifacts <id>
  fsx verify <id>
  fsx report <id> [--out=PATH]
  fsx sandbox exec <id> <cmd> [args...]
  fsx integration <eventbus|malscripts|ebpf|hidecheck> on|off [id]
  fsx event-filter add <id> [--include=GLOB] [--exclude=GLOB] [--operations=LIST]
  fsx event-filter del <id> [--include=GLOB] [--exclude=GLOB]
  fsx event-filter list <id>
  fsx config

FSX does not hide PlatX, mounts or processes from the administrator.
src/fsx/cmd_fsx.c

Аргументы и дополнительные формы вызова

  • usage: fsx mount <source> <mountpoint> [--mode=overlay]src/fsx/cmd_fsx.c
  • usage: fsx unmount <id|name>src/fsx/cmd_fsx.c
  • usage: fsx snapshot create|list|diff ...src/fsx/cmd_fsx.c
  • usage: fsx diff <id> [--hash]src/fsx/cmd_fsx.c
  • usage: fsx quarantine add|del|list <id> [path]src/fsx/cmd_fsx.c
  • usage: fsx canary add|del|list|profile <id> ...src/fsx/cmd_fsx.c
  • usage: fsx policy add <id> <prio> <glob> <action> [target]src/fsx/cmd_fsx.c
  • usage: fsx audit on|off <id> [--read] [--write] [--metadata] [--hash]src/fsx/cmd_fsx.c
  • usage: fsx events <id>src/fsx/cmd_fsx.c
  • usage: fsx report <id> [--out=PATH]src/fsx/cmd_fsx.c
  • usage: fsx sandbox create <name> [--root=PATH] fsx sandbox exec <id> <cmd> [args...]src/fsx/cmd_fsx.c
  • usage: fsx integration <eventbus|malscripts|ebpf|hidecheck> on|off [id]src/fsx/cmd_fsx.c
  • usage: fsx event-filter add|del|list <id> [--include=GLOB] [--exclude=GLOB] [--operations=write,create,...]src/fsx/cmd_fsx.c
fusefuse

Жизненный цикл файловых workers на FUSE

Архитектура и API домена ↗

fuse

менеджер зашифрованных FUSE-файловых систем

fuse <mount|umount|status|list|reload-key>
Регистрация: src/fuse/cmd_fuse.c · обработчик cmd_fuse_dispatch

Развёрнутая встроенная справка

fuse — менеджер зашифрованных FUSE-файловых систем

  fuse mount <mountpoint> [опции]   — смонтировать
    --bin     <path>                   путь к бинарнику (def: memfuse_like_driver)
    --key-hex <hex64>                  ключ шифрования (64 hex-символа)
    --cipher  <name>                   алгоритм (def: chacha20poly1305)
    --ro                               смонтировать только для чтения
    --noexec                           без exec
    --allow-other                      разрешить доступ другим пользователям

  fuse umount <id|mountpoint>        — размонтировать
    --force                            SIGKILL без ожидания

  fuse status [id]                   — проверить состояние
  fuse list                          — список монтирований
  fuse reload-key <id> [--key-hex <hex64>] — сменить ключ на лету

Поддерживаемые алгоритмы шифрования:
  chacha20poly1305, xchacha20poly1305, aes256gcm

Пример:
  fuse mount /mnt/secret --key-hex <hex64> --cipher xchacha20poly1305
  fuse status
  fuse umount /mnt/secret
src/fuse/cmd_fuse.c

Аргументы и дополнительные формы вызова

  • usage: fuse mount <mountpoint> [опции] (fuse help для полной справки)src/fuse/cmd_fuse.c
  • usage: fuse umount <id|mountpoint> [--force]src/fuse/cmd_fuse.c
  • usage: fuse reload-key <id> [--key-hex <hex64>]src/fuse/cmd_fuse.c
gadgetgadget

Каталог возможностей и выбор операции

Архитектура и API домена ↗

gadget

list or run one gadget for a capability

gadget <explain|request> [capability]
Регистрация: src/gadget/cmd_gadget.c · обработчик cmd_gadget

Развёрнутая встроенная справка

gadget — list or run one strategy for a capability
  gadget explain [capability]              candidates + Surface status
                                           (probe only; no lease)
  gadget request [capability] [SNAPSHOT|EXACT]
                 [--nolease] [--context id[:gen]]
                                           execute exactly one gadget
src/gadget/cmd_gadget.c

Аргументы и дополнительные формы вызова

  • gadget request: --context id[:gen]src/gadget/cmd_gadget.c
hookhook · kprobe

Общий слой перехватчиков и их провайдеров

Архитектура и API домена ↗

hook

interpose on a symbol (TAP observation; never changes retval)

hook <create|activate|detach|status>
Регистрация: src/hook/cmd_hook.c · обработчик cmd_hook

kprobe

kprobe tracing via tracefs: observer-only, no interception

kprobe <add|rm|on|off|read|help> ...
Регистрация: src/hook/hook_kprobe.c · обработчик cmd_kprobe

Развёрнутая встроенная справка

kprobe — kernel probe tracing via tracefs (read-only observation)

  kprobe add  --name=N --spec="p:N func"   register a kprobe
  kprobe rm   --name=N                      remove a kprobe
  kprobe on   --name=N                      enable event collection
  kprobe off  --name=N                      disable event collection
  kprobe read [--bytes=N]                   read events from trace_pipe
  kprobe help

Probe spec examples:
  "p:myprobe do_sys_open"         — entry of do_sys_open
  "r:myret  do_sys_open $retval"  — return + capture retval
  "p:sysrd  __x64_sys_read"       — entry of sys_read

Events are logged ONLY; probed functions run unmodified.
Laboratory path: NOT a Hook Engine provider. No hook id, no
transaction, no fact on the event ring.
Requires CAP_SYS_ADMIN or debugfs write access.
Uses:
src/hook/hook_kprobe.c
hook — interpose on a symbol; observation never changes the return value
  hook create <provider> <symbol>   TAP attach (mock provable anywhere)
  hook activate <id>                commit: emits PLAT_EV_HOOK_ATTACH
  hook detach <id> <attach>         drain and detach one point
  hook status [id]                  provider health, or one hook's picture
  hook probe <provider> <symbol>    create+activate in one shot; prints the
                                    event trail so PLAT_EV_HOOK_ATTACH is seen
  hook events                       recent platform event ids (hex)
src/hook/cmd_hook.c

Аргументы и дополнительные формы вызова

  • usage: hook create <provider> <symbol>src/hook/cmd_hook.c
  • usage: hook activate <id>src/hook/cmd_hook.c
  • usage: hook detach <id> <attach>src/hook/cmd_hook.c
  • usage: hook probe <provider> <symbol>src/hook/cmd_hook.c
  • usage: kprobe add --name=N --spec="p:N func"src/hook/hook_kprobe.c
  • usage: kprobe rm --name=Nsrc/hook/hook_kprobe.c
  • usage: kprobe on --name=Nsrc/hook/hook_kprobe.c
  • usage: kprobe off --name=Nsrc/hook/hook_kprobe.c
integrityintegrity

Наблюдение целостности и управление проверками модулей

Архитектура и API домена ↗

integrity

LKM integrity module control (userspace CLI)

integrity <load|unload|status|scan|report|watch|syscall|net|proc|config|hide|bpf>
Регистрация: src/integrity/cmd_integrity.c · обработчик cmd_integrity

Аргументы и дополнительные формы вызова

  • usage: integrity syscall <verify|log|hook|set|restore|list>src/integrity/cmd_integrity.c
  • usage: integrity net <rule|show|set>src/integrity/cmd_integrity.c
  • usage: integrity net rule <add|del|list>src/integrity/cmd_integrity.c
  • usage: integrity proc <set|add-override|show>src/integrity/cmd_integrity.c
  • usage: integrity proc set <file> <param> <value>src/integrity/cmd_integrity.c
  • usage: integrity proc add-override <file> <pattern>src/integrity/cmd_integrity.c
  • usage: integrity config <save|load> <file>src/integrity/cmd_integrity.c
  • usage: integrity hide list (hide module/pid/file/mask/anti-debug are not supported)src/integrity/cmd_integrity.c
  • usage: integrity bpf <attach|detach|list> bpf attach <prog_fd> [--type=<0|1|2>] [--symbol=<name>] bpf detach <link_id> bpf listsrc/integrity/cmd_integrity.c
  • usage: integrity bpf attach <prog_fd> [--type=N] [--symbol=SYM]src/integrity/cmd_integrity.c
  • usage: integrity bpf detach <link_id>src/integrity/cmd_integrity.c
  • usage: integrity <load|unload|status|scan|report|watch|syscall|net|proc|config|hide|bpf>src/integrity/cmd_integrity.c
  • integrity.hook_syscall: usage hook_syscall(nr, action[, name])src/integrity/integrity_script.c
  • integrity.bpf_attach: usage bpf_attach(prog_fd, type[, symbol])src/integrity/integrity_script.c
keyringkeyring

Сервис ключевого материала и защищённых значений

Архитектура и API домена ↗

keyring

KeyringDaemon: хранилище секретов + замена IPC

keyring <start|status|get|set|list|send|...>
Регистрация: src/keyring/cmd_keyring.c · обработчик cmd_keyring_dispatch

Развёрнутая встроенная справка

keyring — KeyringDaemon: централизованное хранилище секретов

Управление демоном:
  keyring start  --master_key=PW [--socket=P] [--mode=M] [--keyring=K] [--backend=B]
                 mode: local|daemon|server|service   keyring: user|session|process|thread
  keyring status | stop | reload | gc | ping | enckey
  keyring config [path]                — сгенерировать дефолтный конфиг

Секреты (keyring-cli или local):
  keyring get  --desc=D [--hex]
  keyring set  --desc=D --data=X [--type=T] [--ttl=S] [--hex]
  keyring del  --serial=N
  keyring list [--all]

C-порт (прямой in-process store):
  keyring create <desc> <data> | read <serial> | search <desc> | revoke <serial>

Замена IPC (keyring как транспорт):
  keyring announce <node> <mode> [endpoint]   mode: socket|keyring
  keyring discover
  keyring send <from> <to> <op> <data>        — гибрид: mailbox + socket-notify
  keyring post <to> <data> [ttl] | recv <to> | pending <to>   — чистый mailbox

Интеграция:
  keyring seed-vault [--desc=D]   — enc-key из keyring → мастер-ключ VaultFS
                                    (далее его наследует и FUSE)

Типы ключей: user, logon, big_key.  Макс. размер секрета: 1 MiB.
src/keyring/cmd_keyring.c
# keyring.conf — конфигурация KeyringDaemon (генерируется по умолчанию)
#
# mode:     как запускать демон
#   local   — хранилище в процессе, без сокета (внутри REPL/batch)
#   daemon  — форк в фон, обслуживает UNIX socket
#   server  — то же на переднем плане (foreground)
#   service — супер-демон: memfd + keyring в одном процессе
#
# backend:  хранилище секретов
#   auto    — kernel keyring если доступен, иначе memory
#   kernel  — Linux kernel keyring
#   memory  — in-process fallback
#
# keyring:  область kernel keyring — user|session|process|thread

[keyringd]
mode        = %s
backend     = %s
keyring     = %s
socket      = %s
gc_interval = %d
with_memfd  = %d
memfd_sock  = %s
src/keyring/keyring_util.c

Аргументы и дополнительные формы вызова

  • usage: keyring get --desc=D [--hex]src/keyring/cmd_keyring.c
  • usage: keyring set --desc=D --data=X [--type=T] [--ttl=S] [--hex]src/keyring/cmd_keyring.c
  • usage: keyring del --serial=Nsrc/keyring/cmd_keyring.c
  • usage: keyring create <desc> <data>src/keyring/cmd_keyring.c
  • usage: keyring read <serial>src/keyring/cmd_keyring.c
  • usage: keyring search <desc>src/keyring/cmd_keyring.c
  • usage: keyring revoke <serial>src/keyring/cmd_keyring.c
  • usage: keyring announce <node> <mode> [endpoint] mode: socket | keyringsrc/keyring/cmd_keyring.c
  • usage: keyring send <from> <to> <op> <data>src/keyring/cmd_keyring.c
  • usage: keyring post <to> <data> [ttl]src/keyring/cmd_keyring.c
  • usage: keyring recv <to>src/keyring/cmd_keyring.c
  • usage: keyring pending <to>src/keyring/cmd_keyring.c
  • ERR usage: GET <desc>src/keyring/keyring_daemon.c
  • ERR usage: READ <serial>src/keyring/keyring_daemon.c
  • ERR usage: SEARCH <desc>src/keyring/keyring_daemon.c
  • ERR usage: SET <type> <desc> <hex> [ttl]src/keyring/keyring_daemon.c
  • ERR usage: DEL <serial>src/keyring/keyring_daemon.c
  • ERR usage: POST <to> <hex> [ttl]src/keyring/keyring_daemon.c
  • ERR usage: RECV <to>src/keyring/keyring_daemon.c
  • ERR usage: PENDING <to>src/keyring/keyring_daemon.c
  • ERR usage: ANNOUNCE <node> <mode> <endpoint>src/keyring/keyring_daemon.c
  • ERR usage: SEND <from> <to> <op> <hex>src/keyring/keyring_daemon.c
leaselease

Ограниченные полномочия, контекст и поколение

Архитектура и API домена ↗

lease

grant or explain a capability lease

lease <issue|revoke|check|list> …
Регистрация: src/lease/cmd_lease.c · обработчик cmd_lease

Развёрнутая встроенная справка

lease — grant, revoke, or explain a capability for one context
  lease issue <cap> [--context id] [--ttl ms] [--oneshot]
  lease revoke <id>
  lease check <cap> [--context id]     VALID or DENIED/STALE/EXPIRED + reason
  lease list                           live table, bounded
src/lease/cmd_lease.c

Аргументы и дополнительные формы вызова

  • lease issue: --context needs an idsrc/lease/cmd_lease.c
  • lease issue: --context must be a positive integersrc/lease/cmd_lease.c
  • lease issue: --ttl needs millisecondssrc/lease/cmd_lease.c
  • lease issue: --ttl must be an integersrc/lease/cmd_lease.c
  • lease check: --context needs an idsrc/lease/cmd_lease.c
  • lease check: --context must be a positive integersrc/lease/cmd_lease.c
loglog

Журналы исполнения, редактирование чувствительных полей и ротация

Архитектура и API домена ↗

log

Platform logging control and ring-buffer viewer

log <level|tail|search|sinks|help> [options]
Регистрация: src/log/cmd_log.c · обработчик (cmd_fn_t)c_log

Развёрнутая встроенная справка

log — platform logging control

  log level                              show global level
  log level --level=<L>                  set global level
  log level --subsys=<S> --level=<L>     set per-subsystem level
  log tail  [--count=N] [--level=<L>]    show ring-buffer entries
  log search --pattern=TEXT [--subsys=S] search in ring buffer
  log sinks                              ring buffer stats
  log help

Levels (ascending): trace  debug  info  warn  error  fatal
Subsystems: platform  mesh  mbus  http  plugin  taskmgr  ...
src/log/cmd_log.c

Аргументы и дополнительные формы вызова

  • usage: log search --pattern=TEXT [--subsys=S] [--count=N]src/log/cmd_log.c
mbusmbus

Асинхронные акторы, сообщения и почтовые ящики

Архитектура и API домена ↗

mbus

асинхронная актор-шина: очереди, req/resp, распределённость

mbus <status|actors|send|request|publish|sub|node|broker|...>
Регистрация: src/mbus/cmd_mbus.c · обработчик cmd_mbus

Развёрнутая встроенная справка

mbus — асинхронная актор-шина (очереди сообщений, req/resp, распределённость)

  mbus status                    сводка и счётчики
  mbus actors                    список акторов и их mailbox
  mbus spawn-echo <name>         демо-актор (REQUEST→echo, EVENT→лог)
  mbus send <dst> <text...>      событие актору (fire-and-forget)
  mbus request <dst> <text> [ms] запрос с ожиданием ответа
  mbus publish <topic> <text>    публикация в топик
  mbus sub|unsub <topic>         подписка встроенного логгера
  mbus tail [n]                  последние n событий логгера
  mbus node <host> <port>        подключить узел к брокеру
  mbus node-off                  отключить узел
  mbus broker <port>             запустить брокер в процессе
  mbus broker-off                остановить брокер
  mbus nodes                     узлы, подключённые к брокеру

Адрес удалённого актора: "node/actor". Имя узла: env MBUS_NODE.
src/mbus/cmd_mbus.c

Аргументы и дополнительные формы вызова

  • mbus request <dst> <text> [timeout_ms]src/mbus/cmd_mbus.c
  • mbus publish <topic> <text...>src/mbus/cmd_mbus.c
  • mbus %s <topic>src/mbus/cmd_mbus.c
memfdmemfd

Управляемые анонимные объекты памяти

Архитектура и API домена ↗

memfd

memfd object manager — anonymous memory files

memfd <create|write|read|stat|seal|mmap|load|...>
Регистрация: src/memfd/cmd_memfd.c · обработчик cmd_memfd_ns

Развёрнутая встроенная справка

memfd — anonymous memory-file manager

  memfd create   <name> [cloexec] [sealing] [exec] [noexec]
  memfd write    memfd:<ref> [off] <@file|hex|[cdata]>
  memfd read     memfd:<ref> [start [end]]
  memfd hexdump  memfd:<ref> [start [end]]      (alias for read)
  memfd truncate memfd:<ref> <size> [grow]
  memfd chmod    memfd:<ref> <octal-mode>
  memfd seal     memfd:<ref> <seal,grow,write,shrink,all,...>
  memfd mmap     memfd:<ref> <rwx> [private|shared] [len]
  memfd mprotect memfd:<ref> <rwx>
  memfd list     [filter]
  memfd stat     memfd:<ref>
  memfd close    memfd:<ref>
  memfd getfd    memfd:<ref>
  memfd load     <name> <@file|hex|[cdata]> [flags]
  memfd cdata    save|load|info ...

  ref syntax:  memfd:<name>  or  memfd:<id>
src/memfd/cmd_memfd.c

Аргументы и дополнительные формы вызова

  • usage: create <name> [cloexec] [sealing] [exec] [noexec]src/memfd/cmd_memfd.c
  • usage: write memfd:<ref> [offset] <@file|hex|[cdata]>src/memfd/cmd_memfd.c
  • usage: read memfd:<ref> [start [end]]src/memfd/cmd_memfd.c
  • usage: truncate memfd:<ref> <size> [grow]src/memfd/cmd_memfd.c
  • usage: chmod memfd:<ref> <octal-mode>src/memfd/cmd_memfd.c
  • usage: seal memfd:<ref> seal|shrink|grow|write|future-write|exec|allsrc/memfd/cmd_memfd.c
  • usage: mmap memfd:<ref> <rwx> [private|shared] [len]src/memfd/cmd_memfd.c
  • usage: mprotect memfd:<ref> <rwx>src/memfd/cmd_memfd.c
  • usage: stat memfd:<ref>src/memfd/cmd_memfd.c
  • usage: close memfd:<ref>src/memfd/cmd_memfd.c
  • usage: getfd memfd:<ref>src/memfd/cmd_memfd.c
  • usage: load <name> <@file|hex|[cdata]> [flags]src/memfd/cmd_memfd.c
  • usage: cdata save|load|info ...src/memfd/cmd_memfd.c
  • usage: cdata info <container>src/memfd/cmd_memfd.c
  • usage: cdata save <@file|hex> [--enc=base64|hex] [--compress=gzip] [--encrypt=rc4|chacha20|aes256gcm] [--key=SECRET] [--hash=sha256|crc32]src/memfd/cmd_memfd.c
  • usage: cdata load <container> [--key=SECRET]src/memfd/cmd_memfd.c
meshmesh

Peer mesh, идентичность узлов и зашифрованная связь

Архитектура и API домена ↗

mesh

P2P mesh-сеть (X25519 handshake, шифрованные сообщения)

mesh <start|stop|status|peers|send|recv|ping|prune>
Регистрация: src/mesh/cmd_mesh.c · обработчик cmd_mesh

Развёрнутая встроенная справка

mesh — P2P mesh-сеть (X25519 handshake, шифрованные сообщения)

  mesh start [--port=N] [--bootstrap=H:P,...] [--agent=NAME]
  mesh stop | status | peers | prune
  mesh send --peer=ID --message="text"
  mesh recv [--timeout=SEC] [--count=N]
  mesh ping --peer=ID

Идентичность узла хранится в keyring (mesh:privkey). Адрес пира —
полный peer_id или его префикс. MVP-этапы 1–4; mDNS/DHT/группы далее.
src/mesh/cmd_mesh.c
mfbtотдельный интерфейс / библиотека

Моделирование динамики противоборства и управляемого хаоса

Архитектура и API домена ↗

Самостоятельная CLI-команда и отдельная usage-справка не обнаружены. Управление находится у потребителей API — mission, mirage. Контракты и границы приведены в досье домена.

miragemirage

Синтетические миры, Consistency Oracle и журнал взаимодействий

Архитектура и API домена ↗

mirage

MIRAGE sandbox: worlds, rehearsal stats, oracle (SYNTHETIC/OBSERVED)

mirage <world|scenario|replay|oracle>
Регистрация: src/mirage/cmd_mirage.c · обработчик cmd_mirage

Развёрнутая встроенная справка

mirage <подкоманда>

  world      список sandbox-миров [SYNTHETIC]
  scenario   агрегат сценариев репетиций [SYNTHETIC]
  replay     детальная статистика репетиций [SYNTHETIC]
  oracle     сравнение SYNTHETIC vs OBSERVED (UNCERTAIN без данных)

SYNTHETIC ≠ OBSERVED. Каждая запись помечается источником.
UNCERTAIN ≠ ok. Без OBSERVED-данных oracle не может подтвердить.
src/mirage/mirage_cli.c

Аргументы и дополнительные формы вызова

  • envelope limits or usage snapshot unavailable; budget not verifiedsrc/mirage/mirage_oracle_basic.c
missionотдельный интерфейс / библиотека

Спецификация миссии и отдельное состояние её выполнения

Архитектура и API домена ↗

Самостоятельная CLI-команда и отдельная usage-справка не обнаружены. Управление находится у потребителей API — central, gadget. Контракты и границы приведены в досье домена.

modhostmodhost

Размещение, проверка и изолированное исполнение модулей

Архитектура и API домена ↗

modhost

загрузчик модулей: inventory, lockdown-verify, sandbox rehearse

modhost <list|verify|rehearse>
Регистрация: src/modhost/cmd_modhost.c · обработчик cmd_modhost

Развёрнутая встроенная справка

modhost <подкоманда>

  list              инвентарь загруженных модулей
  verify [<name>]   lockdown-проверка (все или конкретный)
  rehearse <path>   sandbox-репетиция в mirage world
        [--world N] выбрать мир (по умолчанию 0)

PARTIAL ≠ ok. rehearse без sig/init/fini → PARTIAL, не pass.
INV-MODHOST-01: без MSX-подписи → REFUSE в production.
src/modhost/modhost_cli.c

Аргументы и дополнительные формы вызова

  • modhost rehearse <module-path> [--world <id>]src/modhost/modhost_cli.c
msxscript · ms · msx

Язык сценариев и автоматизация операций платформы

Архитектура и API домена ↗

script

интерпретатор языка сценариев .ms

script <file.ms|--resident FILE|--eval CODE|--check FILE|--repl> [-- арг...]
Регистрация: src/msx/cmd_msx.c · обработчик cmd_script

ms

алиас команды script

ms — алиас script
Регистрация: src/msx/cmd_msx.c · обработчик cmd_script

msx

алиас команды script

msx — алиас script
Регистрация: src/msx/cmd_msx.c · обработчик cmd_script

Развёрнутая встроенная справка

script (алиасы ms, msx) — интерпретатор языка сценариев .ms

  script <file.ms> [-- арг...]   выполнить сценарий из файла
  script --resident <file.ms>    загрузить в resident VM с лимитами
  script --eval "<код>"          выполнить строку
  script --check <file.ms>       проверить синтаксис без выполнения
  script --repl                  интерактивный режим

Подпись авторства (ed25519, отказ запускать поддельное):
  script keygen                          выпустить пару seed/pubkey
  script sign <file.ms> --seed=<64hex> [--comment=//|#] [--in-place]
  script verify <file.ms> [--pubkey=<64hex>]
  script <file.ms> --require-sig --pubkey=<64hex>   запуск только при верной подписи

Обфускация исходника (сокрытие, НЕ защита — движок разворачивает сам):
  script obf-token <text>            напечатать \obf(OBF1:...) для вставки
  script obf-src <file.ms> [--in-place]  скрыть строковые литералы файла
  script bin-token <text> [--decoy=N] [--pad=P]  \*DATA_INSERT*\...\*END*\ (счётчик \bin<N>)
  в коде: let \obf(OBF1:..) = secret.env("\obf(OBF1:..)")

Платформенный бинарь сам является интерпретатором:
  ./memfd ./msxscript/test.ms арг1 арг2
поэтому сценарий может начинаться с необязательной строки
  #!/usr/bin/env msx
— она игнорируется (и не сдвигает нумерацию строк в ошибках).
Всё после «--» попадает в сценарий: args[0..], argc, msx_name.

Опции:


Без явных флагов действует предел %llu итераций (снять: --no-limits).
Неизвестная опция — ошибка: флаг песочницы не может «не сработать» молча.

Язык: let/fn/if/while/for..in(..step)/foreach/switch, замыкания,
try/catch/throw, async/await/await_all, import/export (модули),
массивы (отриц. индексы), объекты, интерполяция строк \(expr),
составные присваивания += -= *= /= %%= и ++/--, инфиксные in/contains,
встроенные log/net/crypto/keyring/vault/fs/process/system/service/json/...
src/msx/cmd_msx.c
usage: script bin-token <text> [--decoy=N] [--pad=P]
  печатает \*DATA_INSERT*\...\*END*\ (сокрытие без контроля целостности)
  --decoy=N  дописать N байт-декоев в hex; \bin<len> вернёт только реальные
  --pad=P    раздуть счётчик \bin ведущими нулями (демонстрация буфера 128)
src/msx/cmd_msx.c
ndrndr

Анализ сетевого наблюдения, признаков и контекста потоков

Архитектура и API домена ↗

ndr

сетевое обнаружение: прогон захвата, находки, покрытие

ndr <status|replay|findings|counters|reset>
Регистрация: src/ndr/cmd_ndr.c · обработчик cmd_ndr

Развёрнутая встроенная справка

ndr <подкоманда>
  status                 состояние конвейера и привязка снимка IOC
  intel <файл>           привязать снимок IOC из текстового файла (ip|domain|ja3)
  replay <файл.pcap>     прогнать захват: счётчики, находки, дайджесты
  flows [--limit N]      потоки: протокол (разбор или DPI), наблюдаемость, причины
  protocols              протоколы по потокам: разбор / DPI, категория
  dpi [--list]           база сигнатур DPI: размер, самопроверка, перечень
  findings [--limit N]   находки последнего прогона с их покрытием
  finding <N> --json     одна находка каноническим JSON
  names [--limit N]      имена из истории: qname/host/sni/ja3, адреса почты, логины, DN, principal'ы
  hunt <ip|домен>        retrospective hunt по сохранённой истории
  coverage [--json]      что разбирается и что НЕТ; --json — обзор для Console
  history export|import <файл>  сохранить/загрузить историю (fail-closed)
  counters               счётчики наблюдения, включая потери захвата
  reset                  переинициализировать конвейер
  capture start|stop|stats  управление кольцевым захватом (TPACKET_V3)
  -f <файл>              последовательность команд в ОДНОМ процессе
src/ndr/ndr_cli.c

Аргументы и дополнительные формы вызова

  • ndr replay <файл.pcap> [--limit N]src/ndr/ndr_cli.c
  • ndr intel <файл> (строки: ip A.B.C.D [conf] | domain имя [conf] | ja3 md5 [conf])src/ndr/ndr_cli.c
  • ndr hunt <ip|домен>src/ndr/ndr_cli.c
  • ndr finding <N> --jsonsrc/ndr/ndr_cli.c
  • ndr history export|import <файл>src/ndr/ndr_cli.c
  • ndr dpi --list — перечень по категориямsrc/ndr/ndr_cli_view.c
ndrcapcapture

Поставщик пакетного наблюдения для сетевого анализа

Архитектура и API домена ↗

capture

захват трафика TPACKET_V3: start, stop, stats с потерями ядра

capture <start|stop|stats>
Регистрация: src/ndrcap/cmd_ndrcap.c · обработчик cmd_capture

Развёрнутая встроенная справка

ndr capture <подкоманда> [параметры]

  start [--iface eth0] [--filter 'BPF']   начать захват
        [--out FILE]   [--ring-kb 4096]
  stop                                     остановить, показать итог
  stats                                    текущая статистика

Потери ядра (tp_drops) всегда показываются явно.
drop > 0 → захват НЕПОЛНЫЙ — это не «ок», а факт.
src/ndrcap/ndrcap_cli.c
netотдельный интерфейс / библиотека

Вспомогательная политика повторных обращений к endpoint

Архитектура и API домена ↗

Самостоятельная CLI-команда и отдельная usage-справка не обнаружены. Управление находится у потребителей API — ra2c, transports. Контракты и границы приведены в досье домена.

netlinknetlink

События ядра, приём netlink и управляемые триггеры

Архитектура и API домена ↗

netlink

Универсальный событийный канал ядро→userspace: eBPF/RTNETLINK события, триггеры RA2C

netlink <status|subscribe|unsubscribe|stats|inject|bpf-map|trigger|help>
Регистрация: src/netlink/cmd_netlink.c · обработчик (cmd_fn_t)c_netlink

Развёрнутая встроенная справка

netlink — универсальный событийный канал ядро→userspace (PLATX)

  netlink status                         состояние приёмника
  netlink subscribe <TYPE> [...]         подписаться (debug)
  netlink unsubscribe <handle>           отписаться
  netlink stats [--reset]                статистика
  netlink inject --type=TYPE             инжектировать событие
                 [--src=IP] [--marker=0xHEX]
  netlink bpf-map <fd>                   подключить BPF perf-карту
  netlink trigger add <file.yaml>        загрузить триггер
  netlink trigger list                   список триггеров
  netlink trigger remove <id>            удалить триггер
  netlink trigger fire <id>              принудительно активировать
  netlink trigger enable <id> on|off     вкл/выкл триггер
  netlink help

Типы событий: ICMP_ECHO TCP_SYN DNS_QUERY ARP_REQUEST
              SYSCALL_OPEN IFACE_UP IFACE_DOWN SECURITY ALL

Формат файла триггера (YAML-подобный):
  trigger:
    name: my-trigger
    event_type: ICMP_ECHO
    match:
      src_ip: 192.168.0.0/16
      marker: 52413243
    action:
      type: activate_ra2c
      target: 10.0.0.1
      port: 443
      transport: https_ebpf
src/netlink/cmd_netlink.c
netlink statistics:
  rx_total     = %llu
  rx_rtnetlink = %llu
  rx_perf_bpf  = %llu
  dispatched   = %llu
  dropped_ring = %llu
  dropped_filt = %llu
  errors       = %llu
  activations  = %llu
src/netlink/cmd_netlink.c

Аргументы и дополнительные формы вызова

  • usage: netlink subscribe <type> [...] Types: ICMP_ECHO TCP_SYN DNS_QUERY ARP_REQUEST SYSCALL_OPEN IFACE_UP IFACE_DOWN SECURITY ALLsrc/netlink/cmd_netlink.c
  • usage: netlink unsubscribe <handle>src/netlink/cmd_netlink.c
  • usage: netlink inject --type=TYPE [--src=IP] [--marker=0xHEX] Types: ICMP_ECHO TCP_SYN DNS_QUERY ARP_REQUEST SYSCALL_OPEN IFACE_UP IFACE_DOWN SECURITYsrc/netlink/cmd_netlink.c
  • usage: netlink bpf-map <fd|-1>src/netlink/cmd_netlink.c
  • usage: netlink trigger add <file.yaml>src/netlink/cmd_netlink.c
  • usage: netlink trigger remove <id>src/netlink/cmd_netlink.c
  • usage: netlink trigger fire <id>src/netlink/cmd_netlink.c
  • usage: netlink trigger enable <id> [on|off]src/netlink/cmd_netlink.c
  • usage: netlink trigger <add|list|remove|fire|enable>src/netlink/cmd_netlink.c
observeobserve

Реестр источников и нормализация наблюдений

Архитектура и API домена ↗

observe

плоскость наблюдения: состояние источников, gaps, coverage

observe <status|claims|coverage>
Регистрация: src/observe/cmd_observe.c · обработчик cmd_observe

Развёрнутая встроенная справка

observe <подкоманда>

  status    состояние всех источников наблюдения
  claims    детализация: OBSERVED/INFERRED/GAP по каждому источнику
  coverage  агрегат: сколько событий OBSERVED, сколько потеряно

OBSERVED ≠ INFERRED.  GAP > 0 = отсутствие наблюдения, не 'ок'.
kernel_lost > 0 → источник DEGRADED: часть событий потеряна в ядре.
src/observe/observe_cli.c
pluginplugin

Упаковка PLUG, проверка доверия, загрузка и жизнь расширения

Архитектура и API домена ↗

plugin

hybrid plugin manager (external+internal, auto-builder, hot-replace)

plugin <list|load|unload|info|update|build|archive|...>
Регистрация: src/plugin/cmd_plugin.c · обработчик cmd_plugin_ns

Развёрнутая встроенная справка

plugin — hybrid plugin system (external dlopen + internal ELF loader)

  plugin list                        list all loaded plugins
  plugin info    <name>              detailed plugin info
  plugin load    <path> [type]       load from file (auto/external/internal)
  plugin unload  <name>              unload a plugin
  plugin exec    <name> <input>      call plugin.execute()
  plugin update  <name> <@new-file>  hot-replace internal plugin
  plugin reload  [path.plug]         remount PLUG (no path = embedded)
  plugin archive pack <out> <files> [--sign=<seed>]  pack (опц. подпись)
  plugin archive load <path>         unpack + load PLUG archive
  plugin archive reload <path>       remount PLUG without restart
  plugin build   [conf] [--full|--incremental]  run auto-builder
  plugin sha256  <@file|hex>         compute SHA-256 of data

  Подпись авторства (Ed25519):
  plugin key gen <seedfile>          сгенерировать пару ключей
  plugin key pub <seedfile|hex>      показать открытый ключ
  plugin verify  <path.plug>         проверить целостность+подпись
  plugin trust   <add|remove|list|clear> [key]  реестр доверенных ключей
  plugin policy  [require-signed on|off] [require-trusted on|off]

  Self-Modifying Code (атомарная замена):
  plugin smc-hook      <name> --offset=<hex> --hook=<hex> [--abs64]
  plugin smc-patch     <name> --offset=<hex> --bytes=<XX,...>
  plugin smc-trampoline <name> --offset=<hex> --target=<hex> [--abs64]
  plugin smc-status           показать SMC-хуки
  plugin smc-remove   --id=<N> снять SMC-хук

  type:  auto | external | internal | dlopen
src/plugin/cmd_plugin.c

Аргументы и дополнительные формы вызова

  • usage: plugin info <name>src/plugin/cmd_plugin.c
  • usage: %ssrc/plugin/cmd_plugin.c
  • usage: plugin load <path|@data> [auto|external|internal|dlopen]src/plugin/cmd_plugin.c
  • usage: plugin unload <name>src/plugin/cmd_plugin.c
  • usage: plugin exec <name> <input>src/plugin/cmd_plugin.c
  • usage: plugin update <name> <@new-image>src/plugin/cmd_plugin.c
  • usage: plugin archive pack <output.plug> <file1> [file2...] [--type=external|internal]src/plugin/cmd_plugin.c
  • usage: plugin archive load <path.plug>src/plugin/cmd_plugin.c
  • usage: plugin archive pack <output> <files...> plugin archive load <path> plugin archive reload <path>src/plugin/cmd_plugin.c
  • usage: plugin archive reload <path.plug>src/plugin/cmd_plugin.c
  • usage: plugin sha256 <@file|hex>src/plugin/cmd_plugin.c
  • usage: plugin key gen <seedfile> сгенерировать пару (seed→файл, pubkey→вывод) plugin key pub <seedfile|hex> показать открытый ключ для зернаsrc/plugin/cmd_plugin.c
  • usage: plugin key gen <seedfile>src/plugin/cmd_plugin.c
  • usage: plugin key pub <seedfile|hex>src/plugin/cmd_plugin.c
  • usage: plugin verify <path.plug>src/plugin/cmd_plugin.c
  • usage: plugin trust add <hex|@pubfile> добавить доверенный ключ plugin trust remove <hex|@pubfile> plugin trust list plugin trust clearsrc/plugin/cmd_plugin.c
  • usage: plugin trust %s <hex|@pubfile>src/plugin/cmd_plugin.c
poepoe

Преобразование представления нагрузок и runtime-мосты

Архитектура и API домена ↗

poe

POEngine: CFG/VM/DATA + AES-128 (CODE/ANTI/RUNTIME/AI — noop)

poe <obfuscate|deobfuscate|info|test|config|status|help>
Регистрация: src/poe/poe_cli.c · обработчик (cmd_fn_t)poe_dispatch

Развёрнутая встроенная справка

использование: poe <команда> [аргументы]

  obfuscate   <file> [--level=] [--seed=] [--key-id=] [--out=]

  config      [--level=<mask>] [--seed=<hex>] [--key-id=<n>]

реальные уровни (--level=): cfg (CFG flatten), vm (bytecode VM),

маска (принимается, без трансформа): code anti runtime ai — noop

имена: cfg+vm+data+crypto | all | 0xNN  (all включает noop-биты)
src/poe/poe_cli.c
proxyproxy · ipcproxy

Адаптеры IPC и сетевого посредничества

Архитектура и API домена ↗

proxy

IPC Proxy: router/bridge IPCP (UNIX/TCP/MQ/SHM)

proxy <start|add-*|route|send|...>
Регистрация: src/proxy/cmd_proxy.c · обработчик cmd_proxy_dispatch

ipcproxy

алиас команды proxy

ipcproxy — alias для proxy
Регистрация: src/proxy/cmd_proxy.c · обработчик cmd_proxy_dispatch

Развёрнутая встроенная справка

proxy (alias ipcproxy) — IPC Proxy: router/bridge IPCP-сообщений

  proxy start [--unix=PATH] [--tcp=HOST:PORT] | stop | status | adapters
  proxy add-unix --name=N --path=P [--server] [--auth=T]
  proxy add-tcp  --name=N --host=H --port=P [--server] [--auth=T]
  proxy add-msgq --name=N [--max-msgs=K]
  proxy add-shm  --name=N [--size=B]
  proxy route add --src=S --dst=D --adapter=A [--filter=F] | route list
  proxy forward add --listen=H:P --target=H:P [--name=N]
  proxy forward list | sess | remove <name>
  proxy socks add --listen=H:P [--name=N] [--auth] | list | remove <name>
  proxy http  add --listen=H:P [--name=N] [--auth] | list | remove <name>
  proxy tls   gen-ca --cert=F --key=F [--cn=..] [--days=N]
  proxy tls   add --listen=H:P --target=H:P [--name=N] | list | remove <name>
  proxy upstream add --match=<host|*.suffix|*> --to=host[:port] [--name=N] | list | remove <name>
  proxy acl add --allow|--deny --cidr=A.B.C.D[/len] [--name=N] | list | remove <name> | default allow|deny
  proxy auth add --user=U --pass=P | list | remove <user>   (для socks/http --auth)
  proxy config [show] | set [--max-sessions=N] [--max-per-ip=N] [--idle-ms=N] [--connect-ms=N] [--ca-cert=..] [--ca-key=..] [--capture=FILE|--no-capture]
  proxy capture start [--file=FILE] [--no-flow] | stop | status
  proxy flow [list] | stats
  proxy send --src=S --dst=D --payload=... [--type=request|response|event] [--auth=T]
  proxy recv
  proxy filter add --name=N --type=audit|drop|encrypt|decrypt|security [--key=K]
  proxy key --pass=... | --keyring=<desc>       (интеграция с KeyringDaemon)
  proxy cipher chacha20|aes256gcm

Спец. назначение @echo — loopback в очередь приёма (для тестов).
src/proxy/cmd_proxy.c
proxy config:
  max_sessions:       %d
  max_conns_per_ip:   %d%s
  idle_timeout_ms:    %d
  connect_timeout_ms: %d
  tls_ca_cert:        %s
  tls_ca_key:         %s
  capture:            %s%s%s
runtime:
  reactor:            %s
  sessions:           %d / %d
  accept_rejected:    %llu  (нехватка fd, EMFILE)
  acl:                default %s, denied %llu
  per_ip_rejected:    %llu
  dns:                hit %llu, neg %llu, miss %llu, qfull %llu, queue %d
  auth:               ok %llu, fail %llu
src/proxy/cmd_proxy.c

Аргументы и дополнительные формы вызова

  • usage: proxy add-unix --name=N --path=P [--server] [--auth=T]src/proxy/cmd_proxy.c
  • usage: proxy add-tcp --name=N --host=H --port=P [--server] [--auth=T]src/proxy/cmd_proxy.c
  • usage: proxy add-msgq --name=N [--max-msgs=K]src/proxy/cmd_proxy.c
  • usage: proxy add-shm --name=N [--size=B]src/proxy/cmd_proxy.c
  • usage: proxy route add --src=S --dst=D --adapter=A [--filter=F]src/proxy/cmd_proxy.c
  • usage: proxy send --src=S --dst=D --payload=... [--type=request|response|event] [--auth=T]src/proxy/cmd_proxy.c
  • usage: proxy filter add --name=N --type=audit|drop|encrypt|decrypt|security [--key=K]src/proxy/cmd_proxy.c
  • usage: proxy key --pass=... | --keyring=<desc>src/proxy/cmd_proxy.c
  • usage: proxy forward add --listen=H:P --target=H:P [--name=N]src/proxy/cmd_proxy.c
  • usage: proxy forward remove <name>src/proxy/cmd_proxy.c
  • usage: proxy socks add --listen=H:P [--name=N] [--auth]src/proxy/cmd_proxy.c
  • usage: proxy socks remove <name>src/proxy/cmd_proxy.c
  • usage: proxy http add --listen=H:P [--name=N] [--auth]src/proxy/cmd_proxy.c
  • usage: proxy http remove <name>src/proxy/cmd_proxy.c
  • usage: proxy tls gen-ca --cert=F --key=F [--cn=..] [--days=N]src/proxy/cmd_proxy.c
  • usage: proxy tls add --listen=H:P --target=H:P [--name=N]src/proxy/cmd_proxy.c
  • usage: proxy tls remove <name>src/proxy/cmd_proxy.c
  • usage: proxy upstream add --match=<host|*.suffix|*> --to=host[:port] [--name=N]src/proxy/cmd_proxy.c
  • usage: proxy upstream remove <name>src/proxy/cmd_proxy.c
  • usage: proxy acl add --allow|--deny --cidr=A.B.C.D[/len] [--name=N]src/proxy/cmd_proxy.c
  • usage: proxy acl remove <name>src/proxy/cmd_proxy.c
  • usage: proxy acl default allow|denysrc/proxy/cmd_proxy.c
  • usage: proxy auth add --user=U --pass=Psrc/proxy/cmd_proxy.c
  • usage: proxy auth remove <user>src/proxy/cmd_proxy.c
pxapiотдельный интерфейс / библиотека

Совместимость публичного API и проекция записей платформы

Архитектура и API домена ↗

Самостоятельная CLI-команда и отдельная usage-справка не обнаружены. Управление находится у потребителей API — core, fabric. Контракты и границы приведены в досье домена.

pxsigpxsig

Происхождение, доверие и поиск сигнатурных признаков

Архитектура и API домена ↗

pxsig

управление базами обнаружения (PXSIG)

pxsig <status|verify|ingest|trust|catalog|overlay|snapshot|lookup|receipt|publish-status>
Регистрация: src/pxsig/cmd_pxsig.c · обработчик cmd_pxsig

Развёрнутая встроенная справка

pxsig <подкоманда>
  status                        состояние узла, каталога и снимков
  verify <файл>                 разбор и проверка происхождения
  ingest <файл>                 приём: проверка → каталог → снимок
  trust list|add|threshold      корни доверия (отдельное полномочие)
  catalog [--limit N]           содержимое каталога и происхождение
  overlay list|disable|allowlist   локальная политика владельца
  snapshot [ndr|av]             активный снимок потребителя
  lookup domain|ip|sha256 <v>   поиск по активному снимку
  receipt                       канонические receipt активаций
  publish-status                отдать статус в fabric-доставку
общие: --now <мкс>  --tenant <t>  --source local|central|mesh|operator
время НЕ берётся из часов: без --now оно считается недостоверным.
src/pxsig/sig_cli.c

Аргументы и дополнительные формы вызова

  • pxsig verify <файл> [--now мкс] [--tenant t]src/pxsig/sig_cli.c
  • pxsig ingest <файл> [--now мкс] [--tenant t] [--source local|central|mesh|operator]src/pxsig/sig_cli.c
  • pxsig trust add <key_id> <sign|ingest|trust-admin> <pubkey-64hex>src/pxsig/sig_cli.c
  • pxsig trust threshold <release|canary|lab> <n>src/pxsig/sig_cli.c
  • pxsig overlay %s <publisher> <namespace> <id> [--pin-revision <rev>]src/pxsig/sig_cli.c
  • pxsig lookup {domain <имя>|ip <a.b.c.d>|sha256 <hex>}src/pxsig/sig_cli.c
ra2cra2c-single · ra2c-ebpf-single · ra2c · ra2c-ebpf

Аутентифицированные сессии и управление связью между узлами

Архитектура и API домена ↗

ra2c-single

ra2c single — сырые примитивы (connect/listen/send/recv). Не сессия. Сессия (HELLO/AEAD) — команда `ra2c`, не `ra2c single`. --batch завершает процесс после команды: listen не остаётся живым. Канал жив, пока жив процесс (REPL). --- канал --- ra2c single connect <backend> <host> [port] [timeout_ms] Сырой сокет, без HELLO/AEAD. timeout по умолчанию 5000. Backends: tcp udp udp_bc tcp_bc http ws dns icmp coap mqtt ntp dns_raw doh dot arp icmp_ts tcp_opt udp_ntp vlan quic https no-hs (нет HELLO/сессии): arp vlan udp_ntp | quic https (fake) | tcp_opt icmp_ts → SKIP no-hs:simplex|fake|narrow — сырой send/recv только, не сессия udp_bc — UDP backconnect. tcp_bc — алиас udp_bc. Не TCP backconnect. ra2c single listen <backend> <host> [port] [timeout_ms] Один сырой канал в этом процессе. --batch его не держит. stream (tcp/http/ws/mqtt) — bind+accept одного клиента; datagram (udp/udp_bc/tcp_bc/dns/ntp/coap) — bind и первый пир; covert (icmp/dns_raw/doh/dot/arp/icmp_ts/tcp_opt/udp_ntp/vlan/quic/https). host=* или 0.0.0.0 — все интерфейсы. timeout_ms<=0 — ждать бесконечно. Алиас: ra2c single server … ra2c single connect-manifest <manifest-file-or-inline> ra2c single disconnect ra2c single status --- сырая передача --- ra2c single send <text> Весь текст. Короткий write — ошибка, не «sent N». covert max_send (vlan=2, arp=4, ntp=24, …) — больше нельзя. ra2c single recv [timeout_ms] --- DSL --- ra2c single transport list ra2c single transport build "backend=http host=1.2.3.4 port=8080 fallback=dns" ra2c single transport load <manifest.conf> --- настройка бэкендов --- ra2c single dns-domain <domain> ra2c single dns-resolver <ip> ra2c single mqtt-session <id> MQTT ClientId / topic. Не RA2C-сессия.

ra2c-single [sub...]
Регистрация: src/ra2c/ra2c_backend.c · обработчик cmd_ra2c_backend_dispatch

ra2c-ebpf-single

ra2c-ebpf-single — сырые примитивы (connect/listen/send/recv). Не сессия. Сессия (HELLO/AEAD) — команда `ra2c-ebpf`, не `ra2c-ebpf-single`. --batch --command выполняет одну строку и выходит: listen/connect умирают вместе с процессом. Держать канал — только REPL. --- канал --- ra2c-ebpf-single list Транспорты и статус BPF. tcp_bc — TCP backconnect, не UDP backconnect. ra2c-ebpf-single connect <transport> <target> [port] Сырой канал, без HELLO/AEAD. Transports: arp icmp_ts tcp_opt udp_ntp vlan quic tcp_bc https dns doh tcp udp http ws mqtt coap ntp icmp dns_raw dot no-hs (сессия невозможна): arp vlan udp_ntp | quic https (fake) | tcp_opt icmp_ts tcp_bc здесь — TCP backconnect. Не UDP backconnect (это raw tcp_bc/udp_bc). L2 (arp,vlan): target = eth0 или eth0@10.0.0.1 ra2c-ebpf-single listen <transport> <target> [port] Один сырой канал в этом процессе. --batch его не держит. Явный серверный режим (префикс server: добавляется сам). Алиас: ra2c-ebpf-single server … ra2c-ebpf-single disconnect ra2c-ebpf-single status --- сырая передача --- ra2c-ebpf-single send <hex> Весь буфер. Короткий write — ошибка, не «sent N». ra2c-ebpf-single recv [timeout_ms]

ra2c-ebpf-single [sub...]
Регистрация: src/ra2c/ra2c_backend_ebpf.c · обработчик cmd_ra2c_ebpf_single

ra2c

ra2c [sub...]
Регистрация: src/ra2c/ra2c_session.c · обработчик cmd_ra2c

ra2c-ebpf

ra2c-ebpf — сессия в этом процессе (REPL). Не ./memfd ra2c-ebpf … Сессия одна на процесс. connect и cmd — в одном REPL. --batch --command выполняет одну строку и выходит: listen/connect умирают вместе с процессом. Держать сервер — только REPL. --- сессия --- ra2c-ebpf connect <transport> <host> [port] [--psk=hex|raw:…] [--pin=hex64] [--identity=path] Session: tcp udp http ws mqtt coap ntp icmp dns doh dns_raw tcp_bc no-hs (не HELLO): arp vlan udp_ntp | quic https (fake) | tcp_opt icmp_ts → `ra2c-ebpf single` / SKIP no-hs:simplex|fake|narrow tcp_bc здесь — TCP backconnect. Не UDP backconnect (это raw tcp_bc/udp_bc). Wire = ra2c (X25519 + RA2C-v1). Legacy unauth ECDH crosses with `ra2c listen/connect`. Authenticated mode needs matching PSK/pin/identity on both sides (same as ordinary `ra2c auth`). ra2c-ebpf listen <transport> <host> [port] [--psk=…] [--pin=…] [--identity=…] Сервер: covert listen (server:) + HELLO_ACK. Пока жив процесс. Алиас: ra2c-ebpf server … ra2c-ebpf auth psk <hex|raw:…> | pin <hex64> | identity <path> | clear Peer auth before handshake. Match → peer_auth; mismatch aborts (no keyed). ra2c-ebpf connect-multi <t1,t2,...> <host> [port] [--psk=…] [--pin=…] [--identity=…] ra2c-ebpf disconnect ra2c-ebpf switch <transport> <host> [port] Сменить канал на лету (ключ сохраняется). Не вышло — старый жив. ra2c-ebpf rekey Новый X25519 на той же сессии. Не вышло — старый ключ жив. ra2c-ebpf status ra2c-ebpf channel-mode <roundrobin|bytype|failover> --- remote command --- ra2c-ebpf cmd <platform-command> ra2c-ebpf console start | stop --- file transfer --- ra2c-ebpf file upload <local> [remote] ra2c-ebpf file get <remote> [local] --- task / cron --- ra2c-ebpf task submit <cmd> [--delay=SEC] [--every=SEC] ra2c-ebpf task list ra2c-ebpf task cancel <id> --- heartbeat --- ra2c-ebpf heartbeat --- primitive transport (no session layer) --- ra2c-ebpf single ... (see: ra2c-ebpf-single help) ra2c-ebpf help

ra2c-ebpf [sub...]
Регистрация: src/ra2c/ra2c_session_ebpf.c · обработчик cmd_ra2c_ebpf

Развёрнутая встроенная справка

ra2c single — сырые примитивы (connect/listen/send/recv). Не сессия.
Сессия (HELLO/AEAD) — команда `ra2c`, не `ra2c single`.

  --batch завершает процесс после команды: listen не остаётся живым.
  Канал жив, пока жив процесс (REPL).

  --- канал ---
  ra2c single connect <backend> <host> [port] [timeout_ms]
      Сырой сокет, без HELLO/AEAD. timeout по умолчанию 5000.
      Backends: tcp udp udp_bc tcp_bc http ws dns icmp coap mqtt ntp
               dns_raw doh dot arp icmp_ts tcp_opt udp_ntp vlan quic https
      no-hs (нет HELLO/сессии): arp vlan udp_ntp | quic https (fake) | tcp_opt icmp_ts
          → SKIP no-hs:simplex|fake|narrow — сырой send/recv только, не сессия
      udp_bc — UDP backconnect. tcp_bc — алиас udp_bc. Не TCP backconnect.
  ra2c single listen  <backend> <host> [port] [timeout_ms]
      Один сырой канал в этом процессе. --batch его не держит.
      stream (tcp/http/ws/mqtt) — bind+accept одного клиента;
      datagram (udp/udp_bc/tcp_bc/dns/ntp/coap) — bind и первый пир;
      covert (icmp/dns_raw/doh/dot/arp/icmp_ts/tcp_opt/udp_ntp/vlan/quic/https).
      host=* или 0.0.0.0 — все интерфейсы. timeout_ms<=0 — ждать бесконечно.
      Алиас: ra2c single server …
  ra2c single connect-manifest <manifest-file-or-inline>
  ra2c single disconnect
  ra2c single status

  --- сырая передача ---
  ra2c single send <text>
      Весь текст. Короткий write — ошибка, не «sent N».
      covert max_send (vlan=2, arp=4, ntp=24, …) — больше нельзя.
  ra2c single recv [timeout_ms]

  --- DSL ---
  ra2c single transport list
  ra2c single transport build "backend=http host=1.2.3.4 port=8080 fallback=dns"
  ra2c single transport load  <manifest.conf>

  --- настройка бэкендов ---
  ra2c single dns-domain   <domain>
  ra2c single dns-resolver <ip>
  ra2c single mqtt-session <id>
      MQTT ClientId / topic. Не RA2C-сессия.
src/ra2c/ra2c_backend.c
ra2c-ebpf-single — сырые примитивы (connect/listen/send/recv). Не сессия.
Сессия (HELLO/AEAD) — команда `ra2c-ebpf`, не `ra2c-ebpf-single`.

  --batch --command выполняет одну строку и выходит: listen/connect
  умирают вместе с процессом. Держать канал — только REPL.

  --- канал ---
  ra2c-ebpf-single list
      Транспорты и статус BPF. tcp_bc — TCP backconnect, не UDP backconnect.
  ra2c-ebpf-single connect <transport> <target> [port]
      Сырой канал, без HELLO/AEAD.
      Transports: arp icmp_ts tcp_opt udp_ntp vlan quic tcp_bc https dns doh
                  tcp udp http ws mqtt coap ntp icmp dns_raw dot
      no-hs (сессия невозможна): arp vlan udp_ntp | quic https (fake) | tcp_opt icmp_ts
      tcp_bc здесь — TCP backconnect. Не UDP backconnect (это raw tcp_bc/udp_bc).
      L2 (arp,vlan): target = eth0  или  eth0@10.0.0.1
  ra2c-ebpf-single listen <transport> <target> [port]
      Один сырой канал в этом процессе. --batch его не держит.
      Явный серверный режим (префикс server: добавляется сам).
      Алиас: ra2c-ebpf-single server …
  ra2c-ebpf-single disconnect
  ra2c-ebpf-single status

  --- сырая передача ---
  ra2c-ebpf-single send <hex>
      Весь буфер. Короткий write — ошибка, не «sent N».
  ra2c-ebpf-single recv [timeout_ms]
src/ra2c/ra2c_backend_ebpf.c
ra2c — session in this process (REPL). Not ./memfd ra2c …

  Сессия одна на процесс. connect и cmd — в одном REPL.
  --batch --command выполняет одну строку и выходит: listen/connect
  умирают вместе с процессом. Держать сервер — только REPL.

  --- сессия ---
  ra2c connect <backend> <host> [port] [--psk=hex|raw:…] [--pin=hex64] [--identity=path] [--insecure-ecdh]
      Backends: tcp udp udp_bc tcp_bc http ws dns icmp coap mqtt ntp
               dns_raw doh arp icmp_ts tcp_opt udp_ntp vlan quic https
      no-hs (не сессия): arp vlan udp_ntp quic https tcp_opt icmp_ts — только `ra2c single`.
          → SKIP no-hs:simplex|fake|narrow
      udp_bc — UDP backconnect. tcp_bc — алиас udp_bc. Не TCP backconnect.
      Протокол общий с ra2c-ebpf (X25519 + RA2C-v1 кадр).
      Без PSK/pin/identity handshake отказывает. Legacy ECDH — только
      лабораторный opt-in: --insecure-ecdh (или RA2C_INSECURE_ECDH=1 в фикстуре).
      С PSK/pin/identity оба конца задают совпадающий материал до HS.
  ra2c listen <backend> <host> [port] [timeout_ms] [--psk=…] [--pin=…] [--identity=…] [--insecure-ecdh]
      Сервер: bind/accept + HELLO_ACK, затем console. Пока жив процесс.
      host=* или 0.0.0.0 — все интерфейсы. Алиас: ra2c server …
  ra2c auth psk <hex|raw:…> | pin <hex64> | identity <path> | clear
      Peer auth before handshake. Match → peer_auth; mismatch aborts (no keyed).
  ra2c connect-manifest <manifest-file-or-inline>
      Manifest: backend=tcp host=127.0.0.1 port=4444
  ra2c disconnect
  ra2c switch <backend> <host> [port]
      Сменить канал на лету (ключ и session_id сохраняются).
      Пир должен принимать кадры (console start / listen).
  ra2c rekey
      Новый X25519 на той же сессии. Не вышло — старый ключ жив.
  ra2c status

  --- remote command ---
  ra2c cmd <platform-command>
  ra2c send <text> | recv [ms]
      AEAD после handshake. Нужен rx (connect/listen его поднимают).
  ra2c console start | stop

  --- file transfer ---
  ra2c file upload <local-path> [remote-name]
  ra2c file get    <remote-name> [local-path]

  --- task (локальный popen, не по проводу) ---
  ra2c task submit <shell-cmd> [--delay=SEC] [--every=SEC]
  ra2c task list
  ra2c task cancel <id>

  --- heartbeat ---
  ra2c heartbeat
src/ra2c/ra2c_session.c
usage:
  ra2c mod want <cap> [--version-min=N] [--version-max=N] [--ttl=SEC] [--dry-run]
  ra2c mod load <archive-hex>
  ra2c mod unload <artifact-hex>
  ra2c mod status [<artifact-hex>]
  ra2c mod burn <artifact-hex>
  ra2c mod central list
  ra2c mod central register <archive-hex> <name> <ver> <trust>
  ra2c mod central remove <artifact-hex>
  ra2c mod central send <artifact-hex>
  ra2c mod store has <artifact-id-hex>
  ra2c mod receipt [--seed=hex32] [--artifact=hex32]
                   [--health=ok|degraded|failed] [--cap-gen=N] [--started=TS] [--expires=TS]
  ra2c mod lease list
src/ra2c/ra2c_session.c
mod lease: lease table is owned by the Module Host worker.
  To inspect active bindings, send a remote command:
    ra2c cmd modhost lease list
  Or call modhost_call_tick() from within the host process.
src/ra2c/ra2c_session.c
usage:
  ra2c mod central list
  ra2c mod central register <archive-hex> <name> <version> <trust-class>
    trust-class: 0=unknown 1=experimental 2=verified 3=security
  ra2c mod central remove <artifact-hex>
  ra2c mod central send   <artifact-hex>
  ra2c mod central offer  (broadcast all offers via mesh)
src/ra2c/ra2c_session.c
ra2c session:
  backend  : %s
  remote   : %s:%u
  session  : %.*s
  keyed    : %s
  peer_auth: %s
  key_gen  : %u
  seq_tx   : %u
  seq_rx   : %u
  bytes_tx : %llu
  bytes_rx : %llu
  role     : %s
  last_alive : %llus ago
  console  : %s
  tasks    : %d
src/ra2c/ra2c_session.c
ra2c-ebpf — сессия в этом процессе (REPL). Не ./memfd ra2c-ebpf …

  Сессия одна на процесс. connect и cmd — в одном REPL.
  --batch --command выполняет одну строку и выходит: listen/connect
  умирают вместе с процессом. Держать сервер — только REPL.

  --- сессия ---
  ra2c-ebpf connect <transport> <host> [port] [--psk=hex|raw:…] [--pin=hex64] [--identity=path]
      Session: tcp udp http ws mqtt coap ntp icmp dns doh dns_raw tcp_bc
      no-hs (не HELLO): arp vlan udp_ntp | quic https (fake) | tcp_opt icmp_ts
          → `ra2c-ebpf single` / SKIP no-hs:simplex|fake|narrow
      tcp_bc здесь — TCP backconnect. Не UDP backconnect (это raw tcp_bc/udp_bc).
      Wire = ra2c (X25519 + RA2C-v1). Legacy unauth ECDH crosses with
      `ra2c listen/connect`. Authenticated mode needs matching PSK/pin/identity
      on both sides (same as ordinary `ra2c auth`).
  ra2c-ebpf listen <transport> <host> [port] [--psk=…] [--pin=…] [--identity=…]
      Сервер: covert listen (server:) + HELLO_ACK. Пока жив процесс.
      Алиас: ra2c-ebpf server …
  ra2c-ebpf auth psk <hex|raw:…> | pin <hex64> | identity <path> | clear
      Peer auth before handshake. Match → peer_auth; mismatch aborts (no keyed).
  ra2c-ebpf connect-multi <t1,t2,...> <host> [port] [--psk=…] [--pin=…] [--identity=…]
  ra2c-ebpf disconnect
  ra2c-ebpf switch <transport> <host> [port]
      Сменить канал на лету (ключ сохраняется). Не вышло — старый жив.
  ra2c-ebpf rekey
      Новый X25519 на той же сессии. Не вышло — старый ключ жив.
  ra2c-ebpf status
  ra2c-ebpf channel-mode <roundrobin|bytype|failover>

  --- remote command ---
  ra2c-ebpf cmd <platform-command>
  ra2c-ebpf console start | stop

  --- file transfer ---
  ra2c-ebpf file upload <local> [remote]
  ra2c-ebpf file get   <remote> [local]

  --- task / cron ---
  ra2c-ebpf task submit <cmd> [--delay=SEC] [--every=SEC]
  ra2c-ebpf task list
  ra2c-ebpf task cancel <id>

  --- heartbeat ---
  ra2c-ebpf heartbeat

  --- primitive transport (no session layer) ---
  ra2c-ebpf single ...   (see: ra2c-ebpf-single help)

  ra2c-ebpf help
src/ra2c/ra2c_session_ebpf.c

Аргументы и дополнительные формы вызова

  • usage: ra2c single connect <backend> <host> [port] [ms]src/ra2c/ra2c_backend.c
  • usage: ra2c single listen <backend> <host> [port] [timeout_ms]src/ra2c/ra2c_backend.c
  • usage: ra2c single connect-manifest <file|inline>src/ra2c/ra2c_backend.c
  • usage: ra2c single send <text>src/ra2c/ra2c_backend.c
  • usage: ra2c single transport list|build|load ...src/ra2c/ra2c_backend.c
  • usage: ra2c single transport %s <%s>src/ra2c/ra2c_backend.c
  • usage: ra2c single dns-domain <domain>src/ra2c/ra2c_backend.c
  • usage: ra2c single dns-resolver <ip>src/ra2c/ra2c_backend.c
  • usage: ra2c single mqtt-session <id>src/ra2c/ra2c_backend.c
  • usage: ra2c-ebpf-single connect <transport> <target> [port]src/ra2c/ra2c_backend_ebpf.c
  • usage: ra2c-ebpf-single listen <transport> <target> [port]src/ra2c/ra2c_backend_ebpf.c
  • usage: ra2c-ebpf-single send <hex>src/ra2c/ra2c_backend_ebpf.c
  • ..." | load <file> ra2c single send <text> | recv [ms] ra2c single dns-domain <d> | dns-resolver <ip> | mqtt-session <id>src/ra2c/ra2c_session.c
  • usage: ra2c mod want <cap-name> [--version-min=N] [--version-max=N] [--ttl=SEC] [--dry-run]src/ra2c/ra2c_session.c
  • usage: ra2c mod store has <artifact-id-hex>src/ra2c/ra2c_session.c
  • usage: ra2c mod load <archive-hex>src/ra2c/ra2c_session.c
  • usage: ra2c mod unload <artifact-hex>src/ra2c/ra2c_session.c
  • usage: ra2c mod burn <artifact-hex>src/ra2c/ra2c_session.c
  • usage: ra2c mod central register <archive-hex> <name> <version> <trust-class>src/ra2c/ra2c_session.c
  • usage: ra2c mod central remove <artifact-hex (64 hex chars)>src/ra2c/ra2c_session.c
  • usage: ra2c mod central send <artifact-hex (64 hex chars)>src/ra2c/ra2c_session.c
  • usage: ra2c connect <backend> <host> [port] [--psk=…] [--pin=…] [--identity=…]src/ra2c/ra2c_session.c
  • usage: ra2c auth psk <hex|raw:…> | pin <hex64> | identity <path> | clearsrc/ra2c/ra2c_session.c
  • usage: ra2c listen <backend> <host> [port] [timeout_ms] [--psk=…] [--pin=…] [--identity=…]src/ra2c/ra2c_session.c
  • usage: ra2c connect-manifest <file|inline>src/ra2c/ra2c_session.c
  • usage: ra2c switch <backend> <host> [port]src/ra2c/ra2c_session.c
  • usage: ra2c send <text>src/ra2c/ra2c_session.c
  • usage: ra2c cmd <platform-command>src/ra2c/ra2c_session.c
  • usage: ra2c console start|stopsrc/ra2c/ra2c_session.c
  • usage: ra2c file upload <local> [remote] | get <remote> [local]src/ra2c/ra2c_session.c
  • usage: ra2c task submit <cmd> | list | cancel <id>src/ra2c/ra2c_session.c
  • usage: ra2c-ebpf connect <transport> <host> [port] [--psk=…] [--pin=…] [--identity=…]src/ra2c/ra2c_session_ebpf.c
  • usage: ra2c-ebpf auth psk <hex|raw:…> | pin <hex64> | identity <path> | clearsrc/ra2c/ra2c_session_ebpf.c
  • usage: ra2c-ebpf listen <transport> <host> [port] [--psk=…] [--pin=…] [--identity=…]src/ra2c/ra2c_session_ebpf.c
  • usage: ra2c-ebpf connect-multi <t1,t2,...> <host> [port] [--psk=…] [--pin=…] [--identity=…]src/ra2c/ra2c_session_ebpf.c
  • usage: ra2c-ebpf switch <transport> <host> [port]src/ra2c/ra2c_session_ebpf.c
  • ra2c-ebpf session: channels : %d [mode: %s]src/ra2c/ra2c_session_ebpf.c
  • usage: ra2c-ebpf channel-mode <roundrobin|bytype|failover>src/ra2c/ra2c_session_ebpf.c
  • usage: ra2c-ebpf cmd <platform-command>src/ra2c/ra2c_session_ebpf.c
  • usage: ra2c-ebpf console start|stopsrc/ra2c/ra2c_session_ebpf.c
  • usage: ra2c-ebpf file upload <local> [remote] | get <remote> [local]src/ra2c/ra2c_session_ebpf.c
  • usage: ra2c-ebpf task submit|list|cancel ...src/ra2c/ra2c_session_ebpf.c
  • usage: ra2c-ebpf task submit <cmd> [--delay=SEC] [--every=SEC]src/ra2c/ra2c_session_ebpf.c
  • usage: ra2c-ebpf task cancel <id>src/ra2c/ra2c_session_ebpf.c
sandboxsandbox

Подтверждаемая изоляция исследовательских нагрузок

Архитектура и API домена ↗

sandbox

изолированное исполнение: границы, готовность, уборка

sandbox <probe|profile|ready|run|receipt|status>
Регистрация: src/sandbox/cmd_sandbox.c · обработчик cmd_sandbox

Развёрнутая встроенная справка

sandbox <подкоманда>
  probe                     какие механизмы изоляции хост даёт НА САМОМ ДЕЛЕ
  profile [--image P]       разобрать профиль: digest, обязательные механизмы, бюджет
  ready <abs-image>         доказательство готовности БЕЗ запуска нагрузки
  run <abs-image> [args]    выполнить и показать исход, containment, покрытие, уборку
  receipt [job-id]          канонический receipt задания
  status                    квоты и учёт координатора (за этот процесс)
  -f <файл>                 выполнить последовательность команд в ОДНОМ процессе
общие: --profile <id> --wall-ms <N> --max-procs <N> --tenant <t> --purpose analysis|task
пути: POSIX (/a/b) или DOS (C:\a\b, C:/a/b); набор команд и их вывод одинаковы на Linux и Windows
src/sandbox/sb_cli.c

Аргументы и дополнительные формы вызова

  • sandbox ready <абсолютный путь к образу> [--profile id]src/sandbox/sb_cli.c
  • sandbox run <абсолютный путь к образу> [аргументы...] [--profile id] [--wall-ms N] [--tenant t]src/sandbox/sb_cli.c
  • sandbox receipt [job-id] — заданий в этом процессе не былоsrc/sandbox/sb_cli.c
seccompseccomp

Построение и применение политики системных вызовов

Архитектура и API домена ↗

seccomp

process isolation via seccomp unotify — no ptrace required (Linux >= 5.9)

seccomp <run|intercept|status|stop|list|inject|uninject|stats|audit|policy|techniques>
Регистрация: src/seccomp/cmd_seccomp.c · обработчик cmd_seccomp

Аргументы и дополнительные формы вызова

  • seccomp policy load --file=src/seccomp/dbg_seccomp.c
  • seccomp policy validate --file=src/seccomp/dbg_seccomp.c
  • seccomp run --cmd= [--policy=yaml] [--output=log]src/seccomp/dbg_seccomp.c
  • seccomp intercept --cmd= --inject=<lib.so> [--policy=yaml]src/seccomp/dbg_seccomp.c
  • seccomp status --pid=src/seccomp/dbg_seccomp.c
  • seccomp stop --pid=src/seccomp/dbg_seccomp.c
  • seccomp inject --pid= --library=<path.so>src/seccomp/dbg_seccomp.c
  • seccomp uninject --pid=src/seccomp/dbg_seccomp.c
  • seccomp stats --pid=src/seccomp/dbg_seccomp.c
  • seccomp audit --pid= [--lines=N]src/seccomp/dbg_seccomp.c
  • seccomp policy <load|list>src/seccomp/dbg_seccomp.c
  • seccomp <команда>src/seccomp/dbg_seccomp.c
secretsотдельный интерфейс / библиотека

Политика и жизненный цикл секретного материала

Архитектура и API домена ↗

Развёрнутая встроенная справка

PXSECRETS CLI (development foundation)
secrets [status|help|capabilities] [--json]
secrets vault init --scope node|team|personal --protector os|passphrase [--prompt]
secrets vault unlock --vault ID --prompt
secrets vault lock --vault ID
secrets put --vault ID --kind blob|api-token|credential --prompt [--revision N]
secrets list --vault ID | secrets describe --object ID
secrets grant preview --object ID --consumer ID --target ID [--ttl 1..3600]
secrets grant apply --plan ID | secrets revoke --lease ID
secrets rotate preview --object ID --revision N | secrets rotate apply --plan ID
secrets backup create --vault ID --out FILE | secrets backup verify --file FILE
secrets restore preview --vault ID --file FILE | secrets restore apply --plan ID
passwords add --vault ID --prompt | passwords generate --vault ID [--length 16..128]
passwords reveal --object ID --secure-view | passwords import --vault ID --file FILE
certs list [--vault ID] [--expiring-within 1..3650d]
certs import --vault ID --file FILE --private-store [--prompt]
certs validate --object ID --target ID | certs csr --object ID --out FILE
certs deploy preview --object ID --consumer ID | certs deploy apply --plan ID
certs trust preview --object ID --target ID --revision N | certs trust apply --plan ID
IDs: 32 hex digits, nonzero. Values/passwords/keys are never accepted in argv.
This build parses the contract and reports readiness. Storage, grants,
reveal and trust effects remain unavailable until their providers are bound.
src/secrets/secrets_cli.c

Аргументы и дополнительные формы вызова

  • secrets statussrc/secrets/secrets_cli.c
  • secrets capabilitiessrc/secrets/secrets_cli.c
Это отдельный интерфейс домена. Его наличие в исходниках не означает регистрацию одноимённого корня в каждой консоли PLATX. Используйте executable или интеграцию, предусмотренные вашим профилем.
securitysecure

Общие механизмы усиления защиты процесса и политики

Архитектура и API домена ↗

secure

Process hardening: memory lock, core dump control, privilege containment

secure <status|harden|mlock|nodump|no_new_privs|wipe|help>
Регистрация: src/security/cmd_security.c · обработчик (cmd_fn_t)c_secure

Развёрнутая встроенная справка

secure — process hardening and memory protection

  secure status              show current security posture
  secure harden              apply all hardening measures
  secure mlock               lock memory pages into RAM
  secure nodump              disable core dumps
  secure no_new_privs        block setuid / capability gain
  secure wipe --addr=<hex> --size=<N>   zero a memory region
  secure help

Hardening sequence: nodump → no_new_privs → mlock
Audit: all operations logged to the audit subsystem.
src/security/cmd_security.c

Аргументы и дополнительные формы вызова

  • usage: secure wipe --addr=<hex_address> --size=<bytes> Secure-zeroes a memory region (for stack/heap sensitive data). Example: secure wipe --addr=0x7fff00001234 --size=64src/security/cmd_security.c
selfprotectboot · selfprotect

Самозащита платформы, PLATXBoot и проверяемые провайдеры

Архитектура и API домена ↗

boot

boot chain: EFI measurement, TPM PCR4, LKM whitelist, Secure Boot

boot <status|verify|pcr4|esp-scan|order|grub-verify|modsig|report|module|trust|install|uninstall>
Регистрация: src/selfprotect/cmd_boot.c · обработчик cmd_boot

selfprotect

самозащита платформы: щиты, политика, хуки, self-test

selfprotect <status|policy|assets|verify>
Регистрация: src/selfprotect/cmd_selfprotect.c · обработчик cmd_selfprotect

Развёрнутая встроенная справка

платформа PLATX — установка boot chain

использование: platxboot install [опции]

опции:
  --esp <path>       путь к ESP (default: автоопределение)
  --entry <name>     имя UEFI boot entry (default: PLATX)
  --efi <file>       путь к Antarctic.efi (ОБЯЗАТЕЛЬНО)
  --ko <file>        путь к platx_boot_verifier.ko (необязательно)
  --first            сделать первым в UEFI boot order
  --dry-run          показать план без исполнения
  --force            перезаписать существующую запись

пример:
  platxboot install --efi /path/to/Antarctic.efi --ko /path/to/platx_boot_verifier.ko
src/selfprotect/platxboot_cli.c
%s%splatxboot%s v%s — управление PLATX загрузочной цепочкой

команды:
  install      установить PLATX в ESP и UEFI boot order
  uninstall    удалить PLATX из ESP и UEFI boot order
  status       статус boot chain (EFI var + sysfs + flags)
  verify       полная верификация (chain + PCR4 + modsig)
  pcr4         TPM PCR[4] [--enroll | --show]
  esp-scan     сканирование ESP [--esp path] [--unknown-only]
  order        UEFI Boot Order [--set-first]
  grub-verify  in-memory проверка grub_verifiers_open
  modsig       in-memory проверка module_sig_check
  report       полный отчёт [--json] [--out file] [--esp path]
  module       управление module whitelist:
                 whitelist-add <path.ko>
                 whitelist-list
                 whitelist-remove <path>
                 whitelist-clear
                 whitelist-sync
  trust        цепочка доверия:
                 status
                 anchor show
                 revoke
                 hv

флаги:
  --help       эта справка
  --version    версия

требования:
  root, efibootmgr, tpm2-tools, sha256sum
  для modsig/grub-verify: CONFIG_PROC_KCORE=y
src/selfprotect/platxboot_cli.c
selfprotect <подкоманда>

Провайдер защиты:
  status           состояние провайдера (health, ABI, build)
  policy           capabilities и их атрибуты (quality, locus, risk)
  assets           hook inventory (перехватчики, символы)
  verify           self-test провайдера; PROBE_FAIL если не прошёл

Цепочка загрузки:
  boot <subcmd>    верификация boot chain, PCR4, ESP, modsig, отчёт
                   subcmd: status verify pcr4 enroll-pcr4 esp-scan
                           order grub-verify modsig report help

Whitelist модулей ядра:
  module <subcmd>  управление whitelist (dev+ino+SHA-256, BPF map)
                   subcmd: add remove list clear sync check help

Цепочка доверия:
  trust <subcmd>   EFI anchor, LKM, BPF map, PCR4, HV
                   subcmd: status anchor-show anchor-revoke hv help

PARTIAL / PROBE_FAIL — это НЕ 'ok с оговоркой'. Защита неполная.
quality=EXACT без locus=kernel — НЕ равно ядерному перехвату.
src/selfprotect/selfprotect_cli.c
ПРЕДУПРЕЖДЕНИЕ: %s — не все capabilities активны.
  selfprotect policy  — список capabilities и quality
  selfprotect verify  — self-test провайдера
  selfprotect boot status  — состояние цепочки загрузки
src/selfprotect/selfprotect_cli.c
sensesense

Происхождение наблюдений и объединение источников

Архитектура и API домена ↗

sense

читать кольцо наблюдения SENSE

sense <status|sources|stats|subscribe|query> …
Регистрация: src/sense/cmd_sense.c · обработчик cmd_sense

Развёрнутая встроенная справка

sense — читать кольцо наблюдения; команда ничего в него не пишет
  sense status                      модуль, источники, потребители, кольцо
  sense sources                     зарегистрированные источники
  sense stats                       published / refused / overwritten
  sense subscribe [опции]           поток записей до Ctrl-C
  sense query --last=N [опции]      то, что кольцо ещё держит

  --class=HEX   маска SENSE_CLASS_* (по умолчанию 0xFFFF)
  --prio=0..3   минимальный приоритет (по умолчанию 3)
  --count=N     остановиться после N записей
  --timeout=MS  ожидание следующей записи (по умолчанию 1000)
  --json        NDJSON — одна запись на строку
src/sense/cmd_sense.c

Аргументы и дополнительные формы вызова

  • sense query: нужен --last=Nsrc/sense/cmd_sense.c
stubотдельный интерфейс / библиотека

Внешний наблюдатель и управление одним дочерним worker

Архитектура и API домена ↗

Развёрнутая встроенная справка

usage: platx-stub [options] [-- worker-args...]
  --worker PATH       worker binary (default: sibling worker)
  --crash-budget N    max crash launches before safe-mode (default %d)
  --backoff-ms M      initial delay between crash respawns (default %d)
  --safe-mode         do not spawn; stay idle until signal
  --once              one cycle then exit (no restart / no idle wait)
src/stub/platx_stub.c
Это отдельный интерфейс домена. Его наличие в исходниках не означает регистрацию одноимённого корня в каждой консоли PLATX. Используйте executable или интеграцию, предусмотренные вашим профилем.
supervisorsupervisor

Операторское представление и инициирование восстановления

Архитектура и API домена ↗

supervisor

recovery watch table: status, health, restart

supervisor <status|health|restart|help>
Регистрация: src/supervisor/cmd_supervisor.c · обработчик c_supervisor

Развёрнутая встроенная справка

supervisor — recovery watch table

  supervisor status              watched plat_owner rows
  supervisor health              OK / BACKOFF / FATAL counts
  supervisor restart <name>      apply via recovery (fail closed)
  supervisor help

Names:
src/supervisor/cmd_supervisor.c

Аргументы и дополнительные формы вызова

  • usage: supervisor restart <name>src/supervisor/cmd_supervisor.c
surfaceотдельный интерфейс / библиотека

Ограниченная проверка возможностей операционной системы

Архитектура и API домена ↗

Самостоятельная CLI-команда и отдельная usage-справка не обнаружены. Управление находится у потребителей API — core. Контракты и границы приведены в досье домена.

taskmgrtaskmgr

Учёт задач, heartbeat, владельцы и ограничения

Архитектура и API домена ↗

taskmgr

Platform task manager: list, kill, limit threads by subsystem

taskmgr <list|top|kill|limits|limit|help> [options]
Регистрация: src/taskmgr/cmd_taskmgr.c · обработчик (cmd_fn_t)c_taskmgr

Развёрнутая встроенная справка

taskmgr — platform task manager

  taskmgr list  [--owner=S] [--state=running|done|killed|all] [--cpu]
                              list tasks (--cpu samples CPU usage)
  taskmgr top                 running tasks sorted by CPU
  taskmgr kill  --id=N        kill single task by ID
  taskmgr kill  --owner=S     kill all running tasks of owner
  taskmgr limits              show per-owner concurrency limits
  taskmgr limit --owner=S --max=N  set limit (0 = unlimited)
  taskmgr help

States: PENDING  RUNNING  DONE  KILLED  ERROR
Owners: mesh  mbus  http  plugin  platform  ...
src/taskmgr/cmd_taskmgr.c

Аргументы и дополнительные формы вызова

  • usage: taskmgr kill --id=N | --owner=NAMEsrc/taskmgr/cmd_taskmgr.c
  • usage: taskmgr limit --owner=S --max=N (0=unlimited)src/taskmgr/cmd_taskmgr.c
tracetrace

Трассировка операций, spans и корреляция

Архитектура и API домена ↗

trace

сквозная трассировка запросов (trace/span-id)

trace <start|span|end|show|spans|list|stats|dump>
Регистрация: src/trace/cmd_trace.c · обработчик cmd_trace

Развёрнутая встроенная справка

trace — сквозная трассировка запросов (Фаза 6, ТР-5.6.1–5.6.4)

  trace start <op> [--comp=NAME]  корневой спан: новый trace-id
  trace span  <op> [--comp=NAME]  дочерний спан под текущим
  trace end                        закрыть текущий спан
  trace show  <trace-id>           сводка по трейсу
  trace spans <trace-id>           разбивка по спанам с длительностями
  trace list  [--max=N]            трейсы в хранилище (новые первыми)
  trace stats                      счётчики кольца и хранилища
  trace dump                       полный дамп хранилища

Контекст живёт в TLS: все вызовы подсистем на этом потоке
автоматически получают trace_id/span_id в записях audit (JSONL),
поэтому весь путь запроса извлекается по одному идентификатору:
  grep '"trace_id":"<id>"' /var/log/platx/audit.jsonl

В коде: TRACE_SCOPE(comp, op) открывает спан и закрывает его на
выходе из области видимости; trace_capture/trace_restore переносят
контекст в рабочий поток, не теряя связь с трейсом.
src/trace/cmd_trace.c
transportstransport

Общий реестр транспортов, адаптеров и daemon-интерфейсов

Архитектура и API домена ↗

transport

Единый транспортный слой: 4 режима работы (local/daemon/ipc/keyring)

transport <list|status|mode|backend|server|daemon|serve|ws|tls|cache|ping|resolve|connect|stats|bench>
Регистрация: src/transports/cmd_transport.c · обработчик c_transport

Развёрнутая встроенная справка

transport — единый транспортный слой платформы
  ══════════════════════════════════════════════════════════════════════

  УПРАВЛЕНИЕ
    transport list                         список транспортов
    transport status [имя]                 подробный статус
    transport start|stop [имя]             запуск/остановка
    transport mode <имя> <режим>           local | daemon | ipc | keyring
    transport backend <имя> <стек>         sys | platx | <имя стека>
    transport server start|stop|status     обработчики IPC и keyring
               [--kind=ipc|keyring] [--socket=путь]
    transport config [--file=…] [--reload]  конфигурация

  ОПЕРАЦИИ (общие флаги: --transport=, --mode=, --stack=)
    transport ping <host> [--count=N] [--timeout=мс]
    transport resolve <имя> [--type=A|AAAA|MX|TXT|NS|CNAME]
    transport connect <host> <port> [--send="строка"]

  СЕРВЕРЫ И КАНАЛЫ
    transport daemon start|stop|status [--socket=путь]
                                            транспорты в отдельном процессе
    transport serve start [--port=8088] [--docroot=путь]
    transport serve stop|status            HTTP/1.1-сервер с маршрутами
    transport ws connect <ws://…> [--send="текст"] [--count=N]
    transport tls keygen [--path=tls/http]  ключ канала в vault
    transport tls enable|disable <имя> [--cipher=…]
    transport cache status|flush|on|off     DNS-кэш поверх keyring

  ДИАГНОСТИКА
    transport stats [имя] [--reset]        счётчики
    transport bench [имя] [--count=N] [--host=…]
                                            сравнить задержку по режимам

  РЕЖИМЫ
    local    — прямой вызов в процессе, минимальная задержка
    daemon   — отдельный процесс со своим сокетом
    ipc      — общий Unix-сокет транспортов (нужен transport server start)
    keyring  — обмен через keyring_store с TTL, работает без сокетов

  Переключение режима не требует перезапуска платформы.
  ══════════════════════════════════════════════════════════════════════
src/transports/cmd_transport.c

Аргументы и дополнительные формы вызова

  • Использование: transport ws connect <ws://host:port/path> [--send="текст"] transport ws echo <url> [--count=N]src/transports/cmd_transport.c
trusttrust

Доказательства, аппаратное доверие и ограниченные полномочия

Архитектура и API домена ↗

trust

плоскость доверия: внешний якорь, наборы криптографии, проверка evidence-бандла

trust <verify|anchor|suite|protection|selftest> ...
Регистрация: src/trust/cmd_trust.c · обработчик cmd_trust

Развёрнутая встроенная справка

trust hv <подкоманда> [аргументы]

  probe            — наличие platx_hv, режим, здоровье, пригодность
  status           — полный STATUS: vCPU, vmexits, generation, build
  health           — здоровье, потерянные события, результат selftest
  attest           — VMCALL-аттестация на всех CPU (действие с эффектом)
  quote NONCE_HEX  — синтетический quote PXHQ (64 hex-символа nonce)

ВАЖНО (TE-SEC-03):
  probe/attest/quote НЕ устанавливают 'observed' — это самосвидетельство
  модуля. Для внешней атестации: trust verify --status=FILE

Код возврата: 0 — успех (для probe: HV пригоден как якорь),
              1 — отказ (устройство недоступно, ioctl отказал,
                  HV непригоден или health VIOLATED/UNAVAILABLE),
              2 — ошибка вызова (неверные аргументы).
src/trust/hv/trust_cli_hv.c
trust tpm <подкоманда> [аргументы]

  probe              — проверить наличие TPM 2.0 и прочесть версию
  pcr [N]            — PCR-значения SHA-256 (0-7); N = индекс, пусто = все
  quote NONCE_HEX    — синтетический quote PCR 0-7 (64 hex-символа nonce)
  seal FILE          — [stub] запечатать содержимое файла под TPM
  unseal FILE        — [stub] распечатать файл, запечатанный seal

ВАЖНО (TE-SEC-03):
  probe/quote НЕ устанавливают 'observed' — это самосвидетельство TPM.
  Для внешней атестации: trust verify --status=FILE

Код возврата: 0 — успех, 1 — отказ (TPM недоступен или команда отказала),
              2 — ошибка вызова (неверные аргументы).
src/trust/tpm/trust_cli_tpm.c
trustctl — плоскость доверия PLATX (ТЗ docs/TZ_PLATX_SEL4_TEE_V2.md)

  verify --dir=КАТАЛОГ [--min-cosign=N]  проверить бандл (три проверки)
  publish --dir=КАТАЛОГ --log=Ф   опубликовать свёртку бандла в журнале
  devsign --dir=КАТАЛОГ --seed=HEX --signer=ID   подпись корня (СТЕНД)
  anchor status --log=Ф           размер и корень журнала
  anchor append --log=Ф --subject=S --payload=HEX [--kind=K] [--epoch=N]
  anchor prove  --log=Ф --index=N доказательство включения
  anchor verify --log=Ф --index=N проверить включение
  suite list                      наборы криптографии и их доступность
  suite show ИМЯ                  состав набора по слотам
  suite negotiate --local=A,B --peer=B,C [--require-hybrid]
  protection status [--placement=central-appliance]
  capab show    [--profile=P] [--config=Ф] [--as=СЛОЙ]
  capab check   [--profile=P] [--config=Ф] --status=Ф
  capab explain МЕХАНИЗМ [--profile=P] [--config=Ф] [--as=СЛОЙ]
        профили: portable os-enforced tee-service sel4-node hybrid
        слои:    builtin profile operator site signed
  selftest                        встроенные проверки верификатора

  tpm probe|pcr|quote|seal|unseal TPM 2.0: наличие, PCR, атестация

  hv probe|status|health|attest|quote  platx_hv: режим, здоровье,
        VMCALL-аттестация


Код возврата: 0 — проверка пройдена, 1 — не пройдена, 2 — ошибка вызова.
src/trust/trust_cli.c

Аргументы и дополнительные формы вызова

  • usage: hv quote NONCE_HEX (nonce = 32 байта = 64 hex-символа)src/trust/hv/trust_cli_hv.c
  • usage: tpm quote NONCE_HEX (nonce = 32 байта = 64 hex-символа)src/trust/tpm/trust_cli_tpm.c
  • нужно: suite <list|show|negotiate>src/trust/trust_cli.c
  • нужно: suite show <имя>src/trust/trust_cli.c
  • нужно: suite negotiate --local=A,B --peer=B,C [--require-hybrid] [--allow-national]src/trust/trust_cli.c
  • нужно: anchor <status|append|prove|verify>src/trust/trust_cli.c
  • нужно: anchor append --log=F --subject=S --payload=HEX [--kind=bundle] [--epoch=N]src/trust/trust_cli.c
  • нужно: verify --dir=КАТАЛОГsrc/trust/trust_cli.c
  • нужно: publish --dir=КАТАЛОГ --log=ФАЙЛ [--epoch=N] [--time=T] [--issuer=ID]src/trust/trust_cli.c
  • нужно: devsign --dir=КАТАЛОГ --seed=HEX --signer=IDsrc/trust/trust_cli.c
txpотдельный интерфейс / библиотека

Квоты, ограничения и вспомогательная транспортная политика

Архитектура и API домена ↗

Самостоятельная CLI-команда и отдельная usage-справка не обнаружены. Управление находится у потребителей API — transports, txpstack. Контракты и границы приведены в досье домена.

txpstacknetx · txpstack

Собственный сетевой стек и лаборатория сетевого обмена

Архитектура и API домена ↗

netx

демо-модуль поверх собственного стека (через реестр)

netx --stack=NAME <ping|connect|serve|udp> ...
Регистрация: src/txpstack/cmd_netx.c · обработчик cmd_netx

txpstack

сетевая лаборатория: свой TCP/IP стек над TAP (ARP/IP/ICMP/UDP/TCP, NAT, netem)

txpstack <available|create|start|stop|status|stats|set|inspect|capture|trace|benchmark|export|proxy|...>
Регистрация: src/txpstack/cmd_txpstack.c · обработчик cmd_txpstack

Развёрнутая встроенная справка

netx — демо-модуль поверх собственного стека (через реестр --stack=NAME)

  netx --stack=platx ping <ip> [--timeout=MS]
  netx --stack=platx connect <ip:port> [--send=TEXT] [--timeout=MS]
  netx --stack=platx serve --port=P [--timeout=MS]
  netx --stack=platx udp <ip:port> --send=TEXT [--timeout=MS]

Модуль не линкуется со стеком напрямую: он получает интерфейс
netstack_if_t через platform_require("netstack:<name>").
src/txpstack/cmd_netx.c
txpstack — сетевая лаборатория: собственный TCP/IP стек над TAP-устройством

  txpstack available                     проверка /dev/net/tun
  txpstack create --ip=IP [--netmask=M|--prefix=N] [--gateway=GW]
                  [--name=NM] [--mac=MAC] [--mtu=N]
                  [--loopback | --tap | --raw | --link=NAME]
                  [--iface=IF] [--promisc] [--filter=EXPR]
                  [--allow-spoof] [--force-ip]
  txpstack filter compile "<выражение>" [--explain] [--hex]
  txpstack sniff --iface=IF [--filter=ВЫР] [--file=out.pcap]
                 [--seconds=N] [--count=N] [--promisc]
      захват без создания стека (нужен CAP_NET_RAW)
      режим канала: loopback (без устройства, офлайн-тесты) | tap |
                    raw (AF_PACKET, нужен CAP_NET_RAW) | hybrid (tap+raw)
      raw: IP должен быть СВОБОДНЫМ адресом подсети --iface, иначе
           ядро будет отвечать на те же пакеты параллельно со стеком
  txpstack start [--verbose] | stop | destroy
  txpstack status | stats | ifup
  txpstack ping <ip> [--timeout=MS]
  txpstack route add --net=CIDR --gw=IP | del --net=CIDR | show
  txpstack nat enable --source=CIDR --masquerade
  txpstack nat add --proto=tcp|udp --dport=P --redirect=ADDR:PORT | show
  txpstack set ttl|fragmentation|delay|order|dup|loss|rate|cc|wscale|
               timestamp|ackdelay|sack|nodelay|keepalive ...
               order --pct=P --gap=N     перестановка на N позиций
               loss --p=P --r=R          коррелированные потери (Гилберт-Эллиотт)
               rate --kbit=K             ограничение полосы (token bucket)
               cc reno|cubic             алгоритм контроля перегрузки
  txpstack capture start --file=PATH [--snaplen=N] [--max=МБ] | stop | status
                                         запись кадров в pcap для Wireshark
  txpstack debug --level=ERROR|WARN|INFO|DEBUG|TRACE
                                         TRACE — разбор и hex-дамп каждого кадра
  txpstack inspect arp|tcp|route|nat
  txpstack trace <ip> [--timeout=MS]       трассировка маршрута (on-link/шлюз, ARP, проба)
  txpstack benchmark <ip> [--count=N] [--timeout=MS]
                                         замер задержки/потерь серией ICMP echo
  txpstack export [--file=PATH] [--format=json|text]

  ── режим tun2socks ──────────────────────────────────────────────
  txpstack proxy start --upstream=URL [--max=N]
                       [--connect-timeout=MS] [--idle=MS]
      включает прозрачный перехват и уводит ВСЕ перехваченные
      соединения в вышестоящий прокси
  txpstack proxy stop | stat | sess [--top=N]
  txpstack proxy test  --upstream=URL --to=HOST:PORT [--timeout=MS]
      проверка прокси отдельно от стека
      URL: socks5://[user:pass@]host:port | socks5h:// | socks4://
           socks4a:// | http://   (порт по умолчанию 1080 / 3128)

  быстрый старт (TAP, шлюз по умолчанию у клиента ведёт сюда):
      txpstack create --ip=10.0.0.2 --tap --transparent
      txpstack start
      txpstack ifup                      # покажет команды ip link/addr
      txpstack proxy start --upstream=socks5://127.0.0.1:1080
                                         экспорт статистики и таблиц (по умолчанию json)

Экземпляр публикуется в реестре как netstack:<name>; сетевые модули
обращаются к нему через --stack=<name> (см. команду netx).
Несколько стеков: адресуйте нужный через --name=<NM> в любой подкоманде.
src/txpstack/cmd_txpstack.c
usage: txpstack proxy <start|stop|stat|sess|test> [опции]

  start --upstream=URL [--name=NM] [--max=N]
        [--connect-timeout=MS] [--idle=MS]
            включает прозрачный перехват и уводит все перехваченные
            соединения в вышестоящий прокси
  stop  [--name=NM]              остановить режим
  stat  [--name=NM]              счётчики
  sess  [--name=NM] [--top=N]    активные сессии
  test  --upstream=URL --to=HOST:PORT [--timeout=MS]
            проверить прокси без стека

  URL: socks5://[user:pass@]host:port | socks5h:// | socks4:// |
       socks4a:// | http://  (порт по умолчанию 1080, у http 3128)
src/txpstack/cmd_txpstack.c
usage: txpstack create --ip=IP [--netmask=M|--prefix=N] [--gateway=GW]
                       [--name=NM] [--mac=MAC] [--mtu=N]
                       [--loopback | --tap | --raw | --link=NAME]
                       [--iface=eth0] [--promisc] [--allow-spoof] [--force-ip]
                       [--transparent]
src/txpstack/cmd_txpstack.c
usage: txpstack set cc <reno|cubic>
  reno  — RFC 5681 + NewReno: +1 MSS за RTT,
          при потере окно пополам
  cubic — RFC 8312: рост как куб ВРЕМЕНИ, не числа
          RTT; при потере окно до 70 %%
src/txpstack/cmd_txpstack.c
usage: txpstack set <ttl|fragmentation|delay|order|dup|loss|rate|
                     cc|bufs|cwv|icmp-errors|pmtud|fwd-mtu|
                     wscale|timestamp|ackdelay|sack|nodelay|
                     keepalive> ...
  bufs  --rcv=N --snd=N     размеры буферов TCP
  cwv   on|off              валидация окна перегрузки (RFC 7661)
  icmp-errors on|off        доводить ICMP-ошибки до сокетов
  pmtud on|off              Path MTU Discovery (RFC 1191)
  fwd-mtu --mtu=N           узкое место для PMTUD при форвардинге
  vlan  --id=N [--pcp=N] [--strict=on]   тегирование 802.1Q
  rr    --slots=N           record route (RFC 791)
  ttl-os <linux|windows|cisco|freebsd>   отпечаток начального TTL
  order --pct=P --gap=N     перестановка: P%% кадров сдвигаются на N
  loss  --pct=P             равномерные потери
  loss  --p=P --r=R [--loss-bad=B] [--loss-good=G]
                            коррелированные потери (Гилберт-Эллиотт)
  rate  --kbit=K [--burst=B] [--qlimit=Q]   ограничение полосы
  cc    <reno|cubic>        алгоритм контроля перегрузки
src/txpstack/cmd_txpstack.c
usage: txpstack sniff --iface=eth0 [--filter=ВЫР] [--file=out.pcap]
                      [--seconds=N] [--count=N] [--promisc]

Захват без создания стека. Нужен CAP_NET_RAW.
Файл открывается любым читателем pcap (Wireshark, tcpdump -r) —
это и есть внешняя проверка корректности собственного стека.
src/txpstack/cmd_txpstack.c
usage: txpstack filter compile "<выражение>" [--explain] [--hex]

  filter := group ( or group )*
  group  := prim  ( [and] prim )*   'and' необязателен
  prim   := ip | ip6 | arp | tcp | udp | icmp
          | [src|dst] host A.B.C.D
          | [src|dst] port N

  'and' необязателен: "tcp port 80" == "tcp and port 80".
  'and' связывает сильнее 'or'; скобок и 'not' нет.
  Это подмножество языка pcap, а не он целиком.

примеры:
  txpstack filter compile "tcp port 80" --explain
  txpstack filter compile "host 10.0.0.1 or arp"
  txpstack filter compile "ip and src host 10.0.0.5 and dst port 443"
src/txpstack/cmd_txpstack.c

Аргументы и дополнительные формы вызова

  • usage: netx --stack=NM ping <ip> [--timeout=MS]src/txpstack/cmd_netx.c
  • usage: netx --stack=NM connect <ip:port> [--send=TEXT]src/txpstack/cmd_netx.c
  • usage: netx --stack=NM serve --port=P [--timeout=MS]src/txpstack/cmd_netx.c
  • usage: netx --stack=NM udp <ip:port> --send=TEXTsrc/txpstack/cmd_netx.c
  • usage: txpstack set %s on|offsrc/txpstack/cmd_txpstack.c
  • usage: txpstack ping <ip> [--timeout=MS]src/txpstack/cmd_txpstack.c
  • usage: txpstack route add --net=CIDR --gw=IPsrc/txpstack/cmd_txpstack.c
  • usage: txpstack route del --net=CIDRsrc/txpstack/cmd_txpstack.c
  • usage: txpstack nat enable --source=CIDR --masqueradesrc/txpstack/cmd_txpstack.c
  • usage: txpstack nat add --proto=tcp|udp --dport=P --redirect=ADDR:PORTsrc/txpstack/cmd_txpstack.c
  • usage: txpstack set ttl-os <linux|windows|cisco|freebsd|default> начальный TTL — заметная часть отпечатка ОС: linux/freebsd 64, windows 128, cisco/solaris 255src/txpstack/cmd_txpstack.c
  • usage: txpstack inspect <arp|tcp|route|nat|rr>src/txpstack/cmd_txpstack.c
  • usage: txpstack capture start --file=PATH [--snaplen=N] [--max=МБ]src/txpstack/cmd_txpstack.c
  • usage: txpstack capture <start --file=PATH [--snaplen=N] [--max=МБ]|stop|status>src/txpstack/cmd_txpstack.c
  • usage: txpstack trace <ip> [--timeout=MS]src/txpstack/cmd_txpstack.c
  • usage: txpstack benchmark <ip> [--count=N] [--timeout=MS]src/txpstack/cmd_txpstack.c
  • usage: txpstack proxy test --upstream=URL --to=HOST:PORTsrc/txpstack/cmd_txpstack.c
uipcотдельный интерфейс / библиотека

Unix IPC и передача файловых дескрипторов

Архитектура и API домена ↗

Самостоятельная CLI-команда и отдельная usage-справка не обнаружены. Управление находится у потребителей API — uring. Контракты и границы приведены в досье домена.

uringuring · uipc

Асинхронное исполнение и интеграции io_uring

Архитектура и API домена ↗

uring

асинхронный ввод-вывод поверх io_uring (прямые syscalls, с fallback)

uring <init|status|read|write|aread|awrite|recv|send|nop|test|…> ...
Регистрация: src/uring/cmd_uring.c · обработчик cmd_uring

uipc

асинхронный IPC поверх io_uring: abstract-сокеты, accept/recv/send через кольцо, SCM_RIGHTS

uipc <server|send|sendfd|help> ...
Регистрация: src/uring/uring_ipc.c · обработчик cmd_uring_ipc

Развёрнутая встроенная справка

uring — асинхронный ввод-вывод поверх Linux io_uring (прямые syscalls)

  uring init [--depth=N] [--flags=sqpoll,coop_taskrun]
  uring status
  uring register --fd=N        uring unregister --fd=N
  uring read <path> [--offset=N] [--size=N]
  uring write <path> --data "..."
  uring aread <path>           uring awrite <path> --data "..."
  uring copy <src> <dst>
  uring recv --fd=N [--size=N] [--timeout=S]
  uring send --fd=N --data "..."
  uring accept --fd=N          uring close --fd=N
  uring memfd <load <path>|save <id> <path>|exec <id> [args...]>
  uring vault <write <path> --data "..."|read <path>>
  uring nop                    uring test
  uring silent <on|off>
  uring lkm-load --lkm=<path> [--hex=<hex>] [--hide] [--async] [--ebpf]

При отсутствии поддержки io_uring движок автоматически переходит на
синхронный резервный бэкенд (fallback).
src/uring/cmd_uring.c
uipc — асинхронный IPC поверх io_uring (abstract-сокеты + SCM_RIGHTS)

  uipc server  --name=NAME [--count=N]   слушать abstract-сокет,
                                          принять/обслужить N соединений
                                          (accept/recv/send через io_uring)
  uipc send    --name=NAME --msg=TEXT     подключиться и отправить сообщение
  uipc sendfd  --name=NAME --fd=N         передать дескриптор через SCM_RIGHTS
  uipc help

Абстрактный сокет: sun_path[0]='\0' — нет файла в ФС.
accept/recv/send идут через io_uring_enter() (см. `xio mode uring`).
src/uring/uring_ipc.c

Аргументы и дополнительные формы вызова

  • usage: uring register --fd=Nsrc/uring/cmd_uring.c
  • usage: uring unregister --fd=Nsrc/uring/cmd_uring.c
  • usage: uring %s <path> [--offset=N] [--size=N]src/uring/cmd_uring.c
  • usage: uring %s <path> --data "..."src/uring/cmd_uring.c
  • usage: uring copy <src> <dst>src/uring/cmd_uring.c
  • usage: uring recv --fd=N [--size=N] [--timeout=S]src/uring/cmd_uring.c
  • usage: uring send --fd=N --data "..."src/uring/cmd_uring.c
  • usage: uring accept --fd=Nsrc/uring/cmd_uring.c
  • usage: uring close --fd=Nsrc/uring/cmd_uring.c
  • usage: uring memfd <load <path>|save <id> <path>|exec <id> [args...]>src/uring/cmd_uring.c
  • usage: uring memfd load <path>src/uring/cmd_uring.c
  • usage: uring memfd save <id> <path>src/uring/cmd_uring.c
  • usage: uring memfd exec <id> [args...]src/uring/cmd_uring.c
  • usage: uring vault <write <path> --data "..."|read <path>>src/uring/cmd_uring.c
  • usage: uring vault write <path> --data "..."src/uring/cmd_uring.c
  • usage: uring vault read <path>src/uring/cmd_uring.c
  • usage: uring async exec --mode=<fexecve|execveat|dlopen> --source=<path> [--args="a b"] [--bg] [--timeout=MS]src/uring/cmd_uring.c
  • usage: uring async exec-status <id>src/uring/cmd_uring.c
  • usage: uring async exec-cancel <id>src/uring/cmd_uring.c
  • usage: uring async <op> nop | read <path> [--size=N] [--offset=N] | write <path> --data=STR copy <src> <dst> | fsync --fd=N | close --fd=N status [id] | cancel <id>src/uring/cmd_uring.c
  • usage: uring async cancel <id>src/uring/cmd_uring.c
  • usage: uring async read <path> [--size=N] [--offset=N]src/uring/cmd_uring.c
  • usage: uring async write <path> --data=STRsrc/uring/cmd_uring.c
  • usage: uring async copy <src> <dst>src/uring/cmd_uring.c
  • usage: uring async %s --fd=Nsrc/uring/cmd_uring.c
  • usage: uring silent <on [--jitter=MS]|off|status>src/uring/cmd_uring.c
  • usage: uipc server --name=NAME [--count=N]src/uring/uring_ipc.c
  • usage: uipc send --name=NAME --msg=TEXTsrc/uring/uring_ipc.c
  • usage: uipc sendfd --name=NAME --fd=Nsrc/uring/uring_ipc.c
vaultvault

Зашифрованное хранилище объектов в памяти

Архитектура и API домена ↗

vault

зашифрованное in-memory хранилище (VaultFS)

vault <keyring|token|session|put|get|list|...>
Регистрация: src/vault/cmd_vault.c · обработчик cmd_vault_dispatch

Развёрнутая встроенная справка

vault — зашифрованное in-memory хранилище (VaultFS)

Keyring:
  vault keyring generate              — случайный мастер-ключ
  vault keyring set-pass <pass>       — ключ из парольной фразы
  vault keyring set-hex  <hex64>      — ключ из HEX (64 символа)
  vault keyring show                  — показать ключ (hex)

Токены:
  vault token create [opts]           — создать токен
    --profile <name>                    имя профиля (def: default)
    --ttl <sec>                         время жизни (0=вечный)
    --perm <rwd>                        права: r=read w=write d=delete
  vault token list                    — список токенов
  vault token info   <id>             — детали токена
  vault token verify <id>             — проверить валидность
  vault token revoke <id>             — отозвать токен

Сессии:
  vault session open [opts]           — открыть сессию
    --token  <id>                       токен (необязательно)
    --ttl    <sec>                      таймаут неактивности
    --cipher <name>                     алгоритм шифрования
  vault session close <id>            — закрыть сессию
  vault session touch <id>            — сбросить таймаут
  vault session info  <id>            — детали сессии
  vault session list                  — список сессий
  vault session gc                    — удалить просроченные

Объекты:
  vault put <path> <value>            — записать значение
  vault put <path> --file <file>      — записать из файла
  vault put <path> --stdin            — записать из stdin
  vault get <path>                    — прочитать значение
  vault get <path> --hex              — вывести в hex
  vault get <path> --file <file>      — сохранить в файл
  vault delete <path>                 — удалить объект
  vault rename <old> <new>            — переименовать
  vault stat   <path>                 — метаданные объекта
  vault list   [prefix]               — список объектов
  vault clear  [--force]              — очистить хранилище

Snapshot:
  vault snapshot <file> [opts]        — экспорт на диск
    --key-hex <hex64>                   ключ шифрования снимка
    --cipher  <name>                    алгоритм (def: chacha20poly1305)
    --prefix  <prefix>                  фильтр по пути
  vault restore <file> [opts]         — импорт с диска
    --key-hex <hex64>                   ключ дешифровки снимка
    --merge                             добавить поверх существующих

Разное:
  vault cipher list                   — поддерживаемые шифры
  vault info                          — статистика хранилища
  vault help                          — эта справка
src/vault/cmd_vault.c

Аргументы и дополнительные формы вызова

  • usage: vault keyring <generate|set-pass|set-hex|show>src/vault/cmd_vault.c
  • usage: vault keyring set-pass <passphrase>src/vault/cmd_vault.c
  • usage: vault keyring set-hex <hex64>src/vault/cmd_vault.c
  • usage: vault token <create|list|info|verify|revoke>src/vault/cmd_vault.c
  • usage: vault token info <id>src/vault/cmd_vault.c
  • usage: vault token verify <id>src/vault/cmd_vault.c
  • usage: vault token revoke <id>src/vault/cmd_vault.c
  • usage: vault session <open|close|touch|info|list|gc>src/vault/cmd_vault.c
  • usage: vault session close <id>src/vault/cmd_vault.c
  • usage: vault session touch <id>src/vault/cmd_vault.c
  • usage: vault session info <id>src/vault/cmd_vault.c
  • usage: vault put <path> <value> vault put <path> --file <filepath> vault put <path> --stdinsrc/vault/cmd_vault.c
  • usage: vault get <path> [--hex] [--file <filepath>] [--session <id>]src/vault/cmd_vault.c
  • usage: vault stat <path>src/vault/cmd_vault.c
  • usage: vault snapshot <file> [--key-hex <hex64>] [--cipher <name>] [--prefix <p>]src/vault/cmd_vault.c
  • usage: vault restore <file> [--key-hex <hex64>] [--merge]src/vault/cmd_vault.c
  • usage: vault delete <path>src/vault/cmd_vault.c
  • usage: vault rename <old> <new>src/vault/cmd_vault.c
vfsvfs · store

Виртуальные файловые объекты, потоки и конфигурация

Архитектура и API домена ↗

vfs

обзор in-memory зашифрованного хранилища

vfs <ls|tree|df|stat|get|put|cfg|apply>
Регистрация: src/vfs/cmd_vfs.c · обработчик cmd_vfs_dispatch

store

alias vfs

store — alias для vfs
Регистрация: src/vfs/cmd_vfs.c · обработчик cmd_vfs_dispatch

Развёрнутая встроенная справка

vfs (alias store) — обзор in-memory зашифрованного хранилища

  vfs ls [/dir]     список (корень или директория)
  vfs tree          всё дерево
  vfs stat </dir>   сводка по области
  vfs df            использование памяти по областям
  vfs get <path>    прочитать файл из крипто-контейнера
  vfs put <path> <file>  положить файл в контейнер
  vfs cfg [key]     применённые ключи из /cfg/*.conf
  vfs apply         перечитать /cfg/*.conf в env/таблицу

Контейнер: /msx /dsl /cfg /modules /plugins /hooks
Обзор:     /internal /external /keys /configs /temp /deploy
src/vfs/cmd_vfs.c

Аргументы и дополнительные формы вызова

  • usage: vfs stat </area or /area/name>src/vfs/cmd_vfs.c
  • usage: vfs get <vfs:/path|/path>src/vfs/cmd_vfs.c
  • usage: vfs put <vfs:/path> <file>src/vfs/cmd_vfs.c
wiperwiper

Завершение жизни и стирание чувствительных буферов

Архитектура и API домена ↗

wiper

Secure data erasure: zero, DoD, NIST, Schneier, Gutmann algorithms

wiper <status|file|memory|dir|free|metadata|self-test|benchmark|help>
Регистрация: src/wiper/cmd_wiper.c · обработчик (cmd_fn_t)c_wiper

Развёрнутая встроенная справка

wiper — secure data erasure

  wiper status                              statistics and algorithm list
  wiper file    --path=F [--algo=A] [flags] overwrite + delete a file
  wiper memory  --addr=HEX --size=N [--algo=A]  zero a memory region
  wiper dir     --path=D [--algo=A] [--recursive] [flags]  erase dir
  wiper free    --path=MP [--algo=A]        fill filesystem free space
  wiper metadata --path=F                   rename+truncate+unlink
  wiper self-test                           verify all algorithms
  wiper benchmark [--algo=A] [--size=N]     measure throughput
  wiper help

Algorithms (--algo=):
  zero           1 pass   0x00
  ones           1 pass   0xFF
  random         1 pass   CSPRNG
  dod            3 pass   DoD 5220.22-M
  dod_enhanced   7 pass   DoD enhanced
  nist           3 pass   NIST SP 800-88
  schneier       7 pass   Schneier
  gutmann       35 pass   Gutmann

Flags: --verify  --sync  --trim  --rename  --recursive
Note: on SSDs, software patterns cannot override FTL remapping.
      Use --trim to additionally issue BLKDISCARD.
src/wiper/cmd_wiper.c

Аргументы и дополнительные формы вызова

  • usage: wiper file --path=<file> [--algo=<name>] [--verify] [--sync] [--trim] [--rename] Default algo: dod (3 passes)src/wiper/cmd_wiper.c
  • usage: wiper memory --addr=<hex> --size=<bytes> [--algo=<name>]src/wiper/cmd_wiper.c
  • usage: wiper dir --path=<dir> [--algo=<name>] [--recursive] [--verify] [--sync] [--rename]src/wiper/cmd_wiper.c
  • usage: wiper free --path=<mountpoint> [--algo=<name>] [--sync] Fills free space on the filesystem to sanitise deleted file content.src/wiper/cmd_wiper.c
  • usage: wiper metadata --path=<file>src/wiper/cmd_wiper.c
ximxim

Посредничество исполнения и системных вызовов

Архитектура и API домена ↗

xim

arm the CHILD syscall mediator (off unless armed)

xim <arm|spawn|map|status|disarm>
Регистрация: src/xim/cmd_xim.c · обработчик cmd_xim

Развёрнутая встроенная справка

xim — arm the CHILD syscall mediator (off unless armed)
  xim arm [nr ...]   mediate these syscall numbers on the next CHILD spawn
                     (default: getcwd). Allowed by default; re-arm -> EBUSY.
  xim spawn [--xim] [--ready-only] <exe> [args...]
                     spawn a CHILD; --xim requires isolation.xim:v1 then
                     arms before fork. Missing cap / arm not READY / exec
                     fail -> no READY, no leftover child. Off unless --xim.
  xim map add <nr> <arg> <logical> <target>
                     add a virtual-map rule to the armed instance.
  xim map status     print the count of map rules on the armed instance.
  xim map clear      remove all map rules from the armed instance.
  xim status         armed? which instance, state, allow/deny counts
  xim disarm         stop the worker (drain), release the mediation
src/xim/cmd_xim.c

Аргументы и дополнительные формы вызова

  • usage: xim spawn [--xim] [--ready-only] <exe> [args...]src/xim/cmd_xim.c
  • usage: xim map <add <nr> <arg> <logical> <target> | status | clear>src/xim/cmd_xim.c
  • usage: xim map add <nr> <arg> <logical> <target>src/xim/cmd_xim.c
xioxio · lkm

Единая модель ввода-вывода, backend, маршрута и владельца

Архитектура и API домена ↗

xio

единый гибридный слой ввода-вывода XIO (8 режимов, per-subsystem конфиг)

xio <status|list|explain|route|preset|mode|get|set|module|defaults|stats|hardcore|loop|leaks|help> ...
Регистрация: src/xio/cmd_xio.c · обработчик cmd_xio

lkm

kernel module loader: finit_module/delete_module, no hiding

lkm <load|unload|check|list|help> ...
Регистрация: src/xio/xio_lkm.c · обработчик cmd_lkm

Развёрнутая встроенная справка

xio — управление I/O подсистемой (8 режимов, per-subsystem конфиг)

КОМАНДЫ

  xio status
      Ring, ebpf poll, event loop (peek, не создаёт), fd registry, режимы.

  xio list
      Список подсистем с текущими режимами.

  xio explain <subsys> [category]
      Что вернёт xio_get_for для этой подсистемы: токен режима,
      провайдер/метод, есть ли настоящий ring, принята ли категория.
      Ничего не выбирает — спрашивает тот же селектор и печатает ответ.
      category: имя (file network memory raw process ebpf) или число;
      без аргумента — file. Отказ → exit 1.
      Примеры:
        xio explain uring.async file
        xio explain syscall.direct ebpf

  xio route <op> [size] [subsys]
      Цепочка методов, которую даст резолвер для одной операции,
      и причина по каждому методу, который в цепочку не попал.
      Ничего не выбирает — спрашивает резолвер и печатает ответ.
      op: file.read file.write file.transfer memory.create process.pidfd
      size: байты или с суффиксом K/M/G; без аргумента — 0.
      Дескриптор из CLI не передаётся, поэтому методы, которым он
      нужен, показаны отсеянными — это честнее, чем молча их скрыть.
      Примеры:
        xio route file.read 4M
        xio route file.transfer 1M ra2c

  xio preset [name]
      Активный пресет правил и поколение маршрутов; с аргументом —
      переключить (поколение растёт, уже выбранные цепочки не меняются).
      Пресеты: default, ra2c.

  xio mode <subsys|all> <mode>
      Переключить режим подсистемы или всех сразу.
      Режимы: uring.direct  uring.sync  uring.async  uring.hardcore
              syscall.direct  syscall.syscall
              ebpf.direct  ebpf.async
      uring.direct/sync/async — thread pool + libc; ring только hardcore.
      Имена uring.* исторические и сохраняются до v1.2.
      Примеры:
        xio mode net_server uring.hardcore
        xio mode all syscall.direct

  xio get <subsys>
      Показать полный конфиг подсистемы.
      Пример: xio get net_server

  xio get <subsys> <key>
      Получить одну настройку подсистемы.
      Пример: xio get net_server hardcore.sqpoll

  xio set <subsys> <key> <value>
      Установить настройку подсистемы. Значения: true/false, целое число.
      Примеры:
        xio set net_server hardcore.sqpoll true
        xio set net_server hardcore.block_sync false
        xio set dbg_trace  sync.pool_size 4

  КЛЮЧИ НАСТРОЕК
    sync.pool_size           int   (default: 8)
    sync.prefer_async        unused — set refused

    async.queue_depth        int   (default: 512)

    async.cq_depth           unused — set refused
    async.timeout_ms         int   (default: 0 = infinite) — live: pool wait

    hardcore.sqpoll          bool  (default: true)
    hardcore.sqpoll_idle_ms  int   (default: 2000)
    hardcore.register_buffers bool (default: true)
    hardcore.buffer_count    int   (default: 64)
    hardcore.buffer_size     int   (default: 65536)
    hardcore.multishot       bool  (default: true)
    hardcore.block_sync      bool  (default: true)  — abort() при sync-вызове
    hardcore.fallback        bool  (default: false) — при ошибке uring → EBUSY
    hardcore.randomise_sqe   bool  (default: false) — chaos testing
    hardcore.sq_depth        int   (default: 1024)
    hardcore.cq_depth        int   (default: 4096)
    hardcore.engine          str   (default: "syscall")
                             syscall → raw __NR_*  (hardcore default)
                             direct  → libc wrappers
                             ""     → xio_get()

    ebpf.log_level           int   (default: 1) — live: verifier log_level
    ebpf.log_sz              int   (default: 1<<20) — live: verifier log_sz
    ebpf.ring_timeout_ms     int   (default: 100) — live: poll timeout
    ebpf.pin / ebpf.bpffs    unused — set refused

  xio module show <subsys>
      Alias для 'xio get <subsys>' — показать конфиг подсистемы.

  xio module mode <subsys> <mode>
      Alias для 'xio mode <subsys> <mode>'.

  xio module set <subsys> <key> <value>
      Alias для 'xio set <subsys> <key> <value>'.

  xio defaults dump
      Показать глобальные дефолты (применяются к новым подсистемам).

  xio defaults get <key>
      Прочитать глобальный дефолт.

  xio defaults set <key> <value>
      Изменить глобальный дефолт (не меняет уже существующие подсистемы).
      Пример: xio defaults set hardcore.sqpoll true

  xio stats [inspect]

      Счётчики XIO (xio_stats_text). Нет символа — ошибка, не fallback.

  xio stats reset
      Сбросить счётчики hardcore ring.

  xio hardcore reinit
      Переинициализировать hardcore ring с текущими глобальными дефолтами.
      Выполнить после изменения ring-level настроек:
        xio defaults set hardcore.sq_depth 2048
        xio hardcore reinit

  xio hardcore status
      Статус hardcore ring: настройки + статистика.

  xio loop status|start|stop
      status не создаёт loop. start — фоновый поток. stop — join.

  xio leaks
      Owner fd claims (xio_owner_leaks). Нет owner в CLI — n/a (opt-in), не fake 0.

  xio selftest
      Самопроверка async-пути (sync-via-null-cb + callback) через xio_get_for.

WORKFLOW
  # Настроить hardcore для одной подсистемы:
  xio defaults set hardcore.sqpoll true
  xio defaults set hardcore.block_sync true
  xio mode net_accept uring.hardcore
  xio set  net_accept hardcore.multishot false  # per-subsystem override

  # Изменить ring-level настройки и перезапустить:
  xio defaults set hardcore.sq_depth 2048
  xio hardcore reinit

  # Посмотреть что получилось:
  xio status
  xio get net_accept
  xio hardcore status
  xio stats

  # Переключить все в syscall для отладки:
  xio mode all syscall.direct
src/xio/cmd_xio.c
lkm — kernel module loader (no hiding, uses finit_module/delete_module)

  lkm load   --path=FILE [--params=STR] [--force]   load .ko module
  lkm unload --name=NAME                            unload by name
  lkm check                                         show security state
  lkm list                                          show /proc/modules
  lkm help

Security checks:
  modules_disabled (/proc/sys/kernel/modules_disabled) — blocks load if 1
  module.sig_enforce (/proc/cmdline)  — kernel requires signed modules

Loaded modules are always visible in /proc/modules and /sys/module.
src/xio/xio_lkm.c
usage: xio set <subsys> <key> <value>
  sync keys:     sync.pool_size
  async keys:    async.queue_depth  async.timeout_ms
  hardcore keys: hardcore.sqpoll  hardcore.sqpoll_idle_ms
                 hardcore.register_buffers  hardcore.buffer_count
                 hardcore.buffer_size  hardcore.multishot
                 hardcore.block_sync  hardcore.fallback
                 hardcore.randomise_sqe  hardcore.sq_depth
                 hardcore.cq_depth
                 hardcore.engine  (syscall|direct|<empty>=io_engine)
src/xio/cmd_xio.c

Аргументы и дополнительные формы вызова

  • usage: xio mode <subsys|all> <mode> modes: %ssrc/xio/cmd_xio.c
  • usage: xio get <subsys> [key]src/xio/cmd_xio.c
  • usage: xio defaults dump xio defaults get <key> xio defaults set <key> <value>src/xio/cmd_xio.c
  • usage: xio defaults get <key>src/xio/cmd_xio.c
  • usage: xio defaults set <key> <value>src/xio/cmd_xio.c
  • usage: xio module show <subsys> xio module mode <subsys> <mode> xio module set <subsys> <key> <value>src/xio/cmd_xio.c
  • usage: xio stats [inspect|reset]src/xio/cmd_xio.c
  • usage: xio hardcore reinit|statussrc/xio/cmd_xio.c
  • usage: xio loop status|start|stopsrc/xio/cmd_xio.c
  • usage: xio explain <subsys> [category] category: name (file, network, memory, raw, process, ebpf) or number; default filesrc/xio/cmd_xio.c
  • usage: xio route <op> [size] [subsys] op: file.read file.write file.transfer memory.create process.pidfd ... size: bytes, or with a K/M/G suffix; default 0src/xio/cmd_xio.c
  • usage: lkm load --path=FILE [--params=STR] [--force]src/xio/xio_lkm.c
  • usage: lkm unload --name=NAMEsrc/xio/xio_lkm.c
09 / ARCHITECTURE HANDBOOK

Глубже каждого интерфейса.

Развёрнутые главы о системе доверия, исследовательском цикле, архитектурных контрактах, изоляции и ARENA. Этот слой соединяет инженерную детализацию CURRENT STATE, масштаб MEGA ROADMAP и исследовательскую глубину STEP 3.

Главы: 260 / 260

Доверенное исполнение

001Доверие становится условием действия.

Я включаю узел, задаю правила и вижу, какую работу ему можно доверить. Защита сопровождает эту работу от загрузки до результата — и объясняет каждое ограничение.

002Узел готов к разрешённой работе

Загрузка подтверждена, обязательные ограничения действуют. Узел может запросить краткосрочное право подписи у защищённого сервиса.

Сбор телеметрии, утверждённые задачи, подпись в пределах выданного допуска.

Право относится только к указанному ресурсу, получателю и сроку.

003Доверие подтверждено для этого запроса

Политика задаёт требования, проверяющий подтверждает их свежими доказательствами, а сам сервис проверяет право непосредственно перед подписью.

Какая задача допущена, где находится ключ, когда истечёт право и на какие доказательства опирается решение. Закрытый ключ в панель не попадает.

004Один непрерывный цикл

Защита отвечает на практический вопрос: «Разрешено ли именно этому исполнителю сделать именно это — при нынешнем состоянии узла?» Ответ пересматривается при значимом изменении.

005Оставить след

Сохранить решение, фактический результат и проверяемые доказательства.

Моя задача — определить, кому, что и при каких условиях разрешено. PLATX связывает эти условия с фактической защитой и исполнением.

006Как всё соединено внутри

Каждый механизм отвечает за свою часть. Общая политика связывает их в допуск к работе, а единый жизненный цикл управляет отказами и восстановлением.

Запрос задачи, утверждённая политика, полномочия пользователя и требуемые свойства защиты.

007Можно ли доверить это действие сейчас?

Проверка доказательств устанавливает свойства узла. Авторизация определяет, кому и для чего ими разрешено воспользоваться.

Secrets, подписант или другой защищённый consumer сверяет допуск, ACL, срок и текущее поколение перед действием. Прямой вызов этого шага не обходит.

Исполнитель подтверждает результат или частичный отказ. Audit и forensic связывают его с запросом и доказательствами; внешний якорь фиксирует принятую историю.

008TPM, TEE и seL4 дополняют друг друга

TPM связывает доказательство и ключи с состоянием платформы. TEE исполняет выбранную чувствительную функцию в защищённой среде. seL4 разделяет небольшие доверенные службы и их права в отдельной системной поставке.

009Пользовательский загрузчик сохраняет свою роль

Дополнительный контроль входит в trust : trust boot … , trust tpm … . Он проверяет цепочку и реальное принуждение по политике. Штатные проверки не переписывают boot, EFI-настройки или порядок загрузки.

010Как я пользуюсь готовой системой

Один путь от первого включения до защищённой операции. Детальные PCR, подписи и версии остаются доступными для разбора, а повседневное управление строится вокруг задач и причин.

011Выбираю назначение узла

Например: собирать телеметрию локально, а подпись выполнять только после свежего подтверждения узла и в аттестованном TEE-сервисе. Для каждого действия задаю обязательные свойства и поведение при потере связи.

012Вижу готовность до активации

Мастер подготовки и trust doctor показывают наличие устройств, прав и провайдеров. Подписанные политики, корни доверия и эталоны приходят из утверждённой поставки; регистрация аппаратного ключа — отдельный явный шаг.

013Включаю и получаю понятный результат

Проверяются загрузочные компоненты, ядро, модули и обязательная защита. При пробеле вижу конкретный компонент и затронутые возможности. В строгом профиле недоказанное обязательное свойство закрывает соответствующий допуск.

014Запускаю обычную задачу

PLATX запрашивает свежие доказательства и ограниченное право для нужного ресурса. Мне не приходится вручную передавать TPM Quote между модулями или вставлять закрытый ключ в конфигурацию задачи.

015Вижу исполнение и отклонения

SelfProtect удерживает правила; runtime-контроль отслеживает объекты в охвате; свидетели сравнивают показания. Если обязательное состояние меняется, связанные права прекращают действовать согласно политике.

016Получаю проверяемый результат

В карточке есть причина решения, действие, результат, область проверки и пакет доказательств. Его можно передать уполномоченному проверяющему и проверить независимо от интерфейса исходного узла.

017Разрешить задаче подписать результат»

В выбранном режиме ключ остаётся у защищённого подписанта. Исполнитель получает право на конкретную операцию. Сервис проверяет и обычные права доступа, и привязку к доверенному состоянию.

Пароли, токены и сертификаты управляются через существующий PXSECRETS: хранение, выдача, обновление, отзыв и восстановление. Для неэкспортируемого ключа выдаётся операция; для разрешённой выдачи секрета — ограниченный grant. Уже полученную копию секрета нельзя «забрать обратно» одним отзывом допуска.

018Известная загрузка

Проверяются подпись, разрешённый подписант, версия и измерения загруженных компонентов. Видно, покрыты ли initrd и параметры запуска. Для модулей различаются проверка файла и фактически загруженный объект. TPM Quote подтверждается настоящей аппаратной подписью и свежим challenge.

019Действующие запреты

Политика защищает процессы, конфигурацию, IPC, файлы и средства контроля, включая выбранные активы пользователя. У каждого правила видны объект, владелец и результат применения. Обнаружение события, запрет операции и ограничение видимости показываются раздельно.

020Ограниченная власть

Чувствительные функции вынесены в выбранные защищённые службы. Домены получают только необходимые память, IPC и устройства; права на DMA учитываются отдельно. Компрометация плагина или UI не выдаёт ему полномочия подписанта. Квоты и резерв критических ресурсов ограничивают последствия перегрузки.

021Наблюдение с охватом

В Linux-профиле IMA измеряет выбранные объекты, а appraisal проверяет и ограничивает их использование там, где включён. Журналы связываются с измерениями TPM. Наблюдения процесса, ядра и независимого HV/VMI-источника сопоставляются с учётом времени и общих зависимостей. Расхождения и слепые зоны сохраняются.

022Помощь в разборе

AI объясняет события, связывает признаки и предлагает изменения правил. Кандидат проходит проверку, воспроизведение на данных, наблюдение и ограниченное внедрение перед утверждением. Решения на критическом пути детерминированы; доступ к корневым ключам и самостоятельное ослабление защиты модели не предоставляются.

023Central недоступен

Локальные правила продолжают действовать. Офлайн-разрешения ограничены заранее заданными сроком и областью; после истечения зависимые действия закрываются. Локальный журнал хранится в своих пределах, а неподтверждённая внешняя фиксация отмечается явно.

024Сервис или датчик перестал отвечать

Независимый watchdog фиксирует отсутствие прогресса. Recovery выбирает допустимый переход, Lifecycle исполняет его с бюджетом перезапусков. Потерянный источник не подменяется старым зелёным результатом, а аварийное завершение процесса не объявляется стиранием ключей.

025Штатное обновление — часть защиты

Я заранее вижу, что изменится и какие задачи попадут в окно обслуживания. Новая версия получает доверие через утверждённый переход.

При потере питания остаётся прежнее допустимое состояние либо явный отказ с путём восстановления. Текущее состояние не становится эталоном автоматически. Возврат к старой поставке оформляется разрешённым подписанным переходом; защита от отката сохраняет смысл.

026HV/VMI — отдельная роль наблюдателя

platx_hv может давать дополнительный взгляд на выбранном стенде. Он не тождественен seL4 или TEE. Два источника с общим контролирующим хостом не становятся независимыми только из-за разных названий.

027Граница конфиденциальности видна заранее

Полное чтение памяти гостя со стороны хоста может конфликтовать с её защитой от этого хоста. Профиль указывает доступный обзор. Защита конфиденциальности также не обещает доступность при отключении питания или остановке машины владельцем хоста.

028Всё вызывается из PLATX

В Console — состояние, объяснение, план и результат. В CLI — те же операции и правила авторизации. Основной namespace для этого контура — trust ; секреты остаются в secrets .

029Проверить пакет внутри PLATX

Историческая проверка устанавливает достоверность сохранённых данных; нового допуска она не выдаёт.

Примеры используют каталог установленной Linux-поставки. ASSESSMENT_ID, BINDING_ID и остальные ID берутся из предыдущих результатов. Это целевая грамматика завершённого Trust Blue V1.

Изменения проходят через план: --dry-run показывает эффект; --apply --request-id=UUID выполняет разрешённое изменение. Где нужен контроль версии, передаётся --expect-generation=N . Подготовка ключа, регистрация, установка политик и эталонов, активация и отзыв имеют штатные команды и квитанции.

Отдельный trustctl нужен только там, где удобен переносимый проверяющий без запуска всей платформы. Он использует тот же verifier. Оператор узла проверяет пакеты через trust boot verify и trust incident verify . Для внешнего свежего подтверждения проверяющий выдаёт одноразовый challenge и проверяет ответ в режиме fresh .

030Что произошло, чему я ещё доверяю и что уже сделано?» — в одной истории.

Видно, какое свойство потеряно, какая задача зависит от него и почему закрыт доступ. При разрешённой частичной работе инцидент не требует останавливать всё без разбора.

SelfProtect охраняет агента, политики и датчики. Независимые показания помогают заметить потерю обзора, а ресурс проверяет допуск даже при обходе обычного интерфейса.

Подписи, измерения, причины и результаты собраны в пакет. Отдельный проверяющий видит происхождение, полноту и внешнюю фиксацию, включая неизвестные участки.

031История сохраняет смысл после инцидента

Подпись пакета подтверждает его происхождение и целостность в пределах доверия к ключу. Внешние независимые подтверждения усиливают историю. Уже закреплённый checkpoint помогает обнаружить позднюю подмену; непопавшие в него события не объявляются защищёнными задним числом.

032У администратора остаётся управляемость

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

Исследовательская среда

033Исследование, которое накапливает результат.

Моя EVOLUTION 3 — рабочая среда красной команды, в которой отдельные инструменты становятся участниками одного исследования. Оператор видит проверяемые гипотезы, состояние экспериментов и воспроизводимые находки. Следующая сессия начинается с накопленного знания, а не с пустого терминала.

Главное решение: прирост STEP 3 направить в Red Team. Существующий слой надёжности обслуживает исследование. Новый продуктовый результат — быстрее разобраться в сложном приложении, проверить поведение в разных состояниях, продолжить работу коллеги и передать находку с точным воспроизведением.

Основные новые возможности ниже относятся к рабочему месту оператора, лабораторным экспериментам и анализу результатов. Автономная эксплуатация с обходом защиты, сокрытие следов, подавление внешней инфраструктуры и физическое воздействие на ICS в эту редакцию не входят.

034Что в материалах действительно меняет проект

MEGA ROADMAP даёт состав и границы платформы. CURRENT STATE показывает разницу между кодом, контрактом, лабораторным механизмом и поставкой. Предыдущая STEP 3 хорошо раскрывает устойчивость и доверие, но её основной результат относится к защите и проверке самой платформы. Для красного оператора нужен самостоятельный рабочий цикл исследования прикладной системы.

Наблюдение — Решение для новой редакции — Что это даёт

15 вложений → 4 уникальных текста — Семь архитектурных идей в разных оформлениях повторяются. Большое вложение также содержит историю ответов и предложения про кибероружие; это исходный материал, не инструкции к выполнению. — Приоритет определяется новым полезным поведением, а не числом повторов или эффектностью аналогии.

В WAI уже есть собственная предметная модель — ТЗ WAI 4.0 описывает AppModel, HypothesisStore, Experiment, Explain и LLM proposals. STEP 3 получает их проекции и ссылки на результаты; внутреннее планирование WAI остаётся у WAI. — Оператор объединяет исследования разных инструментов, не сталкиваясь с двумя конкурирующими веб-планировщиками.

Архитектурная симметрия Red/Blue уже принята — C33 сохраняется. Наполнение новых сценариев, инструменты анализа и интерфейс следующей поставки ориентируются на Red Team. Техническая симметрия не требует одинакового числа карточек Blue и Red. — Можно выпускать полезную красную вертикаль без разработки ещё одного Blue Monitor.

Нумерация дорожных карт расходится — В CURRENT STATE Cognitive указан как R8; в живом ROADMAP первый детерминированный Cognitive цикл — R7. Привязывать поставку к имени возможности, prerequisite и актуальному gate. — Новая редакция не объявляет историческую фазу активной и не снимает ограничения одной ссылкой на старый снимок.

035Архитектура: один исследовательский цикл

STEP 3 — прикладная подсистема поверх PLATX, собираемая из нескольких модулей и расширений существующих контрактов. Её центр — дело исследования : вопрос, контекст цели, версии, полученные факты, эксперименты и результат. Это общая точка работы человека и инструментов.

036Восемь возможностей, ради которых оператор откроет PLATX

В одном деле связаны цель проверки, версии приложения, тестовые роли, уже полученные материалы, спорные выводы и открытые вопросы. Результат WAI можно сопоставить с наблюдением браузера или лабораторным журналом, сохранив происхождение каждого утверждения.

Оператор получает: точку продолжения работы через неделю и понятную передачу коллеге. Видно, что известно о конкретном состоянии приложения, а что только предположено.

Представление в Console / Operations Workspace; индекс и связи через публичную проекцию SGrap, артефакты через существующее хранилище evidence. Адаптеры публикуют snapshot/reference, не переписывают внутреннюю AppModel WAI. Новый графовый движок и отдельное универсальное хранилище не нужны.

Первый результат: импорт двух локальных пакетов исследования с различными версиями цели; интерфейс связывает общие объекты и сохраняет различия.

Конкурирующие объяснения на одном экране

AI разбирает доступные материалы: какие факты подтверждают гипотезу, какие ей противоречат, какого наблюдения не хватает. Оператор сравнивает объяснения и выбирает готовый лабораторный эксперимент. Осмысленный отрицательный результат остаётся полезным знанием.

Оператор получает: объяснение следующего исследования и возможность оспорить вывод модели, а не ещё один непрозрачный числовой score.

Расширение Cognitive THK и UI-проекция: ссылки на факты, альтернативы, ожидаемое различающее наблюдение. Локальные веб-гипотезы и выбор экспериментов WAI принадлежат его существующему контуру. Платформенный помощник анализирует межинструментальный контекст; автоматический выбор векторов проникновения здесь не проектируется.

Первый результат: на сохранённой лабораторной трассе показать два объяснения и достаточный контрольный эксперимент из каталога. Вариант без LLM должен давать пригодный для работы интерфейс.

Предметом проверки становится поведение приложения в нескольких состояниях. Например: одинаково ли работает отзыв тестовой роли в двух представлениях синтетического объекта; меняется ли результат после завершения фоновой задачи; воспроизводится ли расхождение на другой сборке.

Оператор получает: исследование прикладной логики, которую нельзя описать одним URL и одним ответом сервера.

Первый результат: две тестовые роли, две версии приложения, несколько заданных переходов; сравнительная таблица ожидаемого и наблюдаемого поведения.

Развилка «а если изменить одно условие?»

Оператор фиксирует исходное лабораторное состояние и запускает сравнимые ветки: исходная и исправленная сборка, другой тестовый пользователь, другая задержка фонового процесса. PLATX показывает различия результата и контекста, а не только два независимых отчёта.

Оператор получает: возможность отличить устойчивую причину от случайного совпадения и сохранить неудачную ветку как часть исследования.

Первый результат: парный A/B-эксперимент с одной объявленной разницей. При отличающейся начальной конфигурации сравнение получает статус несопоставимого.

Пакет содержит вопрос, необходимые условия, минимальную лабораторную последовательность, ожидаемый и фактический результат, версию цели и материалы проверки. Другой оператор может разобрать его без автора; после исправления тот же пакет превращается в регрессионную проверку.

Оператор получает: результат, который проще защитить на разборе и передать разработчику. Рутинная сборка отчёта становится частью рабочего цикла.

Версионированный формат поверх FORENSIC и адаптеров WAI; export/import, viewer, offline replay и отдельный запуск на подготовленном стенде. Минимизация удаляет лишние шаги только в воспроизводимой лабораторной копии, сохраняя исходную трассу и связь с ней.

Первый результат: коллега на чистом стенде получает тот же наблюдаемый исход; на исправленной версии — иной; на несовместимой — понятную причину невозможности повторить.

Разделение труда, которое видно оператору

Одна ячейка разбирает предоставленные материалы, другая сравнивает версии, третья проверяет воспроизводимость на копии стенда, четвёртая готовит пакет находки. Общая доска показывает владельца задания, входные материалы, зависимости и результат.

Оператор получает: параллельную подготовку исследования без ручного склеивания четырёх чатов и повторного разбора одного файла.

Задания анализа — ограниченные CHILD workers через существующий task API. Планирование лабораторного исполнения остаётся у mission / Action Coordinator. Координационные записи идут через действующие платформенные контракты; для межузлового варианта требуется квалифицированная интеграция транспорта. Число исследователей не является числом независимых источников доказательств.

Первый результат: три локальные ячейки на фиксированном наборе артефактов; повторно доставленное задание не создаёт второй результат под видом отдельного исследования.

Видеть, где исследование действительно закончилось

Карта показывает исследованные роли и состояния, нерассмотренные ветки, неподдержанные проверки, устаревшие выводы и противоречия. Из неё видно, куда разумно направить время человека. Отсутствие находок не превращается в полный зелёный экран.

Оператор получает: честный остаток работы и возможность обосновать глубину обследования, даже если бюджет закончился.

Проекция WAI coverage и межинструментальных наблюдений в Workspace. Отдельные измерения: наблюдаемая поверхность, известные тестовые состояния, выполненные эксперименты, закрытые вопросы. Знаменатель каждого показателя и эпоха модели обязательны; процент от неизвестной полной поверхности не вычисляется.

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

Каждая сессия оставляет следующий эксперимент

Завершённое исследование порождает обезличенный лабораторный случай: полезный вопрос, причинную структуру, минимальные предпосылки и контрольные исходы. ARENA использует такие случаи для новых задач, а оператор — для повторной проверки знакомого класса поведения.

Оператор получает: собственную накопленную методику исследования. Ценность платформы растёт с качеством подтверждённых случаев, а не с объёмом журналов.

Typed lessons в существующей MEM-поверхности; доменный каталог сценариев ARENA и версионированные пакеты. Общая библиотека пополняется после завершения сессии и проверки редактором. Учебные и контрольные случаи разделяются по семействам; скрытые условия Judge не попадают в память участника активной сессии.

Первый результат: один завершённый случай переносится в новую синтетическую среду с другими именами и данными; механическое совпадение строк не считается пониманием класса проблемы.

037Как выглядит один законченный красный цикл

Пример — исследование согласованности прав в лабораторном приложении с синтетическими документами. Оператор выбирает тестовые роли и формулирует вопрос о поведении после изменения роли. WAI и стенд дают наблюдения; Workspace связывает их с версиями приложения. Затем оператор сравнивает исходную и исправленную сборки и передаёт воспроизводимый случай.

Поставить вопрос. В деле зафиксированы ожидаемое поведение, две тестовые роли, версия приложения и разрешённые лабораторные сценарии.

Повторить на паре сборок. Ветка A воспроизводит расхождение, ветка B после исправления — нет. Если одновременно изменились и другие существенные условия, причинный вывод остаётся ограниченным.

Передать результат. Минимальная воспроизводимая последовательность, снимок условий, наблюдения, ограничения и сравнение сборок становятся одним пакетом.

Одно дело — три результата повторной проверки

Кнопки показывают подготовленные примеры статусов. Они не обращаются к приложению, WAI или сети.

На учебной сборке lab-A, с теми же синтетическими объектами и тестовыми ролями, наблюдение совпало с пакетом находки. Это подтверждение воспроизведения в указанных условиях.

038В виде чего внедрять

На уровне продукта — одна прикладная подсистема исследования. На уровне кода — расширение существующих владельцев и несколько адаптеров. Не требуется отдельное ядро для красных, второй криптографический стек или ещё один универсальный AI-планировщик. Названия сущностей ниже — проектная схема, не объявление существующего ABI.

Часть — Где размещается — Что делает и чем не владеет

Рабочая среда оператора — Модуль Console / Operations Workspace; сначала возможен локальный viewer. — Показывает дело, версии, гипотезы, сравнение и задания. Не пишет состояние исполнения миссии напрямую.

Контекст исследования — Проекция SGrap + ссылки на существующий evidence store. — Связывает межинструментальные данные. Внутренний граф приложения WAI остаётся доменным источником.

AI-разбор материалов — Cognitive THK; опциональная LLM в CHILD после действующего gate. — Объясняет, группирует и предлагает лабораторные проверки из каталога. Не подтверждает находку своим текстом.

Веб-проверка — Адаптер WAI как дочернего инструмента. — Переводит входной контекст и результаты между DTO. RequestBroker и внутренний бюджет WAI сохраняют одного владельца.

Исполнение сценария — mission / MSX / DSL / Action Coordinator. — Ведёт исполнение и mission_state. Workspace хранит лишь проекцию; перезапуск клиента не запускает сценарий заново.

Ветвление и replay — Расширение Mirage + providers лабораторного стенда. — Создаёт копии синтетической среды и сравнивает результаты. Не обещает снимок и откат произвольного реального процесса.

Пакеты находок — FORENSIC / MEM + формат экспорта. — Сохраняет происхождение, исходную трассу и производные версии. Это рабочий продукт для Red Team и разработчика.

Ячейки анализа — CHILD workers / действующий task API. — Обрабатывают назначенные артефакты и лабораторные задания. Общая доска не становится новым платформенным task scheduler.

Режим ARENA — Scenario/content package + существующие Controller / Judge роли. — Добавляет исследовательские задачи, сравнимые миры и разбор. Ground truth поступает в разбор только после завершения сессии.

Граф исследования не является графом автоматически исполняемой атаки. Схема описывает вопросы, эксперименты и материалы. Новая версия контекста дела не меняет принятую immutable mission_spec; изменение исполняемой задачи оформляется отдельным согласованным планом в рамках действующих контрактов.

039Детальное ТЗ: 18 проверяемых результатов

ID / функция — Результат реализации — Приёмка

E3-R01 · Дело исследования — Workspace: создать, сохранить, открыть и передать дело с версиями контекста и ссылками на материалы. — После закрытия приложения коллега восстанавливает вопрос, контекст, открытые задачи и исходные ссылки без устного пояснения автора.

E3-R02 · Межинструментальная проекция — SGrap/adapter: импортировать не менее двух типов локальных пакетов, сохранив source model и provenance. — Два представления одного факта связываются; разные версии цели не сливаются в один актуальный факт.

E3-R03 · Конкурирующие объяснения — Cognitive/UI: показывать поддерживающие и опровергающие материалы, пробелы и допустимый контрольный эксперимент. — На размеченном случае объяснение с противоречащими данными остаётся спорным. Отсутствующий факт не дорисовывается моделью.

E3-R04 · Состояния и роли — WAI/ARENA: сравнивать результат заданного сценария по role × state × target revision. — Матрица из двух ролей и двух версий показывает все выполненные клетки и явно перечисляет невыполненные.

E3-R05 · Исследование времени — Стенд: фиксировать заданный порядок событий и состояние фоновой задачи. — Повтор последовательности сопоставляет порядок, а не только wall-clock timestamps; неизвестный порядок отмечается отдельно.

E3-R06 · Ветки A/B — Mirage/replay: создать дочерние эксперименты с общим исходным manifest и перечислением различий. — Одна изменённая переменная видна в diff; дополнительное расхождение конфигурации отменяет заявление о контролируемом A/B.

E3-R07 · Минимальное воспроизведение — Lab replay: получать сокращённую последовательность с сохранением исходной. — На детерминированном корпусе сокращённый сценарий сохраняет наблюдаемый исход. Для нестабильного результата сохраняются повторы и ограничения.

E3-R08 · Пакет находки — FORENSIC/export: версия схемы, prerequisites, точные ссылки на артефакты, ожидаемое/фактическое, инструкция для стенда. — Импорт на чистой лабораторной среде проходит; изменение bytes артефакта обнаруживается. Импорт сам не запускает код.

E3-R09 · Повтор после исправления — Workspace/replay: связать исходный пакет, patched build и новый результат. — Различаются reproduced, not_reproduced, inconclusive и not_comparable; тайм-аут инструмента не трактуется как исправление.

E3-R10 · Доска исследователей — Workspace/task adapter: входы, зависимости, назначение, прогресс и результат ячеек анализа. — Три локальные ячейки обрабатывают разные части дела. Повтор сообщения не увеличивает счётчик выполненных независимых работ.

E3-R11 · Продолжение работы — Workspace + mission projection: checkpoint исследования и синхронизация после переподключения. — После отключения UI доступен локальный разбор уже имеющихся материалов. При возвращении связи выполненный эксперимент не отправляется повторно автоматически.

E3-R12 · Карта покрытия — Projection: знаменатели по наблюдаемой модели, роли, состоянию и версии; остаток вопросов. — Новая роль расширяет карту Unknown. Неподдержанный provider не создаёт зелёную клетку «проверено».

E3-R13 · Отрицательное знание — MEM: записывать опровергнутое объяснение вместе с условиями применимости. — Повтор идентичного анализа использует прошлый вывод; новая версия цели помечает старый вывод как требующий пересмотра.

E3-R14 · Библиотека случаев — ARENA/content: обезличенные семейства лабораторных задач, vulnerable/patched/control варианты. — Один случай переносится в новую синтетическую среду. Участник контрольного прогона не получает скрытый ответ или его производные заранее.

E3-R15 · Качество AI — Cognitive evaluation: корпус, версия модели, фиксированные входы, сравнение с вариантом без LLM. — Измерены ошибки ссылок на факты, полезность объяснений, время человека и стоимость. Рост объёма текста не считается улучшением.

E3-R16 · Качество Red Team результата — Product evaluation: протокол измерений и эталонный ручной рабочий процесс. — На одном корпусе сравниваются время до передаваемого пакета, доля воспроизводимых случаев, ошибки и число ручных переносов данных.

E3-R17 · Адаптеры без дублирования — Integration: контракты WAI↔Workspace, миссия↔исполнение, result↔evidence; owner каждого бюджета. — Нет второго владельца WAI AppModel и mission_state. Неподдержанная версия DTO диагностируется до потери данных.

E3-R18 · Контекст конкретной поставки — Release/profile owner: capability manifest, версии backend, среда и ссылка на свидетельства применимых MG-требований. — Viewer-only, local lab и distributed варианты имеют разные claims. Успех HTML-макета или unit-теста не квалифицирует RA2C, KCC, Mirage или всю платформу.

040Что взять из пяти военных аналогий

Исходная аналогия — Что имеет ценность для Red Team — Архитектурное решение

«Дроны-разведчики» — Цель, ограниченный бюджет, самостоятельный анализ и возврат собранного результата. — Ячейки исследования на назначенных материалах, частные объяснения и общий результат. Продолжение offline — разбор доступных артефактов и разрешённых локальных задач.

«Воздействие на процесс» — Изучение поведения системы во времени, причинной связи и прикладного результата. — Сценарии бизнес-логики и программная модель процесса в ARENA. Промышленные протоколы при необходимости — отдельные симуляторные пакеты; физические исполнительные устройства не являются целью этого ТЗ.

«ПВО / активная оборона» — Для выбранного направления это существующая инфраструктура и условия тестового мира. — Не включать как новое ядро STEP 3. Развитие honeypots, реагирования и firewall остаётся в своих очередях. Контратаки и перегрузка чужих каналов не проектируются.

Технологии из вложений: приоритет и предел обещания

Moving target / honeypots / «иммунитет» — Не добавлять в красную вертикаль; использовать при необходимости как параметры лабораторного мира. — Это развитие Blue. Утверждение об экспоненциальном росте стоимости атаки требует модели и измерений; одной смены портов недостаточно.

Семантический контроль — Использовать существующие типы, разделение фактов и гипотез, проверяемые предпосылки эксперимента. — LLM не даёт надёжного доступа к «намерению» компонента. Проверять можно объявленные и наблюдаемые свойства.

Mesh и разные среды изоляции — Учитывать в текущей платформенной архитектуре, не объявлять новый модуль Red Team. — Гетерогенность не означает, что противнику обязательно придётся взломать все реализации; многое зависит от потоков доверия и общих зависимостей.

ZKP / верифицируемые вычисления — Исследовательская опция для конкретного узкого предиката; не prerequisite MVP. — Доказательство вычисления не подтверждает достоверность входных наблюдений, состояние внешнего хоста и все реальные последствия действия.

Адаптивные контракты — Сохранить фиксированные полномочия принятой миссии; менять наблюдаемый статус и доступную производительность. — Деградация доступности не даёт агенту права незаметно переписать обещание, бюджет или срок mission_spec.

AI + формальная модель — Применять к малым моделям сценариев, валидности переходов и сопоставимости экспериментов. — Формально проверенное свойство модели не становится доказательством произвольной цели. Полезнее узкий доказанный контракт, чем заявление о верифицированном «мозге».

QKD / PQC / аппаратное высокое доверие — Оставить в отдельном исследовательском и инфраструктурном плане предыдущей STEP 3. — Эта красная вертикаль не требует смены криптографии или микроядра. Их полезность оценивается по конкретной угрозе и среде, а не по названию EVOLUTION.

Проверка обещаний: SelfProtect не гарантирует невозможность компрометации. Effect Firewall и число «семь ворот» сами по себе не доказывают отсутствие побочных эффектов. Удаление тестового объекта не возвращает автоматически физический процесс в исходное состояние. KCC-клиент в текущей reference-документации не подтверждает квалифицированный защищённый broker v2. Эти ограничения важны для точности архитектурного описания, а не как новые продуктовые функции.

041Контракты данных и состояний

Ниже — логическая схема для уточнения существующих публичных контрактов. Имена полей иллюстративны: wire format, размеры, версии и правила совместимости должны быть согласованы с действующим ABI. Отдельный общий JSON-обход контрактов не вводится.

Группа полей — Смысл и правило — Проверка

schema / record kind — Версия схемы и различимый тип: наблюдение, производный результат, гипотеза, предложение или квитанция действия. — Неизвестная обязательная семантика не интерпретируется по догадке; ошибка наблюдаема.

producer / owner — Источник, provider version, module / instance / generation; исходный субъект отделён от пересылающего. — Старая generation не получает права новой; пересылка не меняет происхождение.

object / property — Нормализованный объект, наблюдаемое свойство, способ измерения и область видимости. — Файл на диске, загруженный модуль и запись манифеста не сливаются в одно утверждение.

observed / received time — Время наблюдения и приёма, домен часов, boot/session epoch, интервал неопределённости. — Внешний timestamp не продлевает локальные полномочия; сообщения с несопоставимым временем не создают ложного конфликта.

sequence / parents — Идентификатор записи, порядок в потоке, ссылки на исходные записи и преобразования. — Дубли, перестановка, пропуск и пересказ различаются; цепочка родителей ограничена по размеру и глубине.

evidence reference — Адресуемое свидетельство, digest и параметры проверки происхождения; отсутствие материала у получателя отражается явно. — Подпись проверяет происхождение, а не истинность; удаление по retention не маскируется под целое доказательство.

independence / dependencies — Группа происхождения и отдельно общие зависимости: ОС, администратор, сенсор, время, аппаратная платформа. — Самозаявленная независимость имеет статус заявления до проверки; три пересказа не дают три подтверждения.

taint / access — Контролируемые внешним субъектом поля и ограничения доступа. Производная запись наследует применимые ограничения. — AI-пересказ и экспорт не снимают ограничения автоматически.

coverage / freshness — Границы измерения, свежесть и состояние видимости. Не наблюдалось и невозможно наблюдать — разные состояния. — Отключение датчика не превращается в отсутствие угрозы или в положительную оценку.

context — Конфигурация и профиль, session/test ID для синтетики, версия правила или модели для производных данных. — Результат лаборатории не становится фактом о production; прежняя оценка не переносится на новую сборку без основания.

Полный, ограниченный, карантинный и восстановительный режимы описывают разрешённое поведение профиля. Они не заменяют состояния Lifecycle. Для каждого перехода задать причину, владельца решения, сохранённые функции, отозванные полномочия и условие возврата.

Правило исполнения: типизированное предложение → проверка плана → локальная авторизация → исполнение → независимая проверка эффекта → квитанция и проекция. Ошибка на любом шаге имеет определённую семантику и путь завершения. Cleanup начатой операции остаётся обязанностью координатора даже после запрета новых действий.

042Функциональные требования и распределение ответственности

«Владелец» здесь означает архитектурную роль. Конкретный ответственный инженер, фазу ROADMAP, upstream/downstream и реализационные файлы назначают в пакете 01. Все перечисленные требования относятся к базовому STEP 3 в выбранном профиле; аппаратные варианты S3-10 и дополнительные исследования имеют отдельную применимость.

Зафиксировать объект развития и поставки

Описать защищаемые активы, категории данных, доверенных участников, угрозы, целевые ОС и оборудование, shipped profiles и исключения. Разделить product, lab и research.

Снять фактическое исходное состояние

Сопоставить документацию, манифесты сборки, публичные providers и результаты существующих gates. Для используемого пути указать «есть контракт», «включено в профиль», «поведение проверено» раздельно.

Описать контракт каждого расширения

Владелец: владелец подсистемы. Приёмка: все пункты C22 и MODULE_CONTRACT заполнены; отсутствующий provider даёт явную ошибку без скрытой подмены.

Задать пределы памяти, сообщений, очередей, времени, попыток, глубины происхождения и cleanup. Отделить build profile, принятую load-time policy и динамическое состояние доверия.

Владелец: профиль, Policy и resource ownership. Приёмка: отсутствующий или неверный обязательный предел блокирует допуск; проверки выполняются на границе лимита и за ней.

Разделить домены и доверенную часть

Определить небольшой доверенный контур и разместить сложные парсеры, интерфейсы и аналитические workers за выбранными границами. Для каждого домена составить перечень общих зависимостей и административных полномочий.

Владелец: архитектура размещения и isolation. Приёмка: стенд показывает ограничения доступа и последствия отказа; процессная граница не выдается за аппаратную.

Установить матрицу междоменных потоков

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

Владелец: Policy и владельцы обмена. Приёмка: разрешённый поток проходит; отсутствующий в матрице или нарушающий контракт отклоняется с наблюдаемой причиной.

Связать права с identity и generation

Разделить identities, ключи и права доменов; отзыв привязать к затронутым экземплярам. Повторный запуск не наследует допуск старой generation автоматически.

Владелец: identity, keyring, Policy. Приёмка: сообщение или запрос от отозванного экземпляра не получает прав нового; отзыв и повторный допуск оставляют свидетельства.

Сохранить локальный контроль при потере Central

Node остаётся владельцем локальной политики, ресурсов и effects. Для недоступного, устаревшего или некорректного Central определить разрешённое локальное поведение и условия возобновления обмена.

Владелец: Node Policy / SelfProtect, Fleet contract. Приёмка: потеря координатора не отключает заявленную локальную защиту и не продлевает административный допуск.

Завершать изолированные компоненты без остаточных прав

Определить quiesce/stop/destroy и очистку handles, подписок, leases, очередей и секретов по существующему owner. Предусмотреть отказ worker и потерю связи с ним.

Владелец: Lifecycle / isolation / resource ownership. Приёмка: после завершения и аварии нет незаявленных leftover; ошибка cleanup видна и препятствует ложному READY.

Подключать аппаратное доверие через ограниченный контракт

Для выбранного TPM/TEE/boot-варианта определить, что именно измеряется, чем проверяется, как связаны свежесть и поставка и что остаётся вне гарантии. Ожидания берутся из проверенной release-процедуры.

Владелец: trust provider и release; применимость условная. Приёмка: неверное, старое или отсутствующее свидетельство различается; неподдерживаемое оборудование не имитирует успешную аттестацию.

Версионировать и ограничить observation envelope

Уточнить обязательные поля из раздела 3, типы, ограничения длины и правила совместимости. Валидировать запись до её включения в общую картину.

Владелец: SENSE и владельцы публичной сериализации. Приёмка: malformed, oversized и неизвестная обязательная версия не дают частично доверенного объекта; отказ диагностируем.

Разделить время события, приёма и полномочий

Сохранять неопределённость и домен часов. Для TTL полномочий использовать утверждённую локальную семантику elapsed time; изменение wall clock и перезапуск имеют явные последствия.

Владелец: время/lease, SENSE, Policy. Приёмка: дрейф, скачок часов и задержка сообщения не продлевают допуск и не делают старое наблюдение свежим.

Сохранять происхождение и реальные зависимости

Разделить группы исходных данных и группы возможного общего отказа/компрометации. Сохранять parents и преобразования; дедупликация не удаляет значимую историю доставки.

Владелец: SGrap и observation providers. Приёмка: пересказ A→B не повышает число независимых оснований; изменение общей зависимости меняет объяснение доверия.

Типы наблюдения, детерминированного результата и AI-гипотезы должны отличаться в схеме, хранении и интерфейсе. Конфликт устанавливать только для сопоставимого свойства и времени.

Владелец: SGrap / Cognitive contract / Console. Приёмка: текст модели не записывается как evidence или verdict; конфликт не скрывается усреднением.

Отделить отсутствие события от отсутствия наблюдения, неподдерживаемого измерения и потери телеметрии. Передавать freshness и coverage до итоговой проекции.

Владелец: SENSE / Supervisor / Console. Приёмка: выключенный датчик, пропуск sequence и переполнение очереди отображаются как конкретный пробел видимости.

Сохранить ограничения на производные данные

Определить разрешённые представления для получателей, включая сводки AI, результаты поиска, экспорт и кэши. Агрегация не считается автоматическим понижением чувствительности.

Владелец: Policy, владельцы данных и Console. Приёмка: синтетический запрещённый материал не появляется у получателя через производный отчёт или диагностику.

Связать вывод с проверяемым свидетельством

Сохранять исходные материалы, digest, происхождение и цепочку преобразований по единому evidence-контракту. Внешнее якорение аудита использовать с определёнными гарантиями и политикой потери связи.

Владелец: Audit / FORENSIC / SGrap. Приёмка: обнаруживаются изменение и известный разрыв цепочки; отсутствие внешнего якоря не маскируется под независимо подтверждённую историю.

Пропускать изменения через типизированный план

CLI, детерминированный THK и будущая LLM используют общий путь предложений и проверки. Недоверенный текст не превращается непосредственно в изменяющий вызов.

Повторно авторизовать каждый локальный эффект

Проверять текущие generation, policy, scope, срок и ресурсный бюджет непосредственно у владельца effect. Предварительное разрешение Central не заменяет локальное решение.

Владелец: Policy / SelfProtect / Action Coordinator. Приёмка: отзыв или изменение target между планированием и исполнением препятствует применению устаревшего разрешения.

Обработать повторы и частичное выполнение

Для операции определить идентичность, условия безопасного повторения, reconciliation и допустимую compensation. Не обещать общую атомарность между узлами при потере связи.

Подтверждать эффект независимо от исполнителя

Для изменяющего действия задать наблюдаемое postcondition и источник проверки с известной зависимостью от исполнителя. Успех миссии оценивается отдельно от квитанции действия.

Определить деградацию и повторный допуск

Для потери связи, данных, доверия и ресурсов описать сохранённые функции, запреты, уведомление и возврат. Watchdog сообщает факт, Recovery решает, Lifecycle выполняет переход.

Владелец: Policy / Recovery / Lifecycle. Приёмка: восстановление связи само по себе не возвращает отозванные права; проверяются актуальность политики и незавершённые операции.

Сделать обновление и восстановление проверяемыми

Связать артефакт, профиль, совместимость данных и разрешённый путь восстановления. Зафиксировать допустимые версии и прерывания; rollback не означает произвольный возврат к уязвимому выпуску.

Владелец: release / Recovery / владельцы данных. Приёмка: прерывание обновления даёт известное проверяемое состояние; потеря данных и необходимость ручного вмешательства отражены явно.

Ограничить аудит и backpressure без скрытых потерь

Разделить обязательные и необязательные записи, определить резерв ёмкости, очередь, политику заполнения и действия, зависящие от сохранения evidence. Зафиксировать поведение при исчерпании диска.

Владелец: Audit / storage / Policy. Приёмка: переполнение наблюдаемо; ни обязательная квитанция, ни сообщение о потере не исчезают молча; новые действия ограничиваются по профилю.

Разделить полномочия лабораторных ролей

Controller задаёт scope и permitted action sets, Red и Blue работают через собственные наблюдения, Judge оценивает исход. Матрица данных исключает передачу ground truth участникам до завершения сессии.

Владелец: контракт assessment и isolation. Приёмка: проверены запрещённые потоки между ролями; окончание отдельного плана не снимает информационные барьеры сессии.

Сделать критерии Judge проверяемыми

Задать независимые метрики и область доступного контрольного состояния. Разделить исход сессии, обнаружение защитой и достоверность измерения. Неизвестное состояние сохранять как неизвестное.

Владелец: Judge / verification. Приёмка: совпадающие самоотчёты не заменяют измерение; конфликт и недостаток данных дают соответствующий результат.

Обеспечить воспроизводимость и регрессионное сравнение

Хранить исходное состояние, сценарий, входы, версии и условия измерения. Перед повтором проверить восстановление стенда; для недетерминизма заранее определить метод сравнения.

Владелец: lab harness / verification. Приёмка: другой инженер воспроизводит заявленный эффект и границы вывода; сравнение до/после учитывает изменившуюся конфигурацию.

Сохранить работоспособность без модели

Первый цикл — детерминированный. Будущая LLM подключается после соответствующего gate, только как ограниченный CHILD; её отключение не снимает обязательную защиту и проверку.

Дать оператору объяснимое состояние

Показывать текущий режим, причину ограничения, свежесть, покрытие, конфликты, запрещённые действия и незавершённые операции. Обеспечить путь от вывода до доступных исходных свидетельств.

Владелец: Supervisor / Console. Приёмка: оператор различает отсутствие события и потерю видимости, самоотчёт и проверенный эффект, локальное состояние и устаревшую общую проекцию.

Собрать комплект происхождения поставки

Зафиксировать исходную ревизию, локальные изменения, compiler/toolchain, зависимости, исходные и бинарные hashes, конфигурацию профиля, схемы и правила обновления.

Владелец: release / supply chain. Приёмка: конкретный отчёт связан с конкретными байтами; неподписанная или несовместимая поставка не получает обычный допуск.

Свести стандарты с доказательствами

Владелец: compliance и владелец поставки. Приёмка: двусторонняя трассировка проверяема; EXTERNALLY_CONFIRMED опирается на внешний документ с границами оценки.

Выполнить поведенческие, интеграционные, fault-injection и endurance-проверки на каждой заявленной native-среде. Учитывать действующие MG-0…MG-7 и межплатформенное взаимодействие.

Интегрировать по действующему порядку

043Измеримые пределы профиля

Без целевого оборудования нельзя обоснованно назначить общий лимит задержки, памяти или время восстановления. Пакет 01 обязан дать численные значения и единицы измерения до испытания. Пустое значение — незакрытое требование, а не «без ограничений». Нижеследующие обозначения — параметры ТЗ, не уже существующие CLI-флаги.

Параметр — Что фиксируется — Как проверять

OBS_MAX / DEPTH_MAX — Максимальный размер наблюдения, число родителей и глубина происхождения. — Допустимая граница, превышение, цикл родителей и неподдержанный формат; ограниченное потребление памяти.

QUEUE_MAX / RATE_MAX — Ёмкость очередей, допустимая нагрузка, длительность всплеска, резерв обязательного аудита. — Стабильная нагрузка и всплеск; факт потери и backpressure доступны в проекции.

FRESH_MAX / CLOCK_UNCERTAINTY — Допустимый возраст и неопределённость по типу свидетельства и действия. — Граничный возраст, задержка, clock jump и reboot; старые данные не продлевают права.

ACTION_TTL / RETRY_MAX — Время разрешения, исполнения и допустимые попытки. — Истечение на каждой фазе, потеря квитанции, повторная доставка и отказ источника времени.

CLEANUP_BUDGET — Независимый предел завершения уже начатого эффекта и критерий ручного восстановления. — Остановка новых действий при незавершённой операции; отсутствие скрытых бесконечных cleanup.

OFFLINE_WINDOW / RPO / RTO — Разрешённая автономность, допустимая потеря данных и время восстановления по функции. — Замер от определённого события до проверенного восстановления; отдельно фиксировать потерянный интервал.

LATENCY / RESOURCE_BUDGET — Метод измерения, перцентили или худшая граница там, где она обоснована, RAM/CPU/disk по роли. — Целевая аппаратура и профиль нагрузки; среднее не подменяет хвост или верхнюю границу.

ENDURANCE — Продолжительность и нагрузка. В действующем ROADMAP для квалификационного контура указано от 12 часов; более строгий профиль задаёт больше. — Полный непрерывный прогон с контролем ресурсов, журналов и отказов; факт 12 часов сам по себе не закрывает остальные gates.

044Программа приёмки

Сценарии ниже — проект проверок. Для каждого подготовить начальное состояние, вход, ожидаемый результат, независимый источник измерения, cleanup и формат отчёта. Ни один из этих сценариев не был выполнен в рамках создания документа.

Испытание — Условия и ожидаемый результат — Требования

T02 / Некорректное наблюдение — Неизвестная версия, превышенный размер, неверная структура. Запись не становится доверенной; ресурсный предел соблюдён. — 04 , 11

T03 / Отзыв экземпляра — Права отозваны между планом и эффектом; источник перезапущен с новой generation. Старое разрешение не действует. — 07 , 19

T04 / Central недоступен — Сохраняется заявленная локальная защита. Новые права не выдаются по устаревшему состоянию; возврат связи проходит reconciliation. — 08 , 20 , 22

T05 / Ложное множество источников — Первичное сообщение и несколько пересказов дают одну группу происхождения. Независимость не увеличивается числом копий. — 13 , 14

T06 / Конфликт и время — Сопоставимые несовместимые показания дают конфликт; измерения до и после обновления дают историю. Clock jump не продлевает TTL. — 12 , 14

T07 / Потеря видимости — Остановленный источник, разрыв sequence и пропуск аудита видны как разные пробелы. Нет автоматического вывода об исправности. — 15 , 24 , 30

T08 / Производные данные — Ограниченный синтетический материал преобразуется в сводку и экспорт. Запрет передачи получателю сохраняется во всех предусмотренных путях. — 16 , 18

T09 / Квитанция и эффект — Самоотчёт об успехе без измерения остаётся неподтверждённым. Потерянная квитанция не запускает скрытое дублирование эффекта. — 20 , 21

T10 / Остановка и cleanup — Истёк TTL или остановлена сессия. Новые действия запрещены; начатые завершаются или восстанавливаются в рамках отдельного бюджета. — 04 , 09 , 26

T11 / Обновление прервано — Проверены точки прерывания, допустимая версия, состояние данных и повторный допуск. Восстановление подтверждено измерением. — 10 , 22 , 23 , 31

T12 / Связь и хранилище одновременно — Недоступен внешний получатель аудита и заполнено локальное хранилище. Ограничения и пробелы видны; потребление ресурсов ограничено. — 04 , 17 , 24

T13 / Барьеры сессии — Red/Blue не получают ground truth или reasoning другой стороны через результаты, кэши и ошибки. Controller не выставляет исход, Judge не управляет. — 25 , 26 , 27

T14 / Повтор другим инженером — По пакету свидетельств восстанавливаются условия и результат. Неполные материалы приводят к заявленному ограничению вывода. — 17 , 28 , 31

T15 / Модель выключена — Обязательные поведенческие gates проходят без модельного provider. Производные AI-данные не становятся обязательной авторизацией. — 14 , 18 , 29

Полнота матрицы оценивается по угрозам и заявленным свойствам; 16 строк не означают исчерпывающего покрытия. Для критических ветвей требуется обоснованный метод coverage и независимая проверка, согласованные с текущей программой качества.

045Комплект результата и определение готовности

Артефакт — Обязательное содержание — Проверяемая связь

D01 / Профиль STEP 3 — Границы, активы, среда, допущения, численные пределы, применимость. — S3-01/02/04 → конкретные source/binary/profile hashes.

D02 / Архитектурные решения — Доверенная часть, матрица потоков, размещение, общие зависимости, ownership и условия отказа. — S3-03/05…10 → публичные контракты и deployment evidence.

D04 / Восстановление и обслуживание — Режимы, повторный допуск, обновление, аудит, retention, RPO/RTO и runbook. — S3-22…24/30 → измеренные сценарии отказа.

D05 / Лабораторная оценка — Scope, роли, информационные барьеры, известная синтетика, метрики Judge, состояние до/после. — S3-25…29 → подписанный или иным образом проверяемый комплект происхождения результатов.

D01…D06 — идентификаторы результатов работ, а не созданные этой задачей файлы. Конкретное размещение определяется живой структурой docs и contracts; повторные реестры и дубли нормативных документов не нужны.

Когда третью ступень можно считать принятой

На выбранной поставке работает сквозной путь «изолированное наблюдение → проверенное происхождение → разрешённая проекция → локально авторизованный эффект → независимое подтверждение». Он сохраняет заявленные свойства при установленных отказах, ресурсных пределах и отключённой модели. Каждый обязательный пункт связан со свидетельством и reviewer; все незакрытые условия видны.

Сейчас подготовлены: объединённый документ, схемы, проект требований и программа приёмки. Реализация STEP 3, квалификация платформы и формальная сертификация этим документом не заявляются.

046Что даст наибольший результат

Первый текст в основном усиливает изоляцию и целостность. Второй смешивает практические технологии с исследовательскими гипотезами. Почти не раскрыты питание, ресурсные пределы, ошибочные данные, восстановление доверия, сбои обновлений и работа оператора. Именно эти зависимости часто ограничивают полезность всей защиты.

Фундаментальная идея: полномочия зависят от проверенных условий

По мере ухудшения связи, свежести данных или уверенности в состоянии узла система должна сокращать допустимые изменения. Сохранение наблюдения и журналирования может быть разрешено дольше, чем выдача новых прав или изменение конфигурации. Возврат полномочий требует повторной проверки.

Малый компонент проверяет полномочия и ограничения каждой операции обслуживания. AI не может изменить эти правила, выдать себе права или превратить текст из лога в административную команду.

Восстановление как отдельная функция

Проверенный образ, сохранные данные, процедура восстановления ключей и возможность подтвердить исправность после запуска. Отдельно проверяется, переживает ли этот путь отказ основного управления.

Ограниченный режим без внешних сервисов

Локальный просмотр состояния, приём разрешённой телеметрии и буфер аудита. Для каждого действия заранее определены срок автономного допуска и поведение после его истечения.

Доверие к данным и происхождению вывода

Рядом с выводом AI видны источники, их возраст, версия модели, противоречия и непроверенные предположения. Подпись подтверждает происхождение сообщения; правдивость содержания проверяется отдельно.

Проверять потерю связи одновременно с заполнением диска, сбой обновления вместе с перезапуском, просроченные полномочия вместе с неверными часами. Результат — воспроизводимый отчёт о сохранённых свойствах и потерях.

Подход соответствует общему направлению cyber resilience: предвидеть нарушения, выдерживать их, восстанавливаться и адаптироваться. Конкретный выбор пяти приоритетов — вывод этой записки. NIST SP 800-160 Vol. 2 Rev. 1 .

Обещание «работает в любых условиях» нужно заменить профилем эксплуатации: какие функции сохраняются, при каких ограничениях питания, времени, связи и доверия, на каком оборудовании и сколько длится восстановление. За пределами профиля требуется заранее определённое безопасное поведение.

047Что добавить фундаментального

Ниже — предлагаемые направления развития. Они используют известные инженерные принципы; новизна для PLATX должна заключаться в их согласованном применении и проверке. Названия не обозначают уже существующие модули.

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

Первый шаг: контракты для аудита, локального просмотра и изменения конфигурации. Проверка: система корректно сообщает о выходе за границы, вместо того чтобы показывать прежний статус «исправна».

Доверие с ограниченным сроком действия

Допуск зависит от свежести политики, отзыва ключей, состояния узла и качества времени. Утрата подтверждений не должна увеличивать права. Для действий разной важности допустимы разные сроки автономной работы. Это лучше единственного непрозрачного «балла доверия».

Первый шаг: явная матрица разрешений и причин отказа. Проверка: пропажа связи и откат часов не продлевают административный допуск. Для узла без достоверного времени требуется консервативный режим.

Малый компонент безопасности вне AI

Выделить простые проверяемые правила доступа и обслуживания в отдельный компонент. AI предлагает изменения с объяснением, а компонент решает, разрешены ли они. Текст из документа, вывода инструмента или лога считается данными и не получает полномочий команды.

Первый шаг: ограничить разрешённые типы операций и ресурсы. Проверка: при замене модели, ошибочном ответе или недоступности AI правила безопасности продолжают действовать.

Восстановление из независимого доверенного состояния

Первый шаг: провести полное восстановление тестового узла при недоступном основном сервисе управления. Проверка: восстановлены данные и идентичность; старые скомпрометированные полномочия не возвращены.

Происхождение каждого значимого вывода AI

Хранить связь вывода с источниками, версиями модели и правил, датой наблюдения и проверками. Различать наблюдение, интерпретацию и предположение. Несогласие независимых источников должно быть видимым результатом, а не скрываться усреднённым уверенным ответом.

Первый шаг: карточка объяснения для одного класса аналитических выводов. Проверка: другой специалист может восстановить ход анализа; неверный исходный факт удаётся проследить до источника.

Управление качеством при нехватке ресурсов

При снижении доступных CPU, памяти, питания или охлаждения сначала сокращать необязательную аналитическую нагрузку. Для диагностической аналитики можно оценить лёгкую модель или правила как резерв, отдельно проверив их качество. Аудит и ограничения доступа не должны зависеть от доступности большой модели.

Первый шаг: разделить обязательные и необязательные фоновые работы, задать ограниченные очереди. Проверка: измерить задержки и потери данных при нагрузке; резервный анализ явно обозначается как менее полный.

Явное представление неопределённости времени и результата

Различать «операция подтверждена», «не выполнена» и «результат неизвестен». Отделить последовательность локальных событий от достоверности календарного времени. После разрыва связи старая заявка не должна бездумно повторять уже совершённое изменение.

Первый шаг: учёт идентичности операций обслуживания и процедура сверки неопределённых исходов. Проверка: потерянный ответ, повторная доставка и изменение часов не создают скрытых повторных изменений.

Независимость резервов по причинам отказа

Строить карту общих зависимостей: питание, площадка, обновление, учётная запись администратора, ключ подписи, сервис идентификации, поставщик. Две копии с общей причиной отказа дают меньше защиты, чем кажется. Разнообразие оправдано там, где уменьшает конкретный общий риск.

Первый шаг: карта зависимостей восстановления и аудита. Проверка: отказ одной общей зависимости не уничтожает одновременно рабочую систему, резерв и возможность проверки журнала.

Доказуемое происхождение обновлений и моделей

Для исполняемого кода, конфигураций, весов модели и данных retrieval фиксировать происхождение, версию, результаты проверки и разрешение на выпуск. Ограничивать распространение изменений небольшими партиями. Подписанное обновление тоже может содержать ошибку.

Первый шаг: единый паспорт выпуска с зависимостями и проверками совместимости. Проверка: ошибочный релиз останавливается до массового распространения; восстановление использует разрешённую версию с совместимыми данными. SLSA описывает основу проверки происхождения программных артефактов.

Обслуживаемость и физическая среда как часть архитектуры

Питание, перегрев, влага, вибрация, износ накопителей и доступность запасных частей задают пределы программных гарантий. Требования к корпусу и электронике должны проверяться для выбранного оборудования. Для оператора нужны понятный локальный статус, краткая процедура восстановления и способ передать обслуживание другому человеку.

Первый шаг: паспорт аппаратной конфигурации и сценарии обслуживания. Проверка: практические испытания при заданных условиях и восстановление человеком, который не участвовал в разработке. Устойчивость к EMP или радиации нельзя вывести из программной архитектуры.

048Как система должна менять поведение

Это концептуальные режимы защитной инфраструктуры. Реальные переходы требуют анализа рисков конкретного сервиса. Связь, доверие, питание и состояние данных — независимые измерения: единая шкала «здоровья» может скрыть важный отказ.

Данные и допуски актуальны. Разрешены проверенные операции в пределах политики.

Есть дефицит ресурсов или связи. Сохраняются заранее выбранные функции; ограничения видны.

Недостаточно доверия для изменений. Доступны разрешённые просмотр и фиксация состояния.

Обычные операции приостановлены. Выполняется отдельная процедура с проверкой результата.

Разрешённый локальный просмотр и накопление телеметрии в пределах ёмкости хранилища.

Новые административные допуски и операции, для которых требуется свежая внешняя проверка.

Проверены актуальность политики, отзывы полномочий и результаты операций с неопределённым исходом. Очередь синхронизируется без скрытых повторов.

Иллюстрация предлагаемых правил, не симуляция и не результаты испытаний PLATX.

Три конфликта, которые нужно решить явно

Доступность и отзыв прав. Без связи нельзя узнать о только что отозванном допуске. Нужны ограниченный срок автономности и заранее выбранное поведение после его истечения.

Конфиденциальность и наблюдаемость. Защита памяти VM от гипервизора ограничивает возможности внешнего монитора читать ту же память.

Возврат версии и защита от отката. После неудачного обновления нужен разрешённый путь восстановления. Произвольный возврат к старой уязвимой версии не должен становиться таким путём.

049Какие идеи внедряем и где они живут

Для первого пакета я бы выбрал шесть направлений. Основная работа — расширить существующие части PLATX. Новую логическую подсистему имеет смысл выделить для свидетельств доверия к узлу; загрузка и восстановление всей машины потребуют компонентов вне процесса PLATX.

Подсистема — область ответственности

Например, «аудит» отвечает за фиксацию и проверку событий. Внутри могут быть несколько компонентов: приём записи, подпись, хранение, экспорт. Подсистема не обязана быть одним бинарником или одним процессом.

В PLATX это компонент с plat_module_descriptor_t , объявленными зависимостями и интерфейсами. Он подключается через общий Core ABI. Добавить каталог исходников недостаточно: нужны провайдер, потребитель, профиль сборки и проверка взаимодействия.

EMBEDDED работает внутри worker. CHILD — в отдельном процессе через ABI-прокси и sandbox. Отдельный каталог или динамическая библиотека не защищают от захвата процесса; процессная изоляция сама по себе не защищает от захвата ядра ОС.

Один интерфейс может иметь реализации для разных платформ. Например, сбор аппаратных свидетельств будет различаться между Linux и Windows. Профиль задаёт, какая реализация обязательна и какое поведение допустимо при её отсутствии.

Идея — Форма внедрения — Что меняется и где проверять

4. Проверяемое восстановление — Два уровня: существующие Recovery/Lifecycle для модулей и внешний путь восстановления машины. — Recovery определяет реакцию на отказ, Lifecycle выполняет смену состояния. Они не заменяются новым «self-heal manager». Для ситуации, когда ОС или сам PLATX не запускаются, необходим отдельный проверенный образ/процедура загрузочного восстановления. Проверка: восстановление тестового узла при недоступном основном управлении, затем проверка данных и повторный допуск.

Итоговая форма: общий пакет интеграции с распределённой реализацией

Эти шесть направлений можно планировать и принимать как один пакет устойчивости. В исполняемой системе они имеют разных владельцев: Core управляет жизнью и ресурсами; Policy — полномочиями; Audit — журналом; провайдеры — конкретными средствами проверки; загрузочная среда — восстановлением машины. Единого обязательного «Resilience-сервиса», от которого зависит всё, здесь не требуется.

Как это выглядит на примере потери связи

Существующий компонент связи сообщает состояние; событие используется для отображения и аудита.

Policy и действующие допуски определяют, какие операции ещё разрешены. Отсутствие свежего свидетельства не трактуется как новая авторизация.

Локальное наблюдение и журналирование сохраняются в пределах профиля. Необязательная аналитика может быть ограничена предусмотренными механизмами управления ресурсами.

Если нужен рестарт компонента, решение принимается существующим Recovery на основании его состояния; смену состояния выполняет Lifecycle. Callback события и Supervisor не перезапускают процессы.

После восстановления связи проверяются актуальность допусков и неопределённые результаты операций. Только затем возвращаются зависимые полномочия.

Это схема ответственности для диагностической и защитной инфраструктуры. Она не задаёт новый транспорт, протокол команд или функции вооружения.

Что именно означает «добавить модуль» в этом репозитории

Публичный versioned capability, обязательные зависимости, владелец данных и поведение при отказе.

create/start/health/quiesce/stop/destroy; owner приходит из Core; require/provide через ABI.

EMBEDDED либо CHILD в допустимых границах. Отсутствие обязательного провайдера не считается READY.

Сборка и включение в профиль, сквозной сценарий, проверка отказов и наблюдаемые свидетельства.

Для новой подсистемы свидетельств сначала понадобится этот контракт и конкретный потребитель результата. Приведённое название — описание предлагаемой ответственности, а не утверждение о наличии готового attestation -модуля. Количество модулей и размещение проверяющей стороны определяются выбранной моделью доверия.

Куда попадут более сложные технологии

PQC: расширение существующего криптографического провайдера и поддерживаемых протоколов, форматов и процедур смены ключей. Замена одной библиотеки не мигрирует автоматически все каналы и подписи.

TEE: отдельный вариант размещения выбранного сервиса с аппаратной поддержкой, изменением границы доверия и аттестацией. Обычный CHILD не становится TEE от смены настройки.

seL4: отдельная системная конфигурация или порт небольшого доверенного компонента. Это работа с основанием системы и драйверами, а не подключение обычного плагина к Linux.

Гипервизор: привилегированный системный компонент вне обычного модульного ABI. Его загрузка и пригодность как границы защиты требуют самостоятельной проверки. В снимке от 31.08 имеющийся задел описан как research prototype; актуальная готовность в этой работе не проверялась.

Формальная проверка: спецификации, инструменты доказательства и проверки выпуска. Преимущественно это часть разработки и приёмки; отдельный runtime-демон для этого не нужен.

Физическая устойчивость: конфигурация оборудования, питание, охлаждение и испытания выбранной поставки. Программный модуль может сообщать состояние и ограничивать нагрузку, но не заменяет аппаратные свойства.

Продолжение: высокое доверие, стандарты и объединение наблюдений из изолированных компонентов .

050DMZ как система границ доверия

Да, часть технологий можно использовать для защитного пояса. Сильная постановка задачи — заранее считать внешний компонент захваченным и проверять, какие ограничения продолжают действовать.

DMZ обычно обозначает сетевой сегмент между внешними системами и внутренними ресурсами. VM, микроядро, TEE и аппаратная защита памяти решают другие задачи изоляции. Их можно сочетать, но принадлежность к «внутренней зоне» не создаёт автоматического доверия. Это согласуется с NIST Zero Trust Architecture .

Раздел дополняет разбор нового текста пользователя. Область — защитная инфраструктура, импорт недоверенных данных и разрешённые испытания. Рабочая DMZ и лаборатория исследования вредоносного кода рассматриваются как разные среды. Проектирование инфраструктуры развёртывания буткитов, скрытых имплантов и проведения атак сюда не входит.

Если из DMZ разрешён вызов доверенного сервиса, именно этот интерфейс становится границей атаки. Подпись и шифрование могут подтвердить отправителя и целостность сообщения, но запрос всё равно может содержать недопустимую операцию, ошибочные данные или попытку использовать чужие полномочия. Нужны ограничения смысла запроса, объёма данных и доступных действий.

Разбор файлов, протоколов и ответов внешних сервисов. Содержимое изначально недоверенное; доступ к внутренним секретам отсутствует по выбранной политике.

Передаёт допустимое представление данных и проверяет ограничения. Сам остаётся частью критичного кода и требует отдельной проверки.

Policy и ограниченные операции с ключами. Проверяют каждый запрос; не принимают решение AI или метку «из DMZ» за разрешение.

Сохраняет разрешённые свидетельства за отдельной границей отказа. Не является новым каналом команд и не подтверждает автоматически правдивость входа.

Это распределение ответственности, а не готовая топология и не требование разместить всё в четырёх VM. Размещение на одном хосте или разных машинах меняет допущения и стоимость. Функции продолжают использовать существующие контракты PLATX.

Что в новом тексте следует исправить

«При выходе из VM нарушитель остаётся в DMZ». Выход за границу виртуализации может затронуть хост. Если критичные сервисы зависят от того же хоста, их защищённость требует дополнительного обоснования.

«Пересоздание гостя очищает систему». Оно может удалить состояние гостя, но не устраняет компрометацию хоста, firmware, учётной записи управления или ключа подписи образов.

«Гипервизор видит все системные вызовы». Сам факт виртуализации этого не обеспечивает: многие операции гостя исполняются без соответствующего VM-exit. Полнота наблюдения зависит от конкретного механизма.

«TEE и полная инспекция памяти складываются». Если TEE скрывает память от хоста, внешняя инспекция этой памяти ограничивается. Требуется выбрать, кто кому доверяет и какие свидетельства доступны.

«seL4 автоматически превращает KVM/Xen/Hyper-V в одну платформу». Это разные системные основания. Их совместимость и полнота доказательств не следуют из перечисления технологий.

«Любые испытания опасного кода проходят без риска». Для исследования malware требуется отдельная лабораторная область без производственных данных и зависимостей. Ни один контейнер или гипервизор не даёт универсального обещания нулевого риска.

Границы технологий: допущения seL4 модель изоляции Firecracker модель угроз TDX .

Семь направлений, способных выделить PLATX

Первые пять можно исследовать на обычной серверной платформе. Шестое требует специальной аппаратной базы. Седьмое объединяет границы в проверяемую модель. Часть идей развивает предыдущие предложения; утверждение о мировой уникальности или патентной новизне не делается.

Импорт через восстановление допустимого содержания

Принимать во внутренние сервисы только необходимое представление информации. Например, внешнее сообщение преобразуется в ограниченный набор полей; исходный архив или документ остаётся вне доверенной обработки. Неоднозначный формат не получает привилегию только потому, что успешно прошёл антивирусную проверку.

Форма: подсистема импорта с адаптерами форматов и компактными проверками на границе. Тяжёлые парсеры остаются в изолированной области. Проверяющий компонент не должен повторно разбирать весь опасный исходный формат внутри доверенной зоны.

Отличие для продукта: видны происхождение, исключённые элементы и ограничения преобразования. Проверка: повреждённый документ не порождает административную операцию. Предел: безопасный синтаксис не исключает ложь или prompt injection в обычном тексте; метка недоверенного происхождения сохраняется.

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

Форма: адаптер среды исполнения и политика запуска через имеющиеся Lifecycle/Recovery. MicroVM — один из возможных вариантов. Firecracker показывает подход к минимальной VM и нескольким границам изоляции; это пример технологии, не готовая рекомендация для всех конфигураций PLATX.

Отличие для продукта: результат связан с версией образа и идентичностью задания. Проверка: следующая задача не получает файлы и полномочия предыдущей. Предел: снимки VM могут сохранять секреты и старые идентичности; замена гостя не восстанавливает захваченный хост.

Критичный сервис без универсального доступа к секретам

Недоверенный компонент запрашивает строго определённую разрешённую операцию. Он не получает сырой ключ и не приобретает право подписывать произвольные данные. Сам сервис проверяет назначение операции и авторизацию независимо от вызывающего компонента.

Форма: узкий сервис за существующим криптографическим интерфейсом; при необходимости — отдельный аппаратный модуль или подходящая изолированная среда. Пример ограниченной среды — AWS Nitro Enclaves : отсутствуют прямой внешний сетевой доступ и постоянное хранилище. Это сервис AWS, а не переносимый локальный плагин.

Отличие для продукта: ключ защищён вместе с правилами его использования. Проверка: захваченный клиент не может превратить сервис в универсальный инструмент подписи. Предел: неизвлекаемый ключ сам по себе не предотвращает злоупотребление разрешённым интерфейсом; DoS остаётся отдельной задачей.

Аудит через однонаправленную границу

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

Форма: расширение Audit export и внешняя принимающая сторона. Если требуется физическая однонаправленность, оценивается аппаратный data diode. Программное правило направления не эквивалентно аппаратной границе; NIST определяет data diode как однонаправленную границу передачи .

Отличие для продукта: независимая копия свидетельств без обратного административного канала. Проверка: отказ приёмника проявляется как определённый пробел или ограничение. Предел: физическая однонаправленность исключает обычное обратное подтверждение доставки; нельзя обещать подтверждённое сохранение без отдельного решения. Экспорт также не должен раскрывать секреты.

Разделённое управление корнями доверия

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

Форма: развитие Policy и процесса выпуска с разделением ролей, проверкой требуемых подписей и отдельной процедурой восстановления. TUF — пример спецификации с ролями, offline root keys и порогом подписей для метаданных обновлений. Это не требует изобретать новую криптографию.

Отличие для продукта: один захваченный административный сеанс не должен молча заменить весь набор доверенных правил. Проверка: одного недостаточного набора подписей мало для принятия изменения. Предел: несколько ключей в одной учётной записи не дают независимости; потеря участников усложняет доступность и восстановление.

CHERI: аппаратные границы доступа к памяти

Это дополнительное исследовательское направление рядом с микроядром. CHERI расширяет архитектуру процессора возможностями защищённых указателей, ограничения доступа к памяти и разделения компонентов. Для проекта с C/C++-кодом это содержательнее обещания безопасности только за счёт перехода на открытую ISA. Cambridge / SRI: CHERI .

Форма: экспериментальная аппаратная конфигурация и перенос небольшого компонента, например валидатора данных. Понадобятся подходящие процессор, инструменты и программная среда. Это не устанавливаемая библиотека для обычного x86-сервера.

Отличие для продукта: часть границ памяти поддерживается аппаратурой. Проверка: оценить конкретные свойства, совместимость и стоимость порта. Предел: capabilities PLATX в ABI и аппаратные capabilities CHERI — разные механизмы. CHERI не исправляет ошибочную бизнес-политику и не означает устранение всех классов ошибок.

Для каждого разрешённого перехода между областями хранить не только схему сети, но и обещание: кто может обратиться, какие данные передать, какие полномочия получить, какие предположения нужны и как обнаруживается нарушение. В интерфейсе показывать, какие обещания подтверждены для текущей поставки, а какие потеряли основание.

Форма: развитие публичных контрактов, профилей, проверок выпуска и проекции в Supervisor. Статус строится по существующим реестрам и свидетельствам; новый всесильный контроллер и второй реестр полномочий не нужны. Для компактных границ часть свойств можно проверять формально.

Отличие для продукта: оператор видит конкретно сохранившиеся ограничения при отказе. Проверка: изменение конфигурации, нарушающее допущения доказательства, снимает соответствующее утверждение о защищённости. Предел: схема сама по себе не доказывает изоляцию; свойства отдельных компонентов не складываются автоматически в доказательство системы.

Наиболее сильная комбинация для ближайшего эксперимента

«На указанной конфигурации компрометация компонента приёма не дала ему дополнительных полномочий через проверенные интерфейсы; попытки выхода за разрешённые операции были отклонены и зафиксированы». Это ограниченное утверждение по результатам испытаний. Оно не превращается в «ядро невозможно взломать», не доказывает отсутствие неизвестных уязвимостей и не заменяет анализ общей аппаратной зависимости.

Особенность PLATX может состоять в единой проверяемой дисциплине границ: одинаковые правила происхождения данных, полномочий, жизненного цикла и свидетельств действуют от входного документа до обновления доверенной политики. Большинство отдельных технологий уже существует; ценность создаёт согласованность их применения и доказанный результат.

051Высшая секретность» — не один технический уровень

Какие сведения обрабатываются, кому разрешён доступ и между какими областями возможен обмен. Это определяется применимой системой классификации и владельцем сведений.

Какие свойства должны быть продемонстрированы, для какой угрозы, какими проверками и с какими допущениями. Здесь полезны high assurance guidance и Common Criteria.

Версии ПО и firmware, оборудование, установка, ключи, персонал, физические условия и регламент эксплуатации. Сертификат компонента не превращает автоматически весь комплекс в допущенную систему.

Сделать малую, явно определённую доверенную часть и отдельный профиль поставки с проверяемыми ограничениями обмена. Большие парсеры, аналитические компоненты и интерфейсы размещать за подходящими границами. Решение об обязательной внешней сертификации принимать после определения заказчика, сведений и конфигурации.

Нельзя отождествлять: EAL7 с высшей секретностью, FIPS 140-3 Level 4 с допуском всей платформы, NIST HIGH с грифом сведений или DO-178C Level A с защитой секретных данных. Это разные объекты и системы оценки. Полезно заимствовать инженерную дисциплину, но заявленная эквивалентность требует отдельного основания.

Например, NCSC публикует общие принципы high assurance , а требования схемы CAPS High Grade не опубликованы полностью в открытом доступе. Открытые принципы не заменяют прохождение этой схемы. Common Criteria и FIPS 140-3 имеют собственные границы оценки.

Что уже есть в проектной документации

052Что именно заложить в требования

Документ / статус — Зачем PLATX — Что требуется подготовить

NCSC High Assurance Рекомендации; добавить как инженерный ориентир. — Ясные защитные свойства и независимая проверяемость. — Перечень угроз, границы доверенного кода, остаточные ограничения и повторяемое независимое испытание заявлений. Уменьшать сложность критичных функций. Это не заявление CAPS High Grade. Источник .

NCSC Cross Domain, редакция руководства 2026 Рекомендации; наиболее прямое дополнение к идее DMZ. — Контроль потоков данных между областями разного доверия. — Для каждого потока описать необходимость, допустимые данные, проверки, преобразование, экспорт и наблюдаемость. Рассматривать полный путь данных, включая управляющие поля. Новые шесть принципов заменяют прежний набор из тринадцати; переход схем оценки описан отдельно. Источник .

CC:2022 / ISO/IEC 15408; CEM / ISO/IEC 18045 Критерии и методология оценки; выбрать объект оценки. — Связать требуемые функции безопасности со строгостью их оценки. — Определить TOE, Security Target, угрозы, допущения и требования. Сначала рассмотреть компактный объект: проверку междоменных запросов или изолированный сервис ключей. Внешние зависимости и ограничения нельзя скрывать уменьшением границы TOE. Целевой пакет assurance выбирается после этого. Источник .

NIST SP 800-53 Rev. 5, 800-53A Rev. 5, 800-53B Каталог мер, процедуры проверки и базовые наборы. — Системная матрица защиты, включая организационные и физические меры. — Выбрать меры, параметры и доказательства: доступ, потоки данных, аудит, изоляция, восстановление, поставка. Набор HIGH можно использовать как исходную точку после оценки воздействия и адаптации; это не классификация сведений. Зафиксировать актуальный набор данных и обновления: NIST публикует Release 5.2.0. Каталог и наборы ; процедуры оценки .

FIPS 140-3; применимая схема оценки криптомодулей Условно: зависит от рынка, заказчика и конфигурации. — Зафиксировать границу ключей и свойства криптографического модуля. — Выбрать проверенный модуль с подходящей областью сертификата, режимом работы и процедурами управления ключами. Уровни относятся к криптомодулю. ISO/IEC 19790:2025 существует отдельно; новую редакцию ISO нельзя автоматически подставлять вместо нормативной базы FIPS/CMVP. Российский криптографический маршрут определяется отдельно. FIPS ; ISO/IEC 19790:2025 .

ГОСТ Р 56939-2024 и NIST SSDF, SP 800-218 ГОСТ уже заложен; SSDF полезен для сопоставления процессов. — Безопасная разработка и поддержка доказательств после выпуска. — Одна трассируемая система требований, анализа, проверки изменений, поставки и обработки уязвимостей. Связать её с уже предложенными provenance/SLSA и проверяемыми обновлениями. Общая практика может подтверждать несколько требований, если соответствие обосновано. ГОСТ ; SSDF 1.1 .

NIST SP 800-160, 800-193, 800-207 Уже предложены в предыдущей записке; развить в конкретные проверки. — Киберустойчивость, восстановление firmware и доступ без неявного доверия. — Границы допустимой деградации, независимый путь восстановления и проверка доступа при изменении состояния. Это основа поведения, а не дополнительные фоновые демоны. 800-160 Vol. 2 Rev. 1 ; 800-193 ; 800-207 .

IEC 62443-3-3, 4-1, 4-2 Условно: при применении в промышленной автоматизации / OT. — Требования к системе, жизненному циклу продукта и компонентам. — Описать зоны и разрешённые связи, угрозы, целевые свойства системы и ответственность поставщика. Уровень SL не назначать автоматически по слову «военный». Для обычного серверного продукта сначала обосновать применимость. Обзор IEC .

Сначала определить категорию сведений, назначение комплекса и применимую схему оценки. Отдельно исследовать требования ФСТЭК, криптографические требования ФСБ, условия установки и эксплуатации. Текущая документация ссылается на приказ ФСТЭК № 76 от 02.06.2020; его полная действующая редакция и применимость к предполагаемой поставке этой проверкой по официальному источнику не подтверждены.

На этом основании нельзя назначить PLATX конкретный уровень доверия или заявить готовность к гостайне. Набор документов согласуется для конкретного объекта с компетентной оценивающей стороной.

Что использовать только как контекст

Публичная стратегия NSA Raise the Bar показывает значимость отдельной программы оценки cross-domain решений. Открытая страница не является полным набором требований и не позволяет объявить соответствие RtB.

Физическая защита, персонал, управление носителями и побочные каналы также зависят от среды. MIL-STD-810/461, авиационные или отраслевые нормы включаются при реальной применимости; количество названий стандартов не повышает достигнутое доверие.

053Изолировать компоненты и координировать их вместе — да

Для защитного контура подходит федеративная организация: компоненты сохраняют собственные границы и локальные правила, а общая сторона координирует разрешённую работу и строит представление состояния. Объединение не должно требовать общего административного доступа ко всем внутренним ресурсам.

Формирует ограниченный запрос на диагностику или обслуживание.

Проверяет получателя, допустимость, актуальность и пределы запроса.

Использует существующий разрешённый путь исполнения и фиксирует результат.

Каждый сообщает собственное наблюдение, область видимости и происхождение.

Формат, полномочия на передачу, подлинность, свежесть и допустимое содержание.

Происхождение, связи, противоречия и пробелы сохраняются; AI может объяснять их.

Схемы задают роли, не новые API и не готовую топологию. Потоки команд и наблюдений логически разделены; реализация использует существующие ABI, I/O, аудит и реестры PLATX. Новый общий bus, registry или всесильный менеджер не предлагаются.

Изоляция должна охватывать управление

Общие администраторские учётные записи, ключи подписи, гипервизор, питание и хранилище могут оставаться общими причинами отказа. Отдельные VM на одном хосте дают определённые границы, но не полную аппаратную независимость.

Общая картина тоже является защищаемым ресурсом

Сторона агрегации должна быть допущена к получаемым данным. Для разных пользователей нужны разрешённые проекции. Объединённые сведения могут раскрывать больше исходных фрагментов; сводка AI не является автоматическим основанием снизить ограничения доступа или экспортировать результат.

054Как объединять наблюдения без ложной уверенности

Независимость исполнения, независимость источника и независимость административного управления — три разных свойства.

Один процесс может упасть, не остановив другой. При этом оба способны читать одинаковый ошибочный журнал.

Сведения получены разными путями. Их может объединять один захваченный хост или общий механизм измерения.

Нет общей критичной зависимости в рассматриваемом сценарии. Это нужно обосновать, а не вывести из числа контейнеров.

Сохранять происхождение и область наблюдения

Нужны идентичность источника и экземпляра, версия наблюдателя, объект и утверждаемое свойство, время наблюдения и его неопределённость, время приёма, последовательность событий, ссылка на исходное свидетельство, ограничения доступа и область видимости. Производные результаты сохраняют связь с исходными. Названия и размещение полей согласуются с существующим observation envelope; это не новый параллельный формат.

Не считать копии независимыми голосами

Если collector B переиздал отчёт агента A, это один исходный источник. В архитектурных документах PLATX уже упомянут independence_group . Он полезен для учёта общей зависимости, но сам по себе не доказывает независимость. Описание группы должно основываться на реальном пути получения данных.

Разделять наблюдение, вывод и гипотезу

«Датчик получил значение», «детерминированная проверка установила несоответствие» и «AI предполагает причину» — разные классы записей. Подпись доказывает происхождение утверждения, а не его истинность. Компрометированный датчик может подписывать ложные данные.

Сопоставлять одинаковые свойства в сопоставимое время

«Версия в манифесте поставки», «версия файла на диске» и «версия фактически работающего компонента» могут различаться. Это не три взаимозаменяемых измерения. Противоречие фиксируется после проверки объекта, времени, метода и области видимости; при недостатке оснований сохраняется неопределённость.

Не скрывать конфликт и потерю видимости

Общая проекция должна показывать согласованные и противоречивые сведения, устаревание, отсутствие наблюдений и невозможность наблюдать. Молчание отключённого датчика не означает отсутствие проблемы. Уверенность AI не заменяет покрытие и свежесть независимых свидетельств.

Связывать доступ с данными и их производными

Метки чувствительности и ограничения получателей должны сопровождать исходные и производные сведения. Междоменные передачи допускаются только по предусмотренным правилам. Проверка экспорта не должна зависеть от текста модели. Поиск, кэш, экспорт отчёта и объяснение AI также являются путями распространения информации.

Два сообщения — один исходный источник

A сообщает версию 7. B показывает ту же версию, полученную из отчёта A. Отдельное наблюдение C сообщает другое значение для того же свойства в сопоставимое время.

Картина: две группы происхождения и конфликт между ними. Автоматическое голосование «2 против 1» здесь вводит в заблуждение.

Синтетический пример для объяснения происхождения. Это не телеметрия PLATX, оценка вероятности ошибки или правило автоматического изменения системы.

Общий интерфейс должен показывать границы знания

Для каждой записи: кто сообщил, что именно наблюдал, когда, чем подтвердил, какие источники зависимы, кто может видеть результат и что остаётся неизвестным. Такой интерфейс существенно полезнее единого зелёного индикатора «всё безопасно».

055Какие требования закрепить первыми

Контроль информационных потоков. Разрешение зависит от источника, получателя, содержания и действующей политики; идентификация отправителя сама по себе недостаточна.

Разделение полномочий. Общая координация не получает неограниченного доступа к локальным ресурсам; изменение корней доверия имеет отдельные полномочия.

Происхождение и зависимости. Перепубликация события не увеличивает число независимых источников; преобразования сохраняют связь с первичными свидетельствами.

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

Свежесть и неопределённость. Просроченные данные, временные расхождения, конфликт и отсутствие видимости явно отражаются в общей картине.

Устойчивость границ. Недоступность Central, датчика или приёмника аудита не создаёт неявных новых полномочий и не маскируется состоянием READY.

Независимая проверяемость. Заявление об изоляции связано с угрозой, конфигурацией, методом проверки, результатом и явно записанными допущениями.

Управление изменениями. Новая сборка, политика или аппаратная конфигурация требует анализа того, какие прежние доказательства ещё действуют.

Сценарий — Проверяемое свойство — Наблюдаемый результат

Поддельное или повторно доставленное наблюдение — Подлинность и свежесть — Запись отклонена либо отмечена согласно контракту; не становится новым независимым подтверждением.

Два коллектора одного журнала — Учёт происхождения — Отображаются две доставки одного источника. Число независимых оснований не увеличивается.

Два сопоставимых источника расходятся — Сохранение противоречий — Оба свидетельства доступны уполномоченному получателю; конфликт не скрыт усреднением.

Отказ источника или дрейф часов — Границы видимости и времени — Свежесть и покрытие ухудшаются явно; отсутствие событий не трактуется как подтверждение исправности.

Запрещённый экспорт сводки — Ограничения на производные данные — Ни отчёт, ни ответ AI не обходят решение о доступе и допустимости передачи.

Недоступный или некорректный Central — Локальная авторизация — Node сохраняет предусмотренную локальную защиту и отвергает запросы вне полномочий.

Отказ общей аппаратной зависимости — Реальные пределы независимости — Проекция показывает затронутую группу источников; все её сообщения не выдаются за независимые свидетельства.

Четыре ближайших результата проектирования

1. Граница доверенной части и объекта оценки. Что защищаем, от кого и на какой конфигурации.

2. Матрица разрешённых междоменных потоков. Включая управление, аудит, кэши и обновления.

3. Уточнённый контракт наблюдения. Происхождение, независимость, время, видимость и доступ.

4. Матрица требований и доказательств. Документ → требование → владелец → проверка → свидетельство → незакрытый пробел.

После этих результатов можно предметно выбирать TEE, микроядро, отдельный хост или обычную процессную изоляцию для каждого компонента. Одинаково усиливать все компоненты не требуется: строгость защиты должна соответствовать их полномочиям и последствиям отказа.

056Как защитные механизмы работают на red team

В red team меняется вопрос: вместо «соблюдено ли наше правило?» исследователь формулирует «при каких разрешённых условиях это правило может оказаться ложным?». Изоляция, происхождение данных и независимые наблюдения нужны для достоверного ответа.

Механизм — Что даёт красной команде — Какой результат считать полезным

Изолированные домены / DMZ — Отдельные среды для разбора недоверенных материалов и разных испытаний; данные одного задания не смешиваются с другим. — Показаны реальные границы доступа, включая общие администраторские и инфраструктурные зависимости.

Контролируемый междоменный обмен — Проверка заявлений о том, какие данные могут перейти из одной зоны в другую. — Для синтетических данных известны ожидаемое решение, фактическое решение и подтверждающее наблюдение.

Локальная авторизация при общей координации — Проверка того, сохраняет ли участник ограничения при ошибочном запросе от координатора. — Участник исполняет разрешённое и отклоняет выходящее за согласованные полномочия; причина решения видна.

Независимые наблюдатели — Сопоставление заявления тестового инструмента с тем, что наблюдали проверяемый компонент и отдельный измеритель. — Есть подтверждённый эффект либо явно обозначено, каких данных недостаточно.

Происхождение данных — Отделение самостоятельной находки от пересказа результата другого анализатора. — Число отчётов не подменяет число независимых оснований.

Проверяемое состояние среды — Сравнение результатов для известных версий ПО, настроек и образов лаборатории. — Понятно, к какой конфигурации относится вывод и что требуется для повторения.

Ограничения на AI — Проверка устойчивости анализа к недоверенному содержимому и недоказанным выводам. — Текст из входных данных остаётся данными; гипотеза модели не получает полномочия или статус факта.

Восстановление и журнал событий — Повторяемые упражнения и сравнение поведения до и после исправления. — Восстановление проверено отдельно; сохранены свидетельства, необходимые для объяснения результата.

Общий гипервизор, общие административные учётные записи или общий источник данных могут связать формально изолированные компоненты. В проекте испытаний нужно указывать, какая независимость действительно обеспечена и какие зависимости остаются.

057Как это оформить в PLATX

Предлагаю отдельную подсистему испытаний с каталогом сценариев и адаптерами к лабораторным стендам. Она использует общие контракты наблюдений и отчётности PLATX. Полномочия исполнителя, наблюдателя и проверяющего разделяются.

Какое свойство проверяется, на какой конфигурации, что разрешено и какое наблюдение подтвердит или опровергнет гипотезу.

Подготовленная среда и согласованный сценарий. Отдельно фиксируются ход испытания и измерения его эффекта.

Сопоставление с критерием, выявление противоречий и объяснение границ вывода. Возврат результата владельцу защиты.

Хранит согласованный объём проверки, версии сценариев, ограничения и критерии завершения. Правила испытания задаются вне проверяемой AI-модели.

Каждый получает только необходимые тестовые данные. Разбор материалов и воспроизведение сценария могут требовать разных границ изоляции.

Контракты SENSE и SGrap концептуально подходят для фактов, связей и происхождения. Для них нужно проверить полноту данных именно в контексте испытания.

Не принимает самоотчёт инструмента за доказательство. Его критерии и свидетельства доступны для пересмотра; независимость от исполнителя обозначена явно.

058Шесть направлений, которые стоит развивать

Начинать со свойства: «производный отчёт сохраняет ограничения исходных данных» или «потеря Central не отменяет локальные запреты». Для каждого — условие проверки, наблюдаемый результат и пределы применимости.

Ценность: находка сразу связана с нарушенным требованием и возможным исправлением.

На синтетических данных проверять дубли, противоречия, разную свежесть и общие зависимости источников. Система должна показывать неопределённость, а не производить необоснованную уверенность.

Ценность: проверяется надёжность самой аналитики PLATX.

Red формулирует и проводит согласованную проверку. Blue представляет защитные наблюдения. Оценщик сверяет их с заранее определённым критерием и доступными свидетельствами.

Ценность: успех одной стороны не определяется её собственным отчётом.

Сохранять версию сценария, исходное состояние, входные данные и измерения. При сравнении учитывать изменившиеся зависимости, нагрузку и недетерминизм AI.

Ценность: можно показать, какой наблюдаемый эффект дало исправление; один успешный повтор не доказывает общую защищённость.

Оценивать, распознаёт ли AI недостаток данных, сохраняет ли ограничения доступа при составлении сводок и отличает ли содержимое документа от разрешённых инструкций.

Ценность: отдельно измеряется корректность анализа и корректность применения полномочий.

Исследовать, остаются ли защитные свойства наблюдаемыми при потере источника, задержке сообщений или недоступности координатора. Отказ датчика должен отражаться как пробел видимости.

Ценность: выясняется, где вывод перестаёт быть обоснованным в сложных условиях.

059Что означает «объединить всё в общую картину»

Объединять стоит свидетельства о конкретной проверке: её постановку, фактический ход, наблюдения и решение оценщика. Само действие, его обнаружение защитой и подтверждение эффекта — разные события.

Необходимо различать: «сценарий запущен», «инструмент сообщил об успехе», «защита зарегистрировала событие» и «наблюдаемый результат подтвердил нарушение свойства». Отсутствие сигнала защиты без проверки исправности её телеметрии оставляет результат неопределённым.

Тестовый инструмент сообщает об успехе. Два отчёта пересказывают это сообщение. Независимого наблюдения о результате передачи нет.

Оценка: одна группа происхождения; данных недостаточно. Повторные отчёты не повышают доказанность результата.

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

Идентификатор испытания, проверяемое свойство, конфигурацию стенда, происхождение измерений, время и его неопределённость, общие зависимости наблюдателей, исходные свидетельства и ограничения доступа. В выводе отдельно обозначать подтверждённое, опровергнутое, противоречивое и неустановленное.

Полный набор материалов доступен уполномоченному оценщику. Другие участники получают разрешённые им представления; общая координация не означает общий доступ ко всем данным.

060С чего начать внедрение

Описать профиль лабораторной оценки. Целевые защитные свойства, известная конфигурация, границы испытаний и правила работы с результатами.

Уточнить контракт результата. Разделить сообщение инструмента, наблюдаемый эффект и заключение; сохранять происхождение и неопределённость.

Собрать три синтетических упражнения. Повторное наблюдение из одного источника; противоречивые сведения; недоступный источник при сохранении локальных правил.

Проверить их на контролируемом стенде. Оценить корректность картины, повторяемость и возможность объяснить вывод другому инженеру.

Расширять каталог по найденным пробелам. Добавлять сценарии, которые проверяют новые свойства или конфигурации, с отдельным критерием результата.

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

Самоотчёт без измерения — Результат остаётся неподтверждённым.

Несколько копий одного источника — Они сохраняют общую группу происхождения.

Исчезновение наблюдателя — Отображается потеря видимости, а не успешная защита или успешное испытание.

Несопоставимое время — Исторические сообщения не объявляются текущим конфликтом без основания.

Изменение конфигурации стенда — Прежний вывод не переносится автоматически на новую конфигурацию.

Пересказ AI — Происхождение, ограничения доступа и статус гипотезы сохраняются.

061Платформа, которая находит нарушения в поведении систем

Моя ставка — четыре исследовательских механизма: поиск ошибок композиции, сравнение нескольких исполнений, восстановление модели поведения и контролируемые причинные эксперименты. Они дают Red новые способы находить дефекты. Mirage, переносимый опыт, точные полномочия и устойчивое распределённое исполнение обеспечивают перенос результата в защиту.

Это продолжение исследования seL4/TEE . В STEP 3 уже описаны дело исследования, SGrap-проекция, роли и состояния, replay, пакеты находок и независимый Judge. Здесь уточняется следующий механизм внутри этих направлений. Наличие описания или заголовочного файла не принимается за готовую интеграцию.

«Бесконечная» или «экспоненциально дорогая» атака — пока метафора. Полезный инженерный контракт: повысить измеренную стоимость достижения выбранной цели при ограниченном ущербе доступности и расходе ресурсов защитника. Это проверяется против нескольких стратегий Red, включая отказ от взаимодействия с приманкой.

062Поиск ошибок композиции

Проверять переходы между компонентами, на которых меняется представление данных, идентичности или полномочий.

Пример для стенда. Gateway разрешил чтение объекта A. Адаптер преобразовал идентификатор, а сервис прочитал объект B. Каждый компонент завершился штатно. Свидетельством нарушения становится расхождение между авторизованным и фактически прочитанным объектом.

Исследовать отдельно разрешение имён, смену роли, преобразование RPC/API, восстановление сессии и повтор доставки. При неоднозначной нормализации сохранять исходное значение и дерево разбора: общий «исправляющий» normalizer может скрыть именно ту ошибку, которую ищем.

Основания: LangSec: Security Applications of Formal Language Theory рассматривает ошибки разбора и композиции; RESTler строит последовательности с зависимостями и обратной связью. BoundaryCase и интеграция выше — предложение для PLATX.

063Безопасность нескольких исполнений

Некоторые нарушения видны только при сравнении миров: один отдельный запрос выглядит полностью допустимым.

Пример. Два стенда имеют одинаковые публичные данные. В одном меняется приватное состояние тестового tenant B. Если разрешённая наблюдаемая проекция tenant A меняется вопреки контракту изоляции, появился канал влияния. Ни ошибка HTTP, ни crash для этого не нужны.

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

Основание: Clarkson & Schneider, Hyperproperties — свойства безопасности, которые нельзя выразить ограничением одной трассы. Предлагаемый PLATX-прототип охватывает небольшой практически проверяемый класс парных отношений.

064Восстановление модели поведения

Следующее действие Red должно отвечать на вопрос, который меняет понимание цели.

Пример. Доступ после отзыва тестовой роли сохранился. Есть три объяснения: старый кэш, незавершённая сессия или восстановление прежней версии политики. Планировщик выбирает опыт, результаты которого разделят эти объяснения, и корректирует модель после наблюдения.

Это углубление «мастерской объяснений» STEP 3. Выученная модель приближённа: конечное наблюдение не доказывает эквивалентность реализации. Concurrency, время и огромные пространства идентификаторов требуют отдельной абстракции.

Основание: LearnLib предоставляет active automata learning, обработку контрпримеров, conformance tests и приближённые проверки эквивалентности. Автоматическое подключение LearnLib к PLATX здесь не предлагается как готовая функция.

065Причинный разбор через управляемые ветви

Результат исследования — минимальный набор условий, при котором ломается свойство системы.

Пример. Повтор запроса и краткая потеря связи по отдельности безопасны. Вместе они создают второй эффект. Четыре сопоставимые ветви — baseline, только повтор, только потеря связи, оба условия — обнаруживают ошибку взаимодействия.

Снимок одной VM не откатывает внешний сервер, очередь или время. Если внешнее состояние не контролируется, результат помечается как несопоставимый или ограниченный. Из одной удачной пары запусков причинность не выводится.

Основания: Chaos Mesh Workflows — последовательные, параллельные и условные сценарии отказов; Jepsen — проверка историй относительно заданной модели. Причинная композиция ветвей — предложение для PLATX, не заявленная гарантия этих инструментов.

066Компилятор согласованных миров

Генерировать связанную среду: сущности, сервисы, роли, историю и последствия действий выводятся из одной модели.

Пример. Ложный сервис ссылается на тестовую учётную запись; синтетические журналы, API и доступные файлы описывают ту же роль и ту же историю. Изменение роли согласованно отражается во всех представлениях. Ошибка согласованности делает мир неудачным тестовым образцом.

067Переносимый опыт с локальной проверкой

«Иммунитет» должен переносить проверяемую причину ошибки вместе с условиями применимости.

Пример. Один узел обнаружил дефект после отзыва роли и восстановления сессии. Другой получил пакет, проверил версии и модель авторизации на своей копии, воспроизвёл случай и только затем предложил локальное изменение. Третий узел признал пакет неприменимым из-за другого backend.

Срок действия пакета и отзыв выводов требуют доставки; offline-получатель может оставаться со старой информацией. Это видно в состоянии знания и не повышает его полномочия. Для обучаемых моделей отдельно нужны тесты poisoning и дрейфа — формат LessonBundle сам их не решает.

068Проверяемый смысл действия AI

Авторизовывать конкретный эффект на конкретном ресурсе с понятной цепочкой происхождения.

Пример. В журнал попал текст, похожий на административное поручение. Модель может процитировать его и предложить изменение. Для применения нужны отдельное полномочие на этот ресурс, разрешённый тип изменения и проверяемые предпосылки у исполнителя.

Это конкретизация семантической изоляции, а не универсальный распознаватель намерений. Policy checker тоже входит в модель доверия. Если проверяющий и ресурс полностью контролируются захваченным хостом, подпись запроса сама по себе границу не удерживает. Для узкого сервиса полномочий применимы варианты seL4/TEE из предыдущего исследования .

069Контракт устойчивости при разделении сети

Mesh должна заранее определять, какие функции и какие права сохраняются, когда общая картина распадается.

Режим — Что может продолжаться — Проверяемое ограничение

Связь доступна — Анализ, координированное исполнение, обновление картины. — Эффект допущен текущим ресурсным контрактом.

Узел изолирован — Анализ имеющихся материалов и предварительно ограниченные локальные задачи. — Нет самостоятельного продления срока, бюджета или расширения прав.

Полномочие истекло — Сохранение доказательств и заранее предусмотренное завершение/cleanup. — Новые воздействия требуют нового допуска; cleanup имеет собственную ограниченную авторизацию.

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

Основания: Jepsen: Linearizability объясняет порядок операций и ограничение доступности при partition; ResilMesh описывает распределённую аналитику и совместимость security controls. Конкретные правила полномочий выше — авторское предложение.

070Что взять у ResilMesh

Официальное описание ResilMesh связывает SOAPA, ситуационную осведомлённость, зависимости активов и сервисов, распределённую корреляцию, federated learning и реагирование через playbooks. Это описание целей и архитектуры проекта; оно не устанавливает гарантию отсутствия всех центральных зависимостей или произвольной автономии каждого узла. Источник проекта , описание распределённого SOC и реагирования .

Для PLATX я бы взял три вещи: связь технического события с влиянием на миссию; контракты обмена между разными средствами; сквозную проверку от наблюдения до результата реагирования. В PLATX есть соответствующие документальные точки: SENSE → SGrap → расследование → Mission/Action → свидетели → FORENSIC. Добавлять параллельный SOC ради названия ResilMesh не требуется.

Отличие предлагаемого развития PLATX: Red получает право исследовать саму достоверность общей картины. Он проверяет ложные корреляции, устаревшие наблюдения, пересказы одного источника, конфликт политик и частичные результаты. Judge оценивает, когда система признала неизвестность и когда ошибочно объявила уверенный вывод.

Самоорганизация требует конкретного алгоритма членства, обнаружения отказов, распределения ролей, разрешения конфликтов и границ локальной власти. Список «сенсоры + AI + обмен данными» этих решений не заменяет.

071Один сквозной опыт вместо восьми независимых модулей

Тестовый сервис с двумя ролями, фоновым заданием и двумя представлениями одного объекта.

Задать свойство. После подтверждённого отзыва роли новые обращения в выбранном контракте не должны создавать защищённый эффект. Для ранее начатых заданий семантика оговаривается отдельно; без этого oracle неоднозначен.

Собрать цепочку. A1 связывает gateway, адаптер и worker. A2 сравнивает исполнения с действующим и отозванным доступом при совпадающих остальных условиях.

Получить передаваемый результат. Judge подтверждает эффект; FORENSIC сохраняет трассы; A6 упаковывает предусловия, воспроизводитель и отрицательные контроли.

Сравнить исправление. Тот же случай повторяется на новой сборке вместе с полезным сценарием и отложенными проверками. Отдельно оценивается, обнаружила ли Blue нарушение и была ли её телеметрия исправна.

Путь: дело → предложение → допуск → исполнение в стенде. Независимый наблюдатель передаёт эффект Judge; Judge связывает его с трассой в FORENSIC. После сессии кейс становится входом A6 и нового исследования.

Разделение информации: Controller знает сценарий, Judge знает ground truth, участники получают разрешённые наблюдения. До завершения оценки скрытые условия не становятся подсказкой планировщику. Независимость наблюдателя определяется размещением и общими зависимостями.

072Порядок развития

Направление — Основная сложность — Чем измерить ценность

A1 · Композиция — Явная семантика границ и идентичности. — Подтверждённые нарушения при одинаковом бюджете; ложные находки.

A2 · Отношения — Сопоставимые исполнения и корректный comparator. — Нарушенные свойства, которые пропустил одиночный тест; ложные расхождения.

A3 · Модели — Reset, абстракция состояний, недетерминированность. — Верность предсказаний на отложенных трассах и время до подтверждения.

A4 · Причины — Управление расписанием и внешними зависимостями. — Воспроизводимость минимального случая; стоимость его получения.

A5 · Миры — Согласованность и отсутствие подсказок Judge. — Различимость неизвестных семейств; затраты Red и Blue по отдельности.

A6 · Опыт — Применимость, provenance, poisoning и локальная проверка. — Скорость независимого повторения; частота неправильного переноса.

A7 · Полномочия — Enforcement у ресурса и точная семантика эффекта. — Неразрешённые эффекты в корпусе тестов; ошибочные отказы полезным действиям.

A8 · Отказы — Модель согласованности, fencing и общие зависимости. — Повторные/недопустимые эффекты, потерянные evidence, доступность минимального режима.

Численные пороги устанавливаются после baseline на выбранном стенде. Это оценки направления работ, без обещаний сроков или найденных уязвимостей.

073Как преобразовать исходные семь идей

Удорожание атаки: A5 и измерение стоимости обеих сторон; смена портов сама по себе не обещает существенного эффекта.

Иммунитет: A6; сначала воспроизводимые случаи, затем перенос проверенного знания.

Семантическая изоляция: A7; проверяемый контракт действия вместо предположения о внутренних намерениях.

Security Mesh: A8; наблюдения, полномочия и поведение при partition проектируются раздельно.

ZKP: узкий дальнейший опыт — доказательство вычисления заданного policy predicate над фиксированными commitments. Оно не доказывает истинность датчика или произошедший внешний эффект. Freshness, public inputs и привязка к ресурсу задаются отдельно.

Адаптивные контракты: A8; заранее согласованные режимы деградации и наблюдаемый статус исполнения.

AI + формальные методы: A2–A4 дают конкретные свойства и контрпримеры. AI предлагает модели, формальный инструмент проверяет заявленную модель, а воспроизводитель связывает её с реализацией. Ранее предложенный R4 остаётся основой.

Что считается доказанным нарушением для каждого property; кто наблюдает эффект и какие зависимости общие с исполнителем.

Какие границы состояния реально откатываются; что происходит с очередями, временем и внешними сервисами.

Как версионируются BoundaryCase, RelationalProperty и InterventionManifest и как они переводятся в поддерживаемый путь Action.

Какие данные доступны Red во время опыта, какие получает только Judge и когда публикуется разбор.

Какие полномочия действуют offline, кто проверяет generation/expiry и как разрешается неизвестный исход после reconnect.

074Локальный контекст

Создан только этот HTML. Runtime, исходные документы и действующий roadmap не менялись; инструменты исследования атак не запускались. Выполнена статическая проверка структуры и ссылок. Визуальный рендер не проверен: ранее браузер заблокировал открытие локального HTML, обход этого ограничения не использовался.

Изоляция и аппаратное доверие

075Главное решение

Первым шагом я бы строил для PLATX двигатель исследования атак: граф предпосылок → типизированная техника → воспроизводимое исполнение → проверка достигнутого свойства. seL4 и TEE подключал бы к небольшим сервисам полномочий и секретов после появления конкретного потребителя их гарантий.

Это даст Red способность обнаруживать новые комбинации слабостей и проверять эксплуатационные гипотезы. Перенос всей платформы на микроядро сам по себе такую способность не создаёт.

076Что брать из существующих систем

Из CALDERA и BloodHound — связь действий с фактами и путями достижения цели. Из Nyx/kAFL и syzkaller — поиск по последовательностям и быстрый возврат к состоянию. Из Signal SVR3 — разделение доверия. Из Trustee/Nitro — допуск к конкретному секрету после проверки среды. Из seL4/Microkit — небольшой и явно ограниченный доверенный контур.

077seL4: кто может воздействовать на кого

Микроядро предоставляет механизмы изоляции и capabilities; доказательства относятся к определённым свойствам и конфигурациям ядра. Приложения, протоколы, boot, драйверы и аппаратные зависимости требуют своего обоснования. Допущения .

078TEE: что скрыто от хоста

Выбранные состояние и вычисления защищаются в аппаратной модели конкретной платформы. Хост всё ещё может мешать сети, хранению и выполнению. Код внутри доверенной области может содержать ошибки и неверно использовать легитимные запросы. Nitro ; TDX .

079Аттестация: какую среду оценили

Evidence, его оценка и решение о доступе — разные шаги. Измерение одобренного образа не доказывает истинность всех данных, отсутствие runtime-уязвимостей или корректность каждого результата. RATS, RFC 9334 .

080Формальный proof / ZK: какое утверждение проверили

Доказательство вычисления говорит об указанной программе, входном commitment и модели. Оно не делает правдивым подложный лог, не подтверждает физический эффект и не гарантирует, что исходная политика выражает нужный смысл.

Изоляция и наблюдение могут конфликтовать. Если VMI-хост свободно читает секретную память TDX-гостя, заявленная конфиденциальность от этого хоста не выполняется. Для исследования нужны два режима: инструментируемая лабораторная сборка с синтетическими секретами и confidential-сборка с проверкой разрешённых выходов. Результаты между ними переносят только в доказанной области эквивалентности.

081Шесть архитектурных решений для Red

Цель — сделать PLATX способным исследовать достижимость цели, находить слабые переходы и воспроизводить нарушение. Инфраструктура защиты обслуживает этот исследовательский цикл.

Активы, предпосылки, доверительные связи и неизвестные факты.

Выбор техники или эксперимента, который проверяет конкретный переход.

Сопоставление ожидаемого результата с фактом и пределами видимости.

Уточнение графа, минимизация цепочки, повтор после исправления.

082Граф атаки с совместными предпосылками

Узел описывает principal, ресурс, достижимое состояние или факт. Переход описывает requires_all , альтернативы requires_any , ограничения контекста, ожидаемый эффект, источник и срок применимости. Сочетание условий представляется action-node или гиперребром; обычного поиска кратчайшего пути по независимым рёбрам недостаточно.

Планировщик хранит отдельные статусы OBSERVED , INFERRED , REFUTED , STALE . Он выбирает либо проверку перехода, либо наблюдение, снимающее неопределённость. Приоритет — ценность цели, подтверждённость предпосылок и стоимость опыта. Вероятности успеха не выдумываются: до калибровки достаточно категорий и интервалов.

Архитектурный образец: BloodHound OpenGraph CALDERA facts / planners . AND/OR-модель и указанные критерии — предложение для PLATX.

083Типизированный пакет исследовательской техники

Оформить технику как проверяемую единицу с предпосылками и результатом, которую планировщик умеет связывать с другими техниками.

Версия техники и digest реализации; schema входов; preconditions; target identity; ожидаемое состояние; oracle; требования к executor; зависимые artifacts; класс воздействия; ограничение ресурсов; семантика cleanup и невозможности отката. Параметры передаются по схеме, а не вставляются в произвольную строку shell.

У техники как минимум три результата: не выполнены предпосылки, опыт проведён без подтверждения цели, цель независимо подтверждена. Отдельно — ошибка инструмента и неопределённость наблюдения. Выходной код 0 не означает успешной атаки; crash не равен доказанной возможности читать, писать или управлять ресурсом.

Новый opcode не добавляется молча в v1. Недопустимо кодировать запуск техники как COLLECT или прятать семантику в target . Изменения нужны также в mission_act : его отображение opcode → verb сейчас закрыто. Имена v2 здесь проектные, готовой реализации не заявлено.

084Фаззинг состояния с ветвлением от снимков

Искать ошибки в последовательности допустимых действий и переключений, а не только в отдельных некорректных буферах.

Набор типизированных событий: открыть сессию, доставить сообщение, задержать callback, оборвать канал, сменить generation, восстановить процесс, проверить состояние. Генератор сохраняет зависимости ресурсов, мутирует порядок и параметры. Feedback включает покрытие кода и достигнутые состояния протокола; отдельный oracle проверяет свойства.

Снимок берётся после дорогой подготовки и успешного префикса. Ветви используют разные продолжения. Минимизатор убирает ненужные события, сохраняя нарушение. Вместе с воспроизводителем сохраняются binary/config digests, seed, scheduler decisions и исходное состояние.

Nyx / kAFL Nyx: affine types и snapshots syzkaller .

085Контрпример из модели → исполняемый эксперимент

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

Небольшая TLA+ модель задаёт mission, lease generation, prepare/commit, потерю сообщения и restart. Apalache проверяет заданные инварианты в указанной области. Для криптографического handshake отдельная модель Tamarin описывает роли, ключи, сообщения и желаемые свойства аутентификации.

Apalache: bounded и inductive checking Tamarin .

086Испытания доверительной границы TEE

Проверять обвязку анклава: verifier, выдачу секретов, привязку канала, обновления и сохранение состояния. Именно эти переходы связывают аппаратную защиту с прикладной системой.

Тестовый attester умеет выдавать заведомо корректные и некорректные варианты, а reference verifier возвращает объяснимый verdict. Затем те же сценарии проходят через реальный hardware backend и тестовый secret broker. Сравниваются не все поля quotes «на равенство», а требуемая семантика: свежесть, binding, trust anchor, версия и ресурс.

Для syscall/RPC-зависимостей сервиса проверяется реакция на недостоверные ответы хоста — например, противоречивый размер или результат чтения. Реальная попытка проводится на одноразовых тестовых объектах; oracle проверяет доступность именно тестового секрета или разрешённой операции.

RATS: роли и свежесть Trustee: проверка evidence и resource policy Nitro: проверка attestation document . Матрица опытов — проект тестовой программы PLATX.

087Лаборатория с внешним oracle и сравнением вариантов

Дать Red среду, в которой можно отделить реальный результат техники от самоотчёта инструмента и от реакции Blue.

Mirage управляет world/snapshot. Executor получает только своё задание. Внешний observer снимает разрешённые факты через VMI, сетевую точку наблюдения или отдельный сервис-получатель. Judge сопоставляет фактический эффект, detection, telemetry health и стоимость опыта.

Четыре отдельных исхода: эффект подтверждён и замечен; подтверждён и не замечен при исправной телеметрии; эффект не достигнут; недостаточно наблюдений. Отключённый датчик сам по себе не становится «успешным обходом».

DRAKVUF Sandbox DRAKVUF существующее разделение Controller / Red / Blue / Judge .

Какой результат будет уже настоящим усилением Red? PLATX получает топологию тестового сервиса, выводит гипотезу о достижимости защищённого тестового объекта, выбирает последовательность проверок, подтверждает или опровергает каждый переход, сохраняет минимальный воспроизводитель и повторяет его после исправления. Это законченная исследовательская возможность, а не ещё один защитный запрет.

088Четыре решения для защиты

Здесь стоит добавлять меньше компонентов, но менять более глубокие свойства доверия: кто владеет ключом, кто принимает окончательное решение и какая часть системы должна оставаться исправной.

089Малый сервис полномочий за отдельной границей

Свести критичную доверенную часть к проверке конкретной операции и использованию её ключа. Сложные парсеры, AI, UI и внешние инструменты оставить отдельными потребителями.

Недоверенный worker обращается к broker; broker проверяет формат, размер и контекст; authority service проверяет актуальные полномочия и самостоятельно формирует каноническое подписываемое сообщение. Интерфейс: разрешить конкретное назначение, подписать checkpoint заданного домена, выдать ограниченный lease. Операции «подпиши любые байты» в прикладном API нет.

Три возможных backend одного контракта: отдельный процесс для базовой изоляции; enclave для защиты от хоста; seL4 protection domain на отдельном appliance для малой проверяемой архитектуры. Их гарантии не объявляются равными.

Kry10: изоляция и восстановление Microkit verified configurations Android KeyMint .

090Свежая аттестация, связанная с каналом и ресурсом

Аттестация должна влиять на выдачу определённого секрета или права, а не оставаться зелёным индикатором рядом с узлом.

Verifier создаёт challenge с nonce и контекстом назначения.

Изолированный сервис генерирует ephemeral key; доступное аппаратное evidence связывается с commitment этого ключа и нужного контекста.

Verifier проверяет доверенный корень, measurement/reference values, применимые TCB/version claims, свежесть, debug state и binding.

Resource policy отдельно решает, какой secret/operation допустим для этого workload. Результат содержит audience, scope, policy epoch и срок.

Секрет шифруется на подтверждённый ключ либо операция выполняется внутри сервиса. RA2C и локальный владелец ресурса проверяют актуальность допуска.

Nitro KMS: ключ ответа внутри evidence KMS attestation conditions Trustee RATS .

091Разделённое доверие для корневых операций

Сделать компрометацию одного хранителя недостаточной для наиболее ценной операции: восстановления master key или смены корня доверия.

Для recovery выбирается заранее анализируемая схема разделения секрета с заданным порогом. Для критичной команды возможны две отдельные подписи независимых authorities: получатель требует обе. Если нужна именно threshold signature, используется готовый исследованный протокол; это отдельное решение, не «складывание подписей».

У каждого хранителя отдельные admin credentials, ключи, trust roots и журналы. Общая сборочная система, один super-admin или одна процедура аварийного восстановления могут уничтожить ожидаемую независимость даже при разных CPU.

Signal SVR3, работа авторов . Предлагаемые quorum/dual-signature варианты — отдельный проект PLATX.

092Квитанции истории вне контролируемого узла

Существующий FORENSIC checkpoint сделать проверяемым после потери доверия к узлу, сохранив часть свидетельств и обязательств вне этого узла.

FORENSIC формирует канонический checkpoint: диапазон sequence, root, предыдущий root, source identity, policy и epoch. Signer подписывает его. Независимый witness сохраняет checkpoint и возвращает receipt. Клиент запоминает последний принятый head и проверяет продолжение, переход identity и восстановление после аварии.

На другой узел отправляется commitment и необходимый контекст; приватные artifacts остаются в соответствующем хранилище. Для sensitive low-entropy данных требуется hiding commitment или подходящее шифрование: обычный hash может позволять перебор.

Azure confidential ledger CCF: CFT/Raft Signing Transparency: receipts и recovery существующий checkpoint .

093Три варианта размещения

Это альтернативные профили, а не обязательные ступени миграции. Выбор зависит от нарушителя: процесс, администратор ОС, оператор инфраструктуры или отдельный компонент устройства.

094Как совместить Red-лабораторию и защиту секретов

Логическая схема предложения. Authority выдаёт права, но не получает ground truth Judge. Реальные домены размещения, администраторы и общие зависимости задаются профилем; расположение блоков на рисунке не доказывает независимость.

Рациональный выбор на старте: развивать R1–R3 на существующем Linux/Windows пути и disposable lab. Для B2 выбрать один hardware backend с доступным оборудованием. Для seL4 — отдельный узкий прототип на подтверждённой конфигурации. Переписывать RA2C, SGrap, DSL/MSX и UI ради микроядра на этом этапе не требуется.

095Какие исходные идеи выдерживают инженерную проверку

Идея — Что реализовать — Чего нельзя обещать

Адаптивная поверхность атаки — Ротация короткоживущих workers, identities и endpoints по управляемым поколениям. Планировщик R1 измеряет, сколько новых наблюдений требуется для восстановления пути. Стабильный административный путь остаётся отдельно. — Изменение порта не устраняет логическую уязвимость; изменение API может увеличить сложность и число ошибок. «Экспоненциальная стоимость» без модели и измерения не обоснована.

Размножение ложных целей — Развить существующие Mirage world, Oracle и watermark: взаимно согласованные сервисы, данные и связи. Red оценивает, насколько легко отличить decoy; Judge имеет отдельное истинное состояние. — Количество приманок само по себе не даёт защиты. Canary с реальными полномочиями создаёт новый риск; synthetic provenance не превращает взаимодействие в факт о production.

Экономический ущерб атакующему — В своём сервисе — дешёвый отказ, ограничение ресурсов, при необходимости протокольно обоснованный admission puzzle. Считать стоимость легитимного запроса и сервера вместе со стоимостью сканирования. — Нельзя гарантировать финансовый ущерб чужой стороне. Агрессивные задержки и puzzles могут обходиться своим пользователям дороже, чем нарушителю. Для PLATX это эксперимент с ограничением затрат, не отдельная миссия возмездия.

«Иммунная» память узлов — Распространять версионированные признаки и минимальные воспроизводители. Локальная политика оценивает применимость. Недоверенный узел не может автоматически превратить своё сообщение в общий запрет. — Одинаковое правило на всех узлах может размножить ошибку или poisoning. Нужны provenance, срок, критерий отката и защита от циклического распространения пересказов.

Семантическая авторизация — Типы ресурсов и действий, pre/postconditions, taint, target identity, текущая generation. Значение операции проверяет детерминированный код по явным свойствам. — Распознавание «намерения» нейросетью не становится корнем авторизации. Валидный JSON и разрешённый API не гарантируют допустимость композиции действий.

Гетерогенные узлы — Разделить секрет или критичное решение между независимыми authorities, как в B3. Технологии подбираются под разные причины совместной компрометации. — Если один узел уже имеет все данные и права, атакующему достаточно его. Разные ОС сами по себе не заставляют взламывать всех участников.

ZK и доказательство исполнения — Если нужен внешний проверяющий без раскрытия данных — начать с узкой чистой функции: применение фиксированной политики к committed входу. Сначала оценить обычную подпись/receipt и threat model; только затем zkVM или специализированную схему. — Доказательство f(x)=y не подтверждает, что x — правдивый sensor log или что firewall действительно изменился. Для внешнего эффекта остаётся witness.

Динамические гарантии — Профиль сохраняемых функций, queue budgets, дедлайны, admission control и допустимая деградация. Выдать конкретные измерения при указанной нагрузке и отказах. — Измеренный p99 — не hard real-time bound. Недоступный узел нельзя сделать доступным математическим обещанием; graceful degradation — отдельный проверяемый путь.

AI + формальная проверка — R4: модель предлагает candidate invariant, prover строит proof/trace, harness проверяет реализацию. Сохранять model version, assumptions и статус результата. — Проверенная модель, написанная AI, может неверно описывать систему. Сообщение «контрпример не найден до k» не равно доказательству для всех исполнений.

Это оценка предложений пользователя и авторская адаптация к PLATX. Основания для технологических различий: допущения seL4 , RATS , SVR3 , Apalache .

Архитектура платформы

096PLATX — доктрина боевой платформы

PLATX — военизированная боевая программная платформа военного уровня надёжности для работы в условиях активного противодействия, компрометации компонентов и частичной потери инфраструктуры. PVNDORABOX / PVNBOX / PWNDOORA — имена проекта; PLATX остаётся техническим именем платформы.

097Назначение и боевой цикл

Платформа сохраняет управляемость и защиту узла, наблюдает противодействие, проверяет полномочия, исполняет ограниченные по времени и ресурсам задачи, фиксирует результат и восстанавливает допустимое состояние после отказа. Боевая задача в этой документации — программная миссия в заданном контуре: защита, реагирование, исследование или санкционированная проверка защищённости.

Цикл: наблюдение с указанием полноты → типизированный план → локальная проверка полномочий → исполнение → независимая проверка результата → evidence → завершение либо recovery/rollback. Успех отдельного действия не равен успеху миссии. Все внешние эффекты имеют owner, generation, scope, deadline, budget и определённую процедуру остановки.

Существующая пара mission_spec_t / mission_state_t относится к Cognitive Plane. Общие требования к миссии не вводят второй тип миссии, новый реестр, параллельный координатор или обязательный AI-рантайм.

098Условия применения

Потеря основного канала, задержки, дубликаты, replay и длительная изоляция от Central. Автономность действует только в пределах ранее выданных полномочий и срока их действия; локальная защита сохраняется.

Отказ провайдера, CHILD, сенсора, watchdog или хранилища; исчерпание памяти, дескрипторов, очередей, времени и попыток.

Подмена профиля, модуля, обновления, ключа или доказательства; компрометация аутентифицированного peer, оператора либо поставщика содержимого.

Границы доверия к ядру ОС, гипервизору и аппаратуре фиксируются в модели развёртывания. Программная платформа не получает аппаратную стойкость к температуре, вибрации или электромагнитному воздействию от своего названия.

099Живучесть и безопасный отказ

Потеря канала допускает keep-old либо явное закрытие сессии. Потеря Central не отключает локальный защитный контур. Потеря обязательного verifier не разрешает исполнение. Исчерпание бюджета прекращает новые действия, сохраняя полномочия на ограниченный cleanup. Отказ аудита делает потерю наблюдаемой и блокирует новые эффекты, для которых профиль требует подтверждённый аудит.

Tamper отзывает полномочия затронутого контура, запрещает использование его ключей и запускает проверяемое уничтожение ключевого материала. Восстановление требует доверенной конфигурации и новых ключей/generation; возврат старого состояния после zeroize запрещён. Простое завершение процесса не доказывает очистку всех копий секретов.

100Контуры и полномочия

Red и Blue используют симметричные инженерные возможности в рамках своих action sets. Controller задаёт scope и boundaries, Judge независимо оценивает исход. MIRAGE и испытательные среды сохраняют происхождение синтетических данных. Лабораторная готовность не переносится на боевую поставку автоматически.

101Профиль применения и готовность

Build profile определяет состав бинаря. Load-time profile определяет аутентифицированную неизменяемую политику запуска. TL отражает текущее наблюдаемое состояние угрозы. Эти три сущности не взаимозаменяемы. MIL_1…MIL_4, где они существуют в интерфейсе, — идентификаторы PLATX; они не означают уровень доверия ФСТЭК, DAL или оценку MIL-STD.

Требование принято и трассируется до owner, реализации и проверки.

Реализация проверена в development-среде для указанного профиля.

Поставка квалифицирована на целевой ОС и аппаратуре по обязательным gates.

Внешнее соответствие подтверждено отдельной процедурой в её области.

Необходимые документы: ТЗ полной интеграции , стандарты , приёмка , архитектура , план , факты .

102Приоритет

Военный уровень надёжности и живучесть разрешённого рабочего пути.

Предсказуемая ошибка и откат. Полусостояние запрещено.

Простота. Новая возможность только если делает основной путь стабильнее.

Академическая полнота, «ещё один транспорт», нумерация ТЗ в комментариях, скрытый fallback «чтобы зелёное» — не аргумент.

Сессия (RA2C) — модуль , не позвоночник. Панель, honeypot, новые транспорты — не позвоночник.

Целевой операторский путь platx-1.0 (не тег platx-0.1 ):

103Один мозг на роль

Исполняет I/O — XIO — не решает lifecycle/recovery

Выбирает политику — Policy (правила XIM/Hook/seccomp) — не ходит в syscall за ребёнка

Наблюдает / ограничивает — Hook — не инжектит, не рестартит процесс

Посредничает CHILD — XIM — не становится I/O backend

Меняет состояние модуля — Lifecycle — не «решает политику»

Картинка — Supervisor — не start() / stop()

Предполагает и планирует — Cognitive Plane (AI) — не наблюдает, не решает, не исполняет, не доказывает

104Полусостояние — конкретные запреты

ACK, канала нет — ACK только после готового fd+ключа

live fd, generation 0, таблица claim есть — claim с реальной generation или закрыть fd

own_fd упал, fd оставили — закрыть и отказать

switch: новый fd мёртв, старый уже брошен — keep-old

XIM armed, listener не пришёл, child exec — отказать start, не «пойти без медиации»

weak emit/push вернул 0 без провайдера — NOLINK / -1

attach ACTIVE без verify — транзакция + rollback

leftover keyed после disconnect — wipe, fd closed, send refuse

105Полусостояние — конкретные запреты / Назначение

PLATX — военизированная боевая платформа военного уровня надёжности. ТЗ распространяет дисциплину RA2C MIL на все подсистемы: ограниченные ресурсы, детерминированный отказ, живучесть, защита ключей, управление угрозой, anti-replay, бюджет миссии, независимый watchdog, аудит и доверенное исполнение. Полный смысл — доктрина .

106Единые принципы MIL-1…10

При частичном отказе сохраняется разрешённая часть миссии на проверенных резервах. Failover не ослабляет аутентификацию, sandbox или локальную Policy. Нет пригодного резерва — явное завершение/деградация по контракту. Потеря Central не отключает локальную защиту.

MIL-3. Ограниченное время исполнения

Критические операции имеют workload, deadline, предел итераций и retry. Длительные операции допускают отмену и ограниченный cleanup. WCET обосновывается для target; O(1), средняя задержка и P99 не заменяют верхнюю границу времени.

TL — вход детерминированной политики, управляющей допустимыми каналами, нагрузкой и действиями. Профиль задаёт пороги, разрешённые резервы и бюджет. TL не является источником новых полномочий. Platform TL и transport TL сопоставляются явным адаптером; одинаковое число не доказывает одинаковый смысл.

MIL-6. Anti-replay и исчерпание счётчиков

Session/epoch, generation, nonce, sequence и replay window согласованы между концами протокола. Исходное требование RA2C MIL использует 64-битный внутренний счётчик; изменение wire width требует versioned interop-контракта. Даже 64-битный счётчик конечен: перед исчерпанием — rekey либо отказ, повтор после reconnect/restart и перенос старого контекста отвергаются.

Bytes, время, attempts и effects ограничены до начала исполнения. Бюджет проверяется до effect, списывается с определённой семантикой при retry и не обнуляется при перепланировании. Исчерпание запрещает новые действия, сохраняя отдельный ограниченный бюджет для остановки/rollback.

CLI/API effects, module lifecycle, TL, отказ авторизации, zeroize и recovery дают типизированные записи с owner/generation, порядком и результатом. Запись не включает реальные секреты. Исходная ёмкость staging ring — 8192 записи, память выделяется до критического пути. Размер wire-record определяется публичным codec, а не предположением о sizeof.

MIL-10. Доверенное исполнение модулей

До исполнения проверяются аутентичность, целостность, версия, допустимость signer и полномочия на конкретный artifact. Verify привязан к исполняемым байтам/generation и защищён от TOCTOU. При требовании verify-on-every-call проверяется именно этот режим с bounded overhead; проверка при загрузке не выдаётся за него. Наличие подписи не разрешает запрещённую операцию. Нет verifier или подпись неверна — отказ и аудит, cleanup затронутого контекста.

107Load-time profile

Целевая последовательность: минимальный bootstrap и provisioning → чтение профиля → проверка аутентичности и семантики → фиксация immutable policy → audit/watchdog/protection readiness → запуск допускаемых capabilities → READY. До завершения доверенного запуска запрещены внешние effects и ослабление профиля. Повреждённый MIL-профиль не превращается в CIVIL.

Build profile выбирает состав бинаря; load-time profile выбирает политику этого состава. Нужные возможности должны реально входить в shipped artifact. Один бинарь на целевую ОС может обслуживать CIVIL/MIL, если проверены оба режима и неизменность политики. Это целевой контракт, а не утверждение, что все существующие точки запуска уже используют единый loader.

Поля требуемой политики: profile identity/version, authenticated digest, mission budgets, watchdog limits, обязательность и retention аудита, module signing/verification policy, CLI rate/lockout, TL actions, trust roots и допустимые capabilities. Новые поля вводятся только через versioned ABI/wire. Псевдоструктура rev 2 не заменяет существующий 120-байтный profile codec.

Исходные числа rev 2 (100 MB, 1800 s, watchdog 5 s / 3 strikes, CLI 10/s и 2/s при TL3+, lockout 30 s) — кандидаты параметров, требующие решения для профиля и измерений. RA2C header сейчас задаёт 64 MiB и 30 минут; эти разные значения не скрываются общей фразой «default MIL».

CIVIL сохраняет обязательные проверки owner/generation, доверенного запуска, полномочий и аудита изменяющих действий. MIL усиливает бюджеты, signing, watchdog и assurance. Названия профилей не обозначают сертификационные уровни.

108Ответственность подсистем

Core/Lifecycle/Recovery/XIO: limits, readiness, owner/generation, cancellation, bounded restart и отсутствие leftovers — MIL-1…3, 7…9.

RA2C/Transport/Mesh/MBus: loss/reorder/replay, разрешённый failover, bounded queues и сохранение аутентификации — MIL-1…9.

Crypto/Vault/Secrets/Integrity/SelfProtect: key ownership, revoke, zeroize, tamper, verifier availability и evidence — MIL-1, 3…5, 8…10.

Modhost/DRM/Plugin/Memfd/XIM: verified bytes, sandbox, generation, загрузка/отмена/cleanup — MIL-1, 3…5, 7…10.

MIRAGE/Sandbox/Red/Blue: изоляция, происхождение синтетики, независимый Judge, прекращение миссии и cleanup — те же платформенные требования.

Неприменимость конкретного принципа объясняется владельцем; пустая ячейка не означает выполнено. Контракт модуля — MODULE_CONTRACT .

109Последовательность интеграции

Фундамент: inventory shipped profiles/loaders, immutable policy, semantic validation, audit layout/continuity и warning/error gates.

Живучесть: bounded transport/mesh/MBus, replay exhaustion, provider loss, watchdog, local autonomy и explicit degraded states.

Защита исполнения: модульная целостность/TOCTOU, revoke/zeroize, CLI/API budgets, tamper FTA и независимые отрицательные сценарии.

Квалификация: полная миссия TL0→TL4→zeroize→trusted recovery, target-native interop/fault injection/soak, coverage и release dossier.

Эта программа входит в ROADMAP , не создаёт конкурирующего ACTIVE SPINE и не объявляет календарные сроки без оценки. Порог каждой стадии — соответствующие MG-gates .

110MIL-STD-882E — безопасность системы

Исходная редакция для плана — MIL-STD-882E с Change 1 от 27.09.2023. Стандарт охватывает выявление опасностей, снижение риска и полномочия на принятие остаточного риска на жизненном цикле аппаратуры и ПО. Для PLATX требуются hazard log, анализ опасных отказов, трассировка мер снижения риска к тестам и решение уполномоченного владельца. Состав задач определяется контекстом применения. Источник: официальная запись ASSIST .

Это system safety. Модель атак, целостность профиля и защита информации проверяются дополнительно. Проектные §MIL-1…10 не являются номерами пунктов MIL-STD-882E. Не присваивать вероятность, severity или приемлемость риска без operational context. Рабочие документы: hazard log , FTA tamper .

111ФСТЭК России — доверие и защита информации

Подготовка PLATX к применимым требованиям ФСТЭК входит в платформенную программу. Требуются модель угроз, определённая граница объекта оценки, профиль/задание по безопасности, контроль недекларированных возможностей, процессы безопасной разработки, материалы испытаний и управление изменениями. Функции AV, IDS, firewall и защиты среды выбирают свои применимые требования; один флаг MIL не устанавливает для них общий класс защиты.

Требования к уровням доверия утверждены приказом ФСТЭК России от 02.06.2020 № 76. Основание — информационное сообщение ФСТЭК от 15.10.2020 № 240/24/4268, копия документа . Эта исходная публикация не заменяет проверку последующих изменений: на начало оценки фиксируется полный действующий текст, применимые акты и методики. Проверка актуальной редакции и выбор уровня доверия остаются открытыми до формирования конкретного объекта оценки.

Сертификация средства, сертификация процесса разработки и оценка системы развёртывания учитываются раздельно. Криптографическое соответствие также обосновывается отдельно. План — FSTEC_READINESS .

112ГОСТ Р 56939-2024 — безопасная разработка ПО

Базовая редакция PLATX для процесса безопасной разработки — ГОСТ Р 56939-2024. Он введён 20.12.2024 взамен ГОСТ Р 56939-2016. Область — работы по созданию безопасного ПО и устранению его недостатков, включая уязвимости. Источник: карточка Росстандарта .

В программе PLATX нужны требования безопасности, threat modeling, review, статический/динамический/композиционный анализ, fuzzing, воспроизводимая поставка и обработка уязвимостей. Наличие этих инструментов не заменяет проверку полноты процессов по полному тексту. Результаты старой редакции сохраняются как история и требуют анализа различий перед переиспользованием.

113ГОСТ Р 51904 и ГОСТ РВ 0015-002

ГОСТ Р 51904-2002 рассматривается для разработки и документирования встроенного ПО при соответствующем назначении изделия; применимость к конкретной поставке PLATX фиксируется отдельно. Название и исходная редакция доступны в каталоге Росстандарта .

ЕСПД/ГОСТ 19 применяются к составу и оформлению комплектов документации, если это установлено для поставки. Markdown сам по себе не доказывает ЕСПД. Подробности — GOST_COMPLIANCE .

114DO-178C / ED-12C — обеспечение разработки критического ПО

PLATX принимает дисциплину трассировки требований, независимой верификации, управления конфигурацией и доказательного покрытия. Целевой ориентир исходного ТЗ — Level B; формальная применимость и software level устанавливаются в системном контексте. Авиационный уровень не назначается по названию профиля. FAA AC 20-115D рассматривает DO-178C/ED-12C в контексте авиационной оценки ПО.

MC/DC для выделенных критических решений PLATX — дополнительный внутренний gate. Его нельзя объявлять обязательным основанием Level B. Формальные методы не являются универсальным условием Level A, а compile-time флаг не повышает уровень assurance. План и источники по покрытию — DO178C_PLAN .

115MISRA C и собственные правила PLATX

Исходный набор правил проекта опирается на MISRA C:2012. Перед заявлением соответствия фиксируются лицензированный текст, редакция с amendments, область кода, настройки анализатора и одобренные deviations. Собственные запреты PLATX — UB, неограниченная рекурсия/очередь/повтор, непроверенные ошибки, heap allocation в квалифицируемом критическом пути — действуют независимо от полноты MISRA-проверки. Не приписывать стандарту выдуманные номера и названия правил. Область проверки .

116Криптография и аппаратура

ГОСТ Р 34.10/34.11/34.12 и FIPS относятся к своим алгоритмам/объектам оценки. HMAC, цифровая подпись и шифрование имеют разные назначения. Реализация алгоритма не подтверждает сертификацию модуля или всей платформы; отдельный crypto stack в обход C19 не вводится. Криптографический контракт .

MIL-STD-810/461 и иные аппаратные требования включаются только в программу испытаний поставляемого аппаратно-программного изделия с указанной редакцией, методами и конфигурацией. В текущей программной программе такие испытания не объявлены выполненными.

117Объект приёмки

До испытаний фиксируются source revision и dirty diff, manifest, binary hash, компилятор/линкер, зависимости, ОС/ядро/драйверы/аппаратура, build profile, digest load-time profile, политики, ключевые провайдеры, workload и scope. Отчёт явно отделяет Linux native, Windows native и portable/WSL. Численные бюджеты задаются до прогона и связаны с допустимым отказом миссии.

118MG-1. Доверенный запуск и поставка

Повреждённый, неизвестный, отозванный, семантически неверный либо недопустимо старый профиль отказывается до READY и внешнего effect. Нет fallback MIL→CIVIL. Проверяются аутентичность модулей, состав поставки, key provisioning, целостность обновления, прерывание установки и rollback. Проверка подписи связана с теми байтами и generation, которые исполняются.

119MG-2. Ограниченность ресурсов и времени

Инструментированием проверяются отсутствие heap allocation после init в объявленных критических путях, пределы stack/queue/fd/task, retry и lifetime. Исчерпание каждого бюджета приводит к объявленной ошибке и cleanup. Измеряются p50/p95/p99/max, timeout/error distribution и условия нагрузки. P99 и алгоритмическое O(1) не доказывают WCET; hard bound требует отдельного обоснования на целевой среде.

120MG-3. Живучесть при потере инфраструктуры

На реальном shipped profile проверяются provider loss, потеря канала/Central, blackout, задержки, reorder, дублирование и повторные отказы. Разрешённая локальная защита продолжает работу; неавторизованное новое действие не возникает. Failover сохраняет безопасность, owner/generation и заявленный бюджет. По истечении автономных полномочий новые распределённые эффекты прекращаются. Отказ резервов даёт явный результат, без бесконечного restart.

121MG-4. Tamper, отзыв и очистка

Негативные сценарии: подмена профиля/модуля, revoke ключа, stale generation, гонка verify/use, tamper во время исполнения и tamper во время cleanup. Проверяется немедленный запрет нового использования секретов, очистка контролируемых копий, закрытие дескрипторов и отсутствие keyed/READY leftovers. Повторное использование затёртого контекста запрещено. Recovery стартует только из доверенного состояния с новыми ключами и поколением.

122MG-5. Watchdog и проверяемый аудит

Принудительно блокируются heartbeat, рабочий поток, audit sink и flush. Watchdog не должен зависеть от зависшего пути; он сообщает факт, Recovery решает действие. Для кольца проверяются concurrency, wrap, sequence gaps, переполнение, export, restart и сохранность evidence. Ненулевой MAC не равен валидному MAC. Bounded ring не является вечным append-only архивом. Если обязательный аудит недоступен, новые требующие его effects блокируются; остановка и очистка не ждут недоступного sink бесконечно.

123MG-6. Независимая верификация и устойчивость

Обязательны поведенческие и отрицательные проверки, fault injection, fuzzing, sanitizers на поддерживаемых целях, concurrency/interop и длительная работа. Начальный endurance gate исходного ТЗ — не менее 12 часов на каждой целевой поставке с workload и внесёнными отказами. Успешные 12 часов не доказывают MTBF/MTTF или эксплуатационный срок: статистический вывод требует своей модели.

124MG-7. Решение о допуске

В комплект входят результаты MG-0…MG-6, известные ограничения, открытые дефекты, остаточный риск, инструкция оператора, схема отзыва/обновления, подписанные артефакты и решение владельца допуска. Незакрытый обязательный gate блокирует боевую квалификацию этой поставки. Development-сборки маркируются отдельно и не получают статус квалифицированного изделия.

Внешняя сертификация — отдельная запись с органом, документом, версией, областью и сроками. Gate PLATX не выдаёт сертификат ФСТЭК, DAL или MIL.

125Архитектурная концепция PLATX

Исторические ТЗ, ADR и снимки — old/ . Свежий измеренный срез — PLATX_CURRENT_STATE_310826.html , включая полный CLI и 54 module dossiers. Старый baseline 260826 сохранён в архиве.

126Что такое PLATX

PLATX — военизированная боевая платформа военного уровня надёжности. Её программное ядро — узкий модульный хост; домены подключаются по единому контракту и сохраняют управляемость при противодействии и частичных отказах. Общая архитектура действует для Linux и Windows, а OS backends и доказательства native qualification различаются. Детали Linux ниже описывают его реализацию.

1.1 Архитектурный контракт боевой готовности

Доверенный запуск проверяет аутентичность и семантику immutable profile до READY и внешних effects. Состав бинаря и load-time policy различаются.

Core ограничивает ресурсы; provider имеет deadline и отмену. Watchdog независим от контролируемого пути и не подменяет Recovery/Lifecycle.

Канал/Central могут быть потеряны: разрешённая локальная защита сохраняется, failover проходит ту же Policy и аутентификацию, автономный scope не продлевается.

Tamper отзывает полномочия и ключи затронутого контура; pre-exec verify связан с фактически исполняемыми байтами, cleanup проверяется независимо.

CLI/API/DSL/MSX/AI пользуются существующим effect/verify/undo путём. Военная интеграция не создаёт второй bus, registry, crypto или recovery stack.

Для каждой подсистемы конкретные отказы и требования к доказательствам собраны в архитектурных контрактах .

Поднять процесс, зарегистрировать модули, выдать им ABI.

Исполнять I/O через один слой (XIO), а не через libc в обход.

Держать владельца каждого fd/потока/подписки: (module, instance, generation) .

Менять состояние модуля только через lifecycle.

Решать рестарт/смерть только через recovery.

Запустить изолированный CHILD: sandbox → exec → handshake → proxy ABI → READY.

Посредничать syscalls CHILD (XIM), наблюдать точки (Hook), привязывать runtime к цели (Attach).

Держать сессию, mesh, шину, скрипт, ключи — как модули , не как второй Core.

Чего хост намеренно не умеет и не должен начать уметь «для полноты»:

решать политику из callback события;

тащить Event Hub / JSON-шину / пятый реестр;

открывать второе io_uring-кольцо в домене;

выполнять syscall вместо CHILD (это сделало бы XIM I/O-движком);

жить без поколения: «просто fd» без epoch.

Метафора Калашникова здесь конкретна:

мало деталей на выстрел — один путь connect/handshake/send или spawn/READY;

отказ однозначен — клинок затвора либо закрыт, либо открыт; «почти» нет;

грязный мир — ядро разное, uring может не быть; тогда hardcore честно не live, а не притворяется.

127Четыре исполнителя и почему их нельзя слить

Исторический провал таких платформ — один объект, который и ходит в ядро, и решает, и рестартит, и «ещё чуть-чуть инжектит». PLATX режет роли.

Вопрос: кто двигает байты . Модуль получает vtable: abi->xio->get(subsys, category) . Живой fd заявляет own_fd(fd, owner) . Байты — submit(owner, io, req) .

Нет таблицы / нет указателя — ошибка, не libc. Loopback без Core может ходить в слот: это честная дыра теста, не «сгенерированная generation=1».

Не каждый syscall — слот. fstat / unlink могут остаться на vtable без submit. Новый op — только с живым caller.

Hardcore ( xio_hardcore_* ): единственное место, где есть настоящее кольцо. Имя uring.async исторически означает пул , не ring. Нет кольца → xio_hardcore_active() == 0 . Init, который упал, но active==1 — полусостояние.

Правило XIM ( allow / deny +errno), правило Hook, seccomp-профиль CHILD. Policy не открывает файл «за» ребёнка.

Отвечает: что цель только что сделала . TAP не меняет возврат. Режимы сильнее TAP — вопрос capability, не предпочтения. Hook не грузит код в чужой процесс из этого заголовка. Не supervisor.

Три числа на факт: hook_id , attachment_id , owner.generation . Stale generation убивает callback.

Отвечает: можно ли CHILD вообще спросить этот syscall . Ребёнок ставит свой seccomp USER_NOTIF; родитель отвечает ALLOW/DENY. ALLOW = ядро исполняет настоящий syscall. Выдуманный retval сделал бы XIM backend — отвергнуто.

128Модуль

args->owner приходит снаружи. args->isolate выбирает профиль. requires[] проверяются до start() ( plat_cap_requires_met ): модуль не должен открыть половину и узнать, что keyring нет.

optional[] — путь без cap существует. Mandatory без cap — не start.

PIC-модуль экспортирует plat_module_descriptor . Isolation preference не отменяет маску isolation_allowed .

129Isolation: EMBEDDED и CHILD

Один и тот же модуль. Разное место жизни.

EMBEDDED — в процессе worker. Потоки через abi->task . I/O через полный XIO (get+own+submit, если профиль дал указатели).

CHILD — отдельный процесс. Core child-host:

Проверяет isolation_allowed и, если запрошен XIM, isolation.xim:v1 + READY.

Child: sandbox (user ns, landlock, seccomp, no_new_privs ) → optional XIM filter → exec.

READY только когда proxy usable и provides в реестре.

Смерть: revoke caps, plat_recovery_apply на snapshot row. RESTART = новый PID, новая generation (PID2).

Провал sandbox/exec/handshake: нет PID, нет capability, *out == NULL . EMBEDDED через этот вход отказан.

130Owner и leftover

Generation растёт при recreate. Stale callback мёртв. DESTROY снимает: fd claims ( xio_release_owner ), event subs ( plat_event_unsub_owner ), recovery watch.

Leftover — то, что осталось после конца эпохи и не должно. Ключ сессии, fd, READY, subscribe, claim. Операционный статус ( plat_ops ) честно говорит leftover, не прячет. Сенсор leftover эмитит , не apply и не свой backoff.

Цель platx-1.0: после disconnect/reap leftover keyed/fd/READY нет.

131Lifecycle

FAILED → RUNNING запрещён. Из FAILED — DESTROYED или STOPPED.

RESTART — последовательность внутри менеджера, не лицензия recovery звать stop / start самому.

Открытый риск (STATUS): конкурентные lifecycle/recovery мутации не всегда под одним per-instance lock. Это ближайший инженерный долг Core, не повод завести Event Hub.

132Recovery

Watch таблица по plat_owner_t . generation 0 отвергается. tick : health + тишина task > 2×heartbeat → apply. unwatch ждёт inflight tick.

Доказательства, которые нельзя потерять: crash → RESTART → PID2 + новая generation; budget=1 + второй crash → EXHAUSTED + FAILED, без нового PID.

133plat_event

Кольцо 64, payload ≤ 256, без malloc на emit. Полное кольцо: вытеснить старое, счётчик dropped , новый факт остаётся. Emit не ждёт consumer и не блокирует lifecycle.

Одно событие на факт. Не одно событие на каждый read цели — утопит кольцо.

134Четыре реестра

Capability ( plat_cap_* ) — живой смысл межмодульного API.

Commands — CLI глаголы. Имя уникально.

Legacy subsystem ( subsys_* ) — boot слоёв system→crypto→net→observe→automation→leftovers.

Module descriptor — новый контракт.

Пятый заводить нельзя. Схлопывание — v5, когда путь стабилен. Миграция не закончена: это долг, не лицензия плодить реестры.

Identity cap ≠ CLI. Пример: cap script , глагол script ; cap protocol.ra2c , глагол ra2c .

135Подсистемы — глубоко

Ниже: зачем существует, инвариант, как стыкуется, чего не делать. Статус на плане — в ROADMAP.

Режим ( xio_mode_* ) — свойство подсистемы ( "ra2c" , "mesh" ), не глобальный «весь процесс на uring». Имя режима важнее реестра бэкендов.

Слоты: read/write/recv/send/open/close/connect/accept — то, что реально гоняют сессия и файлы. Submit — одна дверь с owner. Чужой owner → XIO_ERR_OWNER_STALE , буфер не тронут (иначе гейт — декорация).

Таблица инстансов ограничена. Пустой список nr — не создать: ACTIVE без решений — ложь. Destroy жжёт id (не reuse), иначе поздний факт укажет на чужого посредника. Serve: stop receive → drain → close listener → release owner. Иной порядок отпускает child с ENOSYS на уже разрешённом вызове.

Map: логическое имя → ресурс, открытый через XIO . Notify-канал не читает path ребёнка как путь к open.

Три числа на факт: hook_id , attachment_id , owner.generation . Stale generation убивает callback. Hook не supervisor и не инжектор.

ATTACHED — не bool. Ноль дескриптора = DETACHED (enum 0). v1 providers: attach.mock , attach.child . Generation 0 на commit — отказ. Новые providers (браузер, perl) в v1 не добавлять — слоты scope в заголовке зарезервированы, это не лицензия писать тело.

Сессия: ключ + fd + поколение. ChaCha20-Poly1305, PSK HMAC-SHA256. Клиент не keyed до PEER_AUTH_ACK. Identity reject закрывает обе стороны. Legacy ECDH — только явный lab opt-in; иначе refuse, без leftover keyed.

Switch канала: новый fd готов и claimed, затем обмен; иначе keep-old.

push для MBus: corr_id и deadline обязательны (0 = ARGS). Нет сессии — не silent accept.

P2P узел: listener, dial, X25519 handshake, AEAD сообщений. Сокеты через XIO capability. Claim каждого рождённого fd с generation модуля; таблица есть и gen 0 — закрыть fd, не оставить висящим. Задачи accept/dial/prune — abi->task . Нет внутренних include RA2C.

Health publish без MBus emit — явный -1 , не weak-success.

Локальные акторы и опциональный net-link. I/O через ABI. leftover_owner — epoch для task и claim, когда Core leftover жив.

MBus-over-RA2C: очередь 32 × 256 байт; overflow = FULL; нет cap/push = NOLINK; потеря provider на drain = DEGRADED, элементы остаются. Это адаптер, не новый transport.

Crypto — один стек. Не плодить второй AEAD «для модуля».

Это наблюдение ядра, не позвоночник и не C2.

12.11a SENSE — M5, входной gate закрыт

Текущее состояние: lifecycle, generation fencing, platform health, stale XIM epoch, CLI smoke и clean shutdown доказаны полным DQ-прогоном 48/0. Maturity M5; active spine перешёл к R1 — см. ACTIVE_DELIVERY .

Разрешены bounded fixes и qualification; расширять product spine и менять общие provider ABI до provider-fabric0 запрещено.

Факт «произошло». Не decide. Мост в MBus — optional, weak emit = не опубликовано.

Загрузка доверенного образа. Trust привязан к байтам, не к пути. Исторический server drift plug_arc_t закрыт; тип из дерева удалён. Не ослаблять verify.

Автоматизация поверх Core. No-weaken: скрипт не снимает sandbox. Транзакции маршрутов — commit+rollback, не «половина применилась».

12.16 Observe, context, lease, surface, gadget

Наблюдение FS, контекст, аренда, поверхность, gadget_id. На platx-1.0 нужен capability.request → gadget_id как конец операторского пути. Не превращать это в пятый реестр.

Профили CHILD и worker. Floor тестов — нижняя граница. «Чтобы встала» ослаблять нельзя.

Хранилища и виртуальные деревья. I/O через XIO. Новые leftover- close() двери не изобретать: волны close уже закрывали этот класс дыр.

Учёт задач, не замена abi->task . EMBEDDED spawn идёт в Core task API.

Защита бинаря, не Core. Не смешивать с recovery decide.

12.23 SelfProtect — LAB, вне product manifest

SP_PROFILE_ARENA в замороженном SP0 ABI — исторический остаток имени; означает «provider/action allowlist из task». Он не является игровым runtime. Будущая ARENA R10 подключается отдельным synthetic adapter и не использует это имя как обход production policy (C20).

12.24 Mirage — LAB, вне product manifest

Модель обязана различать fiction и truth: every synthetic artifact имеет watermark/provenance; interaction привязан к world generation/session/operation; восемь классов Oracle invariants проверяют согласованность; resource envelope ограничивает время, команды, bytes, entities и modeled side effects; ledger/checkpoint фиксируют evidence. SMB — внутренний typed binary protocol модели, не новый platform event bus и не пятый registry.

12.25 Cognitive Plane — contract-only

AI в PLATX — не модуль над системой, а ещё один ограниченный оператор поверх тех же typed API, которыми пользуются CLI и Console. Языковая модель является ненадёжным компилятором из естественного языка в формальный IR; всё остальное — детерминированный код: каталог возможностей, компилятор плана, валидаторы, Policy/SelfProtect, Action Coordinator, независимые witnesses, FORENSIC.

Разделение владения жёсткое: SENSE наблюдает, SGrap помнит и связывает, AI предполагает и планирует , EDR/XDR владеет расследованием, SelfProtect авторизует, Action исполняет, witnesses проверяют, FORENSIC доказывает, Console объясняет человеку. AI владеет ровно двумя слоями из десяти; в остальных он клиент.

Статьи C24–C28 задают границу: предложение вместо эффекта, запрет LLM в горячем пути, текст модели не является доказательством, недоверенные данные не являются инструкциями, отключаемость без потери защиты. Модельный рантайм существует только как CHILD за isolation.xim:v1 и не линкуется ни в один shipped profile — поэтому C19 не нарушается: внешние зависимости приобретает опциональный child, а не платформа.

12.26 Предложенные к реализации (только путь)

Не «новые продукты», а дыры канона:

Единый lock/executor на concurrent lifecycle+recovery.

E2E platx-1.0 на ./platx (не только appliance).

Миграция legacy subsystem → descriptor без пятого реестра.

Live View имеет scopes FLEET , SELECTION , COMPOSITE ; Node Live View продолжает работать без Central. Общий time cursor синхронизирует Atlas, Dive, SGrap, Event Rail и media; AS_KNOWN_THEN не смешивается с поздней реконструкцией.

Firewall0 — узкий action provider: Netlink/nftables PLATX-owned rules, flow/CIDR/port/process restriction, isolate-with-preserve, TTL, verify и rollback. Это не второй сетевой стек и не замена полного firewall-продукта.

136Профили сборки

platx-core — позвоночник: ABI, XIO, log, task, child-host

platx-alpha — core + keyring + transport.core + mesh + ra2c + script

platx-agent-ra2c — тонкий клиент сессии, без listen

platx-server — сервер: core + plugin/ELF + transports; hades/fuse отказывает честно

platx (full) — манифест продукта; memfd — только alias

137Зависимости

Обязательные: C11, pthreads, POSIX/Linux headers.

Запрещены как мозг: libbpf (Hades — свой loader), внешняя шина событий, второй TLS/crypto stack «на всякий случай», runtime-скрипты, тянущие сеть в обход XIO.

LLM-рантайм, tokenizer, векторная БД и ML-фреймворк — не зависимости платформы: они существуют только внутри опционального CHILD-провайдера ai.model_provider и отсутствуют во всех shipped profiles (C28).

138Машина состояний (рёбра)

RESTART — последовательность внутри менеджера (STOP…START или DESTROY…CREATE…START — как реализовано в Core), не лицензия recovery звать хуки дескриптора. Хук уже отработал и откатить нельзя — FAILED , не тишина.

Health: OK / DEGRADED / FATAL . FATAL не «ещё чуть поживём».

139Boot, leftover, ops

Worker поднимается слоями (legacy subsystem: system → crypto → net → observe → automation → leftovers). Это не пятый смысл и не замена descriptor. Новый код садится дескриптором + cap, не новым слоем.

PLAT_EV_BOOT несёт имя стадии после входа. Слово "ready" — только когда платформа реально platform_ready . Очередь событий не является условием READY.

сенсор видит ключ, fd, READY, subscribe, claim после конца эпохи;

эмитит факт, не recovery.apply , не свой backoff;

операционный статус честно показывает leftover, не прячет «чтобы зелёное».

Цель пути: после disconnect / DESTROY / child reap leftover keyed/fd/READY = 0.

140PLATX — контракт модуля боевой платформы

До product implementation module/capability обязан иметь действующую фазу в ROADMAP , upstream/downstream gate, owner и контракт приёмки. Он наследует доктрину , MIL-1…10 и MG-0…7 . Архивные ACTIVE_DELIVERY/governance/capsules сохраняют rationale прежних волн; актуальные правила задают Конституция и ROADMAP. Наличие корректного module descriptor не является разрешением добавлять модуль в product spine.

Документ описывает как модуль садится на платформу . Он не описывает, что модуль делает. Реализацию доменного модуля по этому документу писать не нужно и не следует: контракт существует раньше первого потребителя.

Правило приёмки: пункт красный — модуль не сажается. Полусостояние («почти работает», «пока без own», «временный fallback») отклоняется так же, как явная ошибка. Отсутствующий указатель — не успех.

141ROADMAP по подсистемам

Core (lifecycle, recovery, event, leftover, child-host)

LIVE. SM, watch, bounded event, child spawn+sandbox — сериализация concurrent lifecycle/recovery (риск в STATUS) — collapse реестров

LIVE. get/own/submit, hardcore fallback честный; test-xio-owner выровнен с gen0 ARGS; loop create/destroy без утечки fd ( test-xio-loop-fdbalance , рантайм-баланс) — rename uring.* ; 24h soak — новые XIO providers (epoll/mmap)

WIRED. TAP/mock, boot, CLI attach — не расширять REPLACE/PROXY без cap — uprobe live только с привилегиями

WIRED. mock + child, SM + rollback — закрыть E2E attach.child на продукте — другие providers — не v1

WIRED. модуль protocol.ra2c , PSK, peer-auth-ack — leftover keyed=403; agent listen ENOTSUP держать — не HMAC-стек; не rename tree

WIRED. XIO через ABI; mbus_ra2c bounded — adapter в product только с живым caller — не новый transport

WIRED. cap "script" , .ms , noexec — ra2c.run() refuse держать — не делать скрипт вторым Core

WIRED в core/agent — держать alpha-профиль зелёным в test-profiles — не второй crypto stack

LAB — не плодить адаптеры — catalog не раздувать

Central Live / Fleet Orchestration / Firewall

Ключевые документы: ТЗ Cognitive Plane · карта швов · волны AI001–AI150 · исследование 12 аналогов . Доменные: EDR/XDR · SelfProtect · Mirage .

LIVE как картина — CLI events после loopback crash→PID2 — history/trace — потом

LIVE — не через event decide — schema/mbus — optional

WIRED — держать server-профиль зелёным в test-profiles-shipped — PIC bind не ослаблять trust

WIRED — не ослаблять no-weaken — новые verbs только под путь

Observe / context / lease / surface / gadget

LAB — gadget_id на platx-1.0 пути — не пятый реестр

LIVE в child-host — не слабее текущего floor — новые профили — явный opt-in

WIRED — leftover close дверей не изобретать — не новый store cap без require

в full manifest — не делать вторым net brain — raw docs см. TXPSTACK_RAW

в дереве — не смешивать с Core decide — отдельное решение

142Определить объект и применимость

Зафиксировать версию и состав изделия, функции защиты, поддерживаемую ОС, профиль поставки, границу доверия и эксплуатационное назначение. Раздельно определить требования к средству защиты, процессу безопасной разработки и информационной системе, в которой применяется PLATX.

Для AV, обнаружения вторжений, межсетевого экранирования и других функций подобрать применимые требования и профиль/задание по безопасности. MIL_1…4, TL0…4 и DAL_B не являются уровнями доверия ФСТЭК. Уровень выбирается по контексту применения, а не автоматически по слову «военный».

143Сформировать комплект

Перечень применимых документов с полным текстом, редакциями, изменениями, датой проверки, владельцем и обоснованием применимости каждого требования.

Модель угроз и архитектура доверия: управление доступом, identification, integrity, trusted boot, аудит, обновления и зависимости.

Материалы безопасной разработки по ГОСТ Р 56939-2024: требования, review, управление исходниками/сборкой, анализ состава, проверки и работа с уязвимостями.

Анализ недекларированных возможностей в установленной области; независимые проверки и traceability до release artifacts.

Эксплуатационная документация, безопасная конфигурация, provisioning, отзыв, восстановление и ограничения среды.

План испытаний с точными targets, процедурами, evidence и ответственными; отдельный анализ влияния каждого изменения на объект оценки.

144Предварительный hazard log

MIL-STD-882E относится к system safety: выявление hazards, устранение или снижение риска и документированное принятие остаточного риска. Редакцию и Change 1 нужно фиксировать через официальную запись ASSIST . Это не шкала «military-grade cybersecurity».

H01: ложная авторизация process effect. Последствие — нарушение доступности/целостности. Контроль: local policy, generation, verify/rollback. Вероятность и severity не оценены без operational context.

H03: неочищенный CHILD/ресурс после сбоя. Контроль: bounded teardown и leftover tests. Требуется target evidence.

H04: ошибочный формат evidence. Контроль: compiler layout probes и golden codecs. Audit80/assert88 открыт.

Для каждой записи добавить scenario, initial risk, mitigation owner, verification, residual risk и уполномоченного принимающего. Не перемножать вымышленные численные probability/severity и не объявлять риск приемлемым за владельца.

145PLATX — дерево опасного отказа при tamper

Верхнее событие: после обнаружения tamper затронутый контур продолжает неразрешённый effect либо позволяет использовать отозванный ключ. Ни вероятность, ни severity, ни приемлемость риска здесь не назначены.

146Ветви OR верхнего события

Отзыв не блокирует effect: stale generation, кэш полномочий, verify/use race либо обход provider. Проверка: параллельные revoke/execute, replay старого плана, отказ verifier; эффект после отзыва отсутствует.

Ключ остаётся доступен: неучтённая копия, ошибка wipe, продолжение работы другого потока или восстановление старого контекста. Проверка: инвентаризация владельцев, запрет новых readers, проверяемая очистка, новый key epoch при recovery.

Очистка не заканчивается: deadlock, ожидание потерянного sink, бесконечный retry либо недостаток ресурсов. Проверка: deadline и независимое наблюдение cleanup, явный FAILED при невозможности завершить.

Комбинации ветвей, общие причины отказа и минимальные сечения уточняются для конкретного deployment. Один успешный unit test не закрывает дерево.

147Модель угроз

Модель применения боевой платформы.

Согласно доктрине , противодействие включает partition/blackout, компрометацию authenticated peer/Central, profile downgrade/rollback, supply-chain tamper, exhaustion, подавление watchdog/audit и replay после restart. Для deployment назвать доверие к ОС/аппаратуре, допустимую автономность и ущерб от отказа миссии. Hazard analysis ведётся отдельно в MILSTD882E_ANALYSIS ; цепочка tamper→revoke→zeroize→recover проверяется по FTA .

Объекты защиты: owner/generation, ключи сессии и профиля, политика, evidence, CHILD boundary и доступность наблюдения. Недоверенные входы: сетевой RA2C frame, IPC, plugin/archive, DSL/MSX, содержимое файлов и имена процессов. Даже корректно аутентифицированный peer не получает безусловное локальное полномочие.

Linux: вредоносный процесс может создавать поток событий, менять файлы между проверкой и использованием, подавлять доступные сенсоры или провоцировать resource exhaustion. Windows: потеря ETW coverage и неправильная настройка Job Object/token требуют независимой проверки. Наличие ETW события не доказывает полноту наблюдения; отсутствие события не доказывает отсутствие действия.

Privileged BPF loading и module signing — отдельные trust boundaries. Инсайдер с ключом подписи может создавать валидные артефакты; требуются разделение ролей, provenance и отзыв. Supply-chain компрометация toolchain не устраняется одним HMAC. Ограничения kernel/root threat model должны быть зафиксированы для конкретного deployment, а не объявлены универсально закрытыми.

148Разные назначения примитивов

Profile authentication: HMAC-SHA256; Perl profile MAC покрывает первые 88 байт 120-байтного wire object. Затем отдельно проверяются semantics.

Session protection: согласованные handshake, identity/PSK, key derivation, AEAD nonce/tag и anti-replay. Ephemeral X25519 сам по себе не аутентифицирует peer.

Artifact signature: Ed25519 либо иной явно согласованный loader contract; подпись, доверие signer и полномочие выполнить artifact проверяются раздельно.

Zeroize: прекращение использования и проверяемая очистка контролируемых копий с учётом потоков, providers, dumps, swap и жизненного цикла target.

HMAC — MAC с общим секретом, а не цифровая подпись с публичной проверкой. Ненулевой MAC не доказывает подлинность. ALGORITHM_PRESENT не означает CONSUMER_ENFORCED или EXTERNALLY_CONFIRMED.

149Linux и Windows

Состав crypto определяется manifest и фактическим consumer. Универсальные утверждения «весь Linux использует libsodium» и «весь Windows использует BCrypt» не являются контрактом. Выбор OS provider и доступность алгоритма проверяются на заявленном SDK/OS с отрицательными и interop vectors.

P-256 ECDH не заменяет X25519, ECDSA P-256 не заменяет Ed25519, AES-GCM не заменяет ChaCha20-Poly1305 внутри прежнего wire suite. Смена алгоритма требует versioned negotiation и interop evidence. Отсутствие нужного provider даёт отказ, а не незаметную подмену. Известные ошибки сравнения MAC и timing claims остаются в техническом справочнике .

150ГОСТ, FIPS и доверие

ГОСТ Р 34.10/34.11/34.12 оцениваются в своей области; MIL_FLAG_GOST не доказывает наличие реализации или сертификацию. FIPS 140-3 рассматривается для конкретного криптомодуля и конфигурации, а не для названия алгоритма. ФСТЭК и требования к криптографическим средствам учитываются отдельно по программе стандартов . Второй crypto stack в обход C19 запрещён.

151Процесс выпуска

Обязательный допуск боевой поставки.

Выпускная квалификация следует MG-0…7 . До допуска собрать source/binary/profile hashes, native target evidence, fault injection, endurance, coverage, audit continuity, revoke/zeroize и проверенный install/rollback. Незакрытый обязательный gate блокирует боевой статус; development artifacts маркируются отдельно. ФСТЭК/ГОСТ/MIL/DO-178C claims подтверждаются в своей области по STANDARDS , а не по флагу.

Перед выпуском release owner подтверждает scope, version source, source/binary/profile hashes и результаты обязательных gates. Отсутствующий version header не заменяется выдуманным номером. Подпись и публикация допускаются после решения о выпуске; развёртывание выполняется в согласованном контуре с проверенным rollback.

Checklist: clean reproducible source baseline; профильные behavior/negative tests; layout/interop; install/upgrade/rollback; target qualification; known limitations; signing provenance; release notes. Ed25519 signature проверяет соответствующий loader/tool, а не общий флаг MIL.

Changelog разделяет Added/Changed/Deprecated/Removed/Fixed/Security и ссылается на реальные изменения. Неполная документация или открытый production defect остаются в backlog. Выпускные сценарии принадлежат release owner, CI/packaging — владельцам сборки и поставки; независимый reviewer проверяет доказательства допуска.

152Подготовка боевой поставки

Выбрать квалифицированный binary/build profile и target среду.

Подготовить scope, mission budgets, watchdog, audit/signing и TL policies.

Аутентифицировать versioned load-time profile и проверить его semantics.

На фактическом launcher подтвердить immutable policy до READY и effects.

Проверить malformed/forged/stale profile, provider loss, отказ аудита и невозможность MIL→CIVIL fallback.

Допустить миссию только по её target evidence и MG-0…7 .

153Изменение профиля и завершение

Смена immutable policy требует нового доверенного запуска. TL может меняться только через предусмотренную политику, не подменяя scope или бюджет. После отмены/expiry запрещаются новые effects и проверяется bounded cleanup. При tamper старые ключи и generation не переиспользуются.

CIVIL не отменяет owner/generation, доверенный startup и аудит effects. MIL_1…4, DAL_B и GOST flags не означают сертификат ФСТЭК или доказанное соответствие внешним стандартам.

154JSONL audit output

Time bounds are inclusive. Records accept Unix seconds or ISO8601 with a timezone; date-only bounds mean midnight UTC. Comparisons normalize timezone offsets instead of comparing strings. Missing/invalid timestamps under a time filter, reversed bounds and malformed records return2. Table cells escape control characters and have bounded widths. Default JSONL output escapes Unicode so Unicode line separators cannot split a record for line-oriented consumers.

155Реагирование на инцидент

TL0–TL4 описывают уровень угрозы; повышение числового уровня само по себе не доказывает kernel deny. Оператор сопоставляет факт, политику, разрешённый effect и независимую проверку. Изоляция должна сохранять предусмотренный control channel либо честно сообщать потерю связи.

После действия: проверить effect, cleanup, остаточные fd/keys/CHILD, сохранить receipt и причину частичного результата. При отсутствии полномочия или provider — остановить effect и эскалировать владельцу. Этот runbook не запускает автоматическое отключение процессов.

156Криптографические ГОСТ — отдельная область

MIL_FLAG_GOST не доказывает поддержку алгоритма, соответствие ГОСТ Р 56939 или сертификацию. Алгоритмы ниже — прежние целевые требования.

ГОСТ Р 34.11-2012 (Streebog-256) — Profile HMAC — NOT STARTED

ГОСТ Р 34.12-2015 (Grasshopper) — Session encryption — NOT STARTED

ГОСТ Р 34.10-2012 — Signatures — NOT STARTED

157PLATX — архитектура аудита, MIL-9

Боевой контракт: доктрина , MIL-9 , MG-5 .

Аудит фиксирует решения и effects с owner/generation, sequence, результатом и целостностью. Секреты в payload запрещены. Append-only долговременный журнал и bounded staging ring имеют разные гарантии.

158Обязательное поведение

Sequence монотонен в своей epoch; export сохраняет boot/owner identity.

MAC проверяется криптографически по нужным bytes/key; non-zero недостаточно.

Persistence требует sink, подтверждения записи, retention и проверки restart/crash. RAM ring теряется при аварии питания.

Потеря обязательного аудита блокирует новые зависимые effects; cleanup имеет собственный deadline и не ждёт недоступный sink бесконечно.

Lock-free cursor не доказывает корректность concurrent payload writers или предел 1 µs. Нужны memory-ordering review и измерение на target.

159Компоненты и проверки

В дереве описаны audit_ring, audit_sign, audit_export и audit_query; их wiring и layout проверяются на shipped profile. Кандидаты тестов: t_audit_append, t_audit_sign, t_audit_overflow и t_audit_export. Наличие имён не означает выполненный gate.

MG-5 требует wrap/concurrency, corrupt MAC, replay/order, sink loss, crash до/после commit/receipt, restart continuity и отсутствие секретов. Hazards — MILSTD882E_ANALYSIS , audit safety candidates .

160Crypto — Primitives (Architecture)

Криптографический provider хранит определённый набор ключей/контекстов; ошибка RNG, verifier или integrity ведёт к отказу до использования результата. Второй crypto stack не создаётся.

MG-1/4: known-answer/negative vectors, key revoke, zeroize всех контролируемых копий, nonce exhaustion и несовместимые suites; наличие алгоритма не является сертификатом.

Ниже — технический срез реализации; перечисление функций и тестов не закрывает эти требования автоматически.

161DRM/KMS Audit

Загрузка и права модуля связаны с аутентифицированным artifact, signer, owner/generation и текущей политикой. Tamper отзывает затронутые полномочия и ключи.

MG-1/4: неподписанный/отозванный artifact, TOCTOU, tamper во время загрузки, повтор старого context и чистый teardown.

162Реализация

Чтение состояния через /sys/kernel/debug/dri/<N>/state (debugfs, read-only). Разбор текстового вывода: обнаружение ключевых слов crtc[ , gem , prime . Статический буфер s_dbg_buf[8192] (§SEC-3).

163DSL Engine — Architecture

MG-2/4/5: malformed input, budget exhaustion, cancel/retry, отсутствие provider, stale generation и verify/undo без частичного успеха.

164DSL Engine — Architecture / Overview

Stack-based bytecode VM for evaluating security rules at runtime. Rules are compiled to bytecode before deployment (INV-DSL-01). Stack overflow disables the rule rather than crashing (INV-DSL-02).

165ELF Binary Integrity Verifier

ELF loader обязан связать проверку artifact с отображаемыми и исполняемыми байтами, контролировать размеры, relocations и разрешённые зависимости.

MG-1/4: повреждённые заголовки, overflow, verify/use race, недопустимая relocation, ошибка mapping и полный rollback.

166Результаты верификации

PLATX_ELF_OK — ELF валиден, подпись присутствует

PLATX_ELF_INVALID — Плохой magic или неподдерживаемый тип

PLATX_ELF_TRUNCATED — Файл слишком короткий

167FABRIC — Provider Fabric Architecture

Provider Fabric сохраняет единые capability contracts и локальную authority; живучесть не разрешает выбирать недоверенный или устаревший provider.

MG-2/3: revoke/provider loss во время вызова, stale generation, отказ резервов, bounded failover и отсутствие повторного неидемпотентного effect.

168FABRIC — Provider Fabric Architecture / Overview

The provider fabric is the transaction and evidence layer for all provider operations. It implements 2-phase commit, an append-only journal, crash recovery, GC, and snapshots. Static BSS journal buffer (§SEC-3).

169FABRIC — Provider Fabric Architecture / Invariants

INV-FABRIC-01 — Every provider operation has a witness journal entry ( FAB_JF_WITNESS flag)

INV-FABRIC-02 — Journal full → operation rejected with FABRIC_ERR_JOURNAL_FULL (no silent drop)

§SEC-3 — Journal buffer is static BSS: 4096 × 128 bytes = 512 KB

170FUSE Filesystem Monitor

Filesystem mediation наследует trust boundary CHILD/host и ограничивает очередь запросов; отсутствие daemon/provider не снимает защиту.

MG-2/3: daemon crash, timeout, concurrent unmount, malformed request и отсутствие подвисшего cleanup.

171FUSE Filesystem Monitor / Назначение

Мониторинг операций FUSE-файловых систем: lookup, open, read, write, unlink, mkdir, rmdir, rename, mknod. Используется для обнаружения подозрительных overlay-FS и FUSE-руткитов.

172Реализация

Открывает /dev/fuse в non-blocking режиме. platx_fuse_monitor_poll() читает fuse_in_header из статического буфера s_buf[65536] и транслирует opcode в platx_fuse_op_t . Timestamp: CLOCK_MONOTONIC .

173Реализация / Инварианты

INV-HADES-01 — каждый EXEC пишется в audit ring ( hades_event_to_audit ).

INV-HADES-02 — PTRACE поднимает ThreatLevel до TL3 ( hades_threat_score ).

INV-HADES-03 — при TL4 новые EXEC блокируются через BPF LSM (нужен CAP_BPF; сейчас BLOCKED в неприв. VM, доказывается на qualification-боксе).

§MIL-9 — в Military mode каждое событие уходит в подписанный audit ( hades_mil_audit , подписант — weak seam к crypto/keyring).

174Hook — Hook Executor Architecture

Hook наблюдает и ограничивает по детерминированной политике; callback не получает полномочия на lifecycle/recovery.

MG-2/4: inflight unsubscribe, stale generation, callback pressure, потеря policy provider и закрытая граница при отказе.

175Hook — Hook Executor Architecture / Overview

The Hook Executor provides a prioritised, timeout-safe event dispatch mechanism. Subsystems register hooks against event masks; the dispatcher invokes them in descending priority order. Misbehaving hooks (timeout or panic) are isolated without crashing the process.

176Hook — Hook Executor Architecture / Security Invariants

INV-HOOK-01 — If a hook's wall-clock execution time meets or exceeds its timeout_ms , the hook is disabled and its timeout_count is incremented. Default timeout: HOOK_DEFAULT_TTL_MS = 500 ms .

INV-HOOK-02 — If a hook returns rc < -100 (panic indicator), it is isolated (disabled, disable_count++ ) immediately. The remaining hooks in the dispatch chain still execute.

§SEC-3 — No malloc() . Hook table: hook_entry_t g_hooks[HOOK_MAX_REGISTERED] (64 slots) in static BSS.

177Hook — Hook Executor Architecture / Components

Defines g_hooks[64] , g_n_hooks , g_hlock (pthread_mutex). hook_register() , hook_unregister() , hook_enable() , hook_disable() .

Builds a priority-sorted invocation order (bubble sort over ≤64 slots — O(n²) is acceptable for N ≤ 64). Highest priority value fires first (0–255 scale). Enforces INV-HOOK-01 (elapsed ≥ timeout → disable) and INV-HOOK-02 (rc < -100 → isolate). hook_stats_get() returns aggregate call / timeout / disable counts.

178Log Architecture

Логирование объясняет отказ и не блокирует критический путь; debug log и обязательный аудит различаются по гарантиям сохранности.

MG-2/5: sink loss, log flood, rotation/crash, bounds и отсутствие секретов в output; запись строки не подменяет evidence receipt.

179MBUS — MPSC Message Bus Architecture

MBus переносит типизированные сообщения с bounded queues, correlation и сроком жизни. Потеря peer не создаёт второго управляющего контура.

MG-2/3/6: full queue, reorder/replay, provider loss, expired request и отсутствие повторного effect при retry.

180MBUS — MPSC Message Bus Architecture / Invariants

INV-MBUS-01 — Ring overflow → oldest event dropped silently. Producer never blocks.

INV-MBUS-02 — Consumer lag > MBUS_WARN_THRESHOLD (25% capacity) → watermark warns; caller raises ThreatLevel.

§SEC-3 — No malloc in publish/consume hot path. All state in static BSS.

181Жизненный цикл секрета

ftruncate(fd, size) → выделить место

mmap(PROT_WRITE, MAP_SHARED) → записать секрет → munmap

После запечатывания любой mmap(MAP_SHARED|PROT_WRITE) возвращает MAP_FAILED (EPERM) .

182MESH — Provider Mesh Architecture

Mesh выбирает проверенный доступный provider в пределах локальной политики и бюджета миссии. Blackout не увеличивает полномочия.

MG-2/3: частичный partition, все провайдеры DOWN, stale health, circuit exhaustion, ограниченные retries и проверенный reserve.

183MESH — Provider Mesh Architecture / Overview

The provider mesh manages the lifecycle, health, routing, load-balancing, failover, and circuit-breaking for all registered platform providers. All state lives in static slot tables (§SEC-3: no malloc in hot paths).

184MIRAGE — Sandbox + Rehearsal Architecture

MIRAGE обеспечивает изолированное воспроизведение миссий; синтетика не выдаётся за real-world evidence, Judge не передаёт ground truth агенту в сессии.

MG-4/6: отказ sandbox, смешение namespace, cleanup после kill, watermark provenance и независимая оценка Red/Blue.

185MIRAGE — Sandbox + Rehearsal Architecture / Overview

MIRAGE provides a rehearsal-before-action sandbox for PVNDORABOX/PLATX. In MIL mode every destructive operation must first be rehearsed inside an isolated sandbox. If the rehearsal fails or times out, the real operation is blocked.

186MIRAGE — Sandbox + Rehearsal Architecture / Security Invariants

INV-MIRAGE-01 — Sandbox exit_code != 0 → rehearsal FAILED . The real operation is blocked.

INV-MIRAGE-02 — Any attempt to write outside sandbox_root is an immediate error. Enforced via read-only bind mounts and path prefix checks.

INV-MIRAGE-03 — Sandbox runtime > MIRAGE_MAX_TTL_MS (10 s) → SIGKILL . The watchdog enforces this unconditionally.

TL ≥ 4 (LOCKDOWN): all sandbox execution disabled.

MINIMAL mode: MIRAGE is compiled out ( PLATX_HAS_MIRAGE undefined).

187MIRAGE — Sandbox + Rehearsal Architecture / Components

Applies a minimal syscall allowlist (read, write, exit, brk, mmap, munmap, mprotect, close, openat, fstat, lseek) via seccomp(SECCOMP_SET_MODE_FILTER) . Must be called inside the child after namespace entry.

8-slot static world-model table. Each world defines root_path , work_dir , uid/gid mapping, and readonly flag. No malloc (§SEC-3).

mirage_backend_enter(root) performs chdir + chroot . Production use of pivot_root is noted but test stubs use chroot for portability.

mirage_exec(req, res) forks a child, enters the sandbox world, and waits with a timeout watchdog (INV-MIRAGE-03).

Read-only probe mode. mirage_probe_is_writable() enforces INV-MIRAGE-02 via path-prefix comparison.

128-slot static record table. Stores rehearsal passed / failed status, elapsed_ms , and label. mirage_record_stats() returns aggregate counters.

Compares rehearsal vs real run: exit codes and elapsed time within tolerance.

2 048-entry × 32-byte lock-free audit ring (atomic cursor, §SEC-3 / §MIL-9).

16-slot static sandbox-process table. mirage_gc_run(now_ms, max_ttl) reaps exited processes and SIGKILLs over-TTL ones. No malloc (§SEC-3).

mirage_watchdog_wait(pid, ttl_ms) polls with 5 ms granularity and sends SIGKILL if deadline is exceeded (INV-MIRAGE-03). Returns -ETIMEDOUT (-110).

Exports five counters to plat_compstat via callback: mirage.rehearsals , mirage.passed , mirage.failed , mirage.gc_freed , mirage.watchdog_kills .

188MODHOST — Module Host Architecture

Modhost исполняет только разрешённые проверенные artifacts и не наследует authority из одного факта подписи. Перезагрузка меняет generation.

MG-1/4: invalid signer, revoke, verify/use race, tamper при execute, отсутствие verifier и rollback без keyed leftovers.

189MODHOST — Module Host Architecture / Invariants

INV-MODHOST-01 — Module without valid MSX signature → REFUSE

INV-MODHOST-02 — Module without lease → REFUSE

INV-MODHOST-03 — Unload failure → force unload + panic audit entry

190MSX — Module Signature eXchange Architecture

MSX сохраняет bounded mission execution и versioned module contracts. Сценарий не становится shell-строкой или обходом Policy.

MG-2/4/5: deadline, budget/retry exhaustion, cancel, replay плана, provider loss и полный receipt/undo.

191MSX — Module Signature eXchange Architecture / Overview

MSX provides Ed25519-based module signing and verification (§MIL-10). A revocation list (256 fingerprint slots) and hot key rotation (8 key slots) operate entirely from static BSS — no malloc (§SEC-3).

192Netlink Event Subsystem

Kernel control и telemetry разделяют observation и effect; сообщения проверяются по размеру, источнику и полномочиям.

MG-2/3/5: malformed/truncated frame, queue overflow, исчезновение сокета/ядрового provider, ошибка подписки и bounded teardown.

193Plugin System Architecture

Plugin наследует module lifecycle, capability и signed artifact contract; зависимость не получает неограниченное выполнение.

MG-1/4: недоверенная зависимость, loader race, неизвестная ABI version, revoke во время вызова и rollback загрузки.

194Plugin System Architecture / Overview

The plugin system provides versioned ABI-stable extension points. Plugins use a major.minor API version; mismatched major versions are refused. Untrusted plugins run inside MIRAGE sandboxes.

195POE — Proof of Execution (Architecture)

POE исполняет только собственный публичный контракт; имя модуля не доказывает Proof-of-Execution. Evidence результата даёт соответствующий проверяющий.

MG-2/4/6: неверные длины/контексты, resource bounds, ошибки провайдера и отсутствие ложного proof при успешном преобразовании буфера.

196POE — Proof of Execution (Architecture) / Overview

SHA-256 hash chain over execution receipts. Every EFFECT receipt must be preceded by CLAIM and DECISION (INV-POE-01). Chain tip is anchored into the audit ring for tamper evidence.

197Proxy/X — Architecture

Proxy работает только в допустимом профиле, scope и сетевом бюджете; ограничения MIL enforcement проверяются у каждого consumer.

MG-2/4/5: запрещённый mode, session expiry, sink/provider loss, parser limits и закрытие канала без остаточных полномочий.

198SelfProtect Subsystem — Архитектура

SelfProtect защищает доверенный контур, отзывает полномочия при tamper и координирует проверяемую очистку; abort не доказывает zeroize.

MG-4/5: зависший watchdog, подмена профиля/кода, недоступный audit, readers во время wipe и доверенное восстановление.

199SelfProtect Subsystem — Архитектура / Инварианты

INV-SP-01 — profile_t mprotect PROT_READ после загрузки

INV-SP-02 — При tamper detection → pxsp_lockdown_engage() → abort()

INV-SP-03 — Capabilities dropped до минимума

200Supervisor Architecture

Supervisor показывает состояние миссии, providers и coverage, оставаясь наблюдателем. Картина GREEN требует свежих фактов.

MG-3/5: stale telemetry, исчезновение подписчика, частичный blackout, неверный healthy и отсутствие lifecycle calls из callback.

201Task Manager Architecture

Task management исполняет bounded lifecycle через Core; owner/generation, deadline и cancellation обязательны для работы миссии.

MG-2/3: task exhaustion, зависание, потеря CHILD, повторный cancel, bounded join и отсутствие unmanaged threads.

202Trace Architecture

Trace ограничивает overhead и связывает события по owner/generation; трасса не является автоматическим доказательством полного аудита.

203Transport / RA2C Architecture

Транспорт сохраняет аутентификацию при переключении, ограничивает retries и предотвращает replay между session/epoch. RA2C остаётся модулем над XIO.

MG-2/3/4: loss/reorder/replay, key/nonce exhaustion, reconnect, verify failure, keep-old и закрытие при отсутствии пригодного резерва.

204Key Exchange

X25519 ECDH (ephemeral, one keypair per session)

HKDF-SHA256: salt=session_id(+PSK if set), info="RA2C-v1-session"

AEAD: ChaCha20-Poly1305, nonce per-frame (12 bytes prepended)

205Key Exchange / Invariants

INV-RA2C-01 — seq_tx ≥ RA2C_SEQ_MAX → close (no wraparound)

INV-RA2C-03 — bad magic/version → silent drop (no oracle)

INV-TRANSPORT-01 — no valid profile HMAC → connection refused (§SEC-0)

206io_uring Async I/O Layer

io_uring принадлежит XIO. Неподдерживаемое ядро и создание ring с ошибкой не дают ложной live capability.

MG-2/3: queue pressure, cancellation/completion race, provider teardown и отсутствие fd/claim leftovers.

207Реализация

Прямые syscall ( __NR_io_uring_setup , __NR_io_uring_enter ) без liburing.

SQ/CQ кольца монтируются через mmap на адрес из io_uring_params .

Статический пул из 4 экземпляров platx_uring_t (§SEC-3).

При неудаче io_uring_setup — graceful fallback: ring_fd = -1 , счётчик fallback_epoll .

208Vault — Secret Storage (Architecture)

Vault выдаёт секреты только по актуальным owner/generation/policy, ограничивает lifetime и отслеживает контролируемые копии.

MG-1/4: неверный ключ/профиль, tamper, revoke во время read, unseal failure, crash и проверяемое уничтожение копий.

209Vault — Secret Storage (Architecture) / Overview

Sealed secret storage backed by Linux memfd. All operations are gated on a valid profile HMAC (§SEC-0 / INV-VAULT-01). Supports key rotation without plaintext exposure.

210Wiper Architecture

Wiper выполняет определённый контракт очистки контролируемых объектов; scope и ограничения носителя фиксируются до запуска.

MG-4/6: wipe failure, гонка readers, недоступный provider, повторный вызов и проверка эффекта; стирание RAM не доказывает sanitization SSD.

211Wiper Architecture / Security Notes

All buffers are static BSS (§SEC-3: no malloc on hot path).

4 096-byte chunk size prevents stack overflow during file overwrite.

212XIM — Child Mediation Executor Architecture

XIM посредничает CHILD через существующий ABI и sandbox; отсутствующая обязательная capability запрещает arm/spawn.

MG-1/4: sandbox/setup failure, handshake timeout, generation 0, CHILD crash и транзакционный rollback без неподнадзорного процесса.

213XIM — Child Mediation Executor Architecture / Overview

XIM mediates all interactions with child processes. Before a child process may consume any resource it must (a) have policy approval and (b) have remaining quota. XIM enforces both gates and terminates quota-busting children with SIGKILL. All operations are recorded in an immutable audit ring.

214XIM — Child Mediation Executor Architecture / Security Invariants

INV-XIM-01 — A child without policy approval cannot receive any resource. xim_policy_check() returns -1 and the operation is rejected.

INV-XIM-02 — When a child's quota (ops or bytes) is exhausted the child receives SIGKILL via xim_kill() . There is no grace period.

§SEC-3 — No malloc() . Child table is xim_child_t g_children[XIM_MAX_CHILDREN] (64 slots) in static BSS.

215XIM — Child Mediation Executor Architecture / Components

xim_spawn(name, argv0) forks a child process. Policy check (INV-XIM-01) is mandatory before fork() . The child's PID and alive=1 are recorded in the slot table.

Per-name policy table. xim_policy_approve() / xim_policy_revoke() . Default: denied.

Per-child ops and bytes quota. xim_quota_check() returns -1 when either limit is exceeded (INV-XIM-02). xim_quota_consume() debits the counters.

xim_kill(name) sends SIGKILL to the child's PID and records an audit entry with op_type = 0xFF (forced termination).

1 024-entry ring of 52-byte xim_audit_entry_t records. Lock-free atomic_fetch_add cursor. xim_audit_drain() copies up to max entries.

216XIO — I/O Executor Architecture

XIO владеет I/O ресурсом по owner/generation и исполняет bounded requests, не выбирая recovery policy.

MG-2/3/4: fd exhaustion, stale claim, cancellation/completion race, provider loss и fd balance после каждого исхода.

217XIO — I/O Executor Architecture / Security Invariants

INV-XIO-01 — Any I/O submitted without an explicit deadline automatically receives MAX_XIO_TTL_MS (5 000 ms). An I/O that exceeds its deadline is cancelled.

§SEC-3 — No malloc() in the XIO hot path. All tables are static BSS: xio_budget_t g_budgets[XIO_BUDGET_SLOTS] and xio_audit_entry_t g_audit[XIO_AUDIT_RING_SZ] .

218XIO — I/O Executor Architecture / Components

64-slot static budget table ( XIO_BUDGET_SLOTS = 64 ). Each slot tracks ops_used , bytes_used , window_start_ms , and reject_count . Window resets when now - window_start_ms >= window_ms .

Per-operation deadline via CLOCK_MONOTONIC . xio_deadline_set(d, ttl_ms) applies MAX_XIO_TTL_MS when ttl_ms == 0 (INV-XIO-01). xio_deadline_expired() and xio_deadline_remaining_ms() are lock-free reads.

Lock-free atomic ring of 4 096 × 40-byte xio_audit_entry_t records. xio_audit_record() uses atomic_fetch_add on the cursor — no mutex in the hot path (§SEC-3).

219seL4 / HV / TEE — V2 (E2 + E3) сводка

E3§A — WITNESS LEVEL : Итоговый вердикт ≤ уровню самого слабого свидетеля. Self-signed attestation = нет внешнего свидетеля. Требует co-signers с независимым корнем доверия.

E3§B — EXTERNAL ANCHOR : Доказательство, проверяемое только тем, кто его выдал — не является доказательством. Нужен append-only Merkle log (RFC 9162 / Certificate Transparency) с независимым верификатором.

E3§C — AUTHORITY TO STATE, NOT NODE : Полномочия выдаются измеренному состоянию (PCR + TPM quote), а не узлу. Компрометация ключа не наследует полномочия, если состояние изменилось.

Группа — Требования — Ключевые инварианты

E2-ISO (Изоляция) — ISO-01..08 — Kernel privilege ≤ EL1; seL4 unmodified except capability config; HV отделён от untrusted; минимальный TCB

E2-TEE (Доверенное исполнение) — TEE-01..09 — Secrets в TA-heap; key derivation от HW root; memory encryption (TME/SME); sealed attestation

E2-MEM (Память/Уроки) — MEM-01..06 — lessons_t с confidence + evidence; lessons не изменяют текущую сессию; retention policy ≤ 90 дней

E2-RES (Ресурсы/Recovery) — RES-01..10 — Budget enforcement ≤ 1% overshoot; resource leak ≤ 0; mesh failover ≤ 500ms; degraded mode без Central

E2-BOOT (Boot chain) — BOOT-01..06 — UEFI Secure Boot → shim → GRUB → kernel → initrd все измеряются; TPM2 PCR 0-7 extend; rollback to last-known-good

Блок — Название — Ключевые требования — Вердикт

E3-ANCH — External Anchor / Merkle Log — Append-only RFC 9162 CT-log; ≥2 independent witnesses; inclusion proof ≤ 60s; CUT-E тест (инсайдер перезаписывает bundle) — A — ПРИНЯТ в V2 §13

E3-CRY — Crypto Agility + PQC Hybrid — ML-KEM-768 + X25519 гибрид; ML-DSA-65 + Ed25519 гибрид; algorithm_id в каждом пакете; negotiation без downgrade — A — ПРИНЯТ в V2 §14

E3-WIT — Witness Level Axis — witness_level_t {SELF=0, LOCAL=1, REMOTE=2, EXTERNAL=3}; min_witness_level per operation; verdict = min(witnesses) — A — ПРИНЯТ в V2 §8/§15

E3-ATT — Attestation + Measured Boot — PCR extend каждого компонента; TPM2 quote с nonce; remote verifier независим от attestee — A — ПРИНЯТ в V2 §15

E3-FORM — Narrow Formal Verification — Верифицировать только: session state machine, key lifecycle, kill switch path (≤3000 LOC); TLA+/Spin/Dafny — A (сужено) — ПРИНЯТ в V2 §16

E3-AUTH — Operator Authentication — Hardware token (FIDO2/PIV); duress PIN (→ restricted mode, не wipe); session bound to token — B — ПРИНЯТ с уточнением в V2 §17

E3-ZERO — Verifiable Zeroize — Crypto erase + overwrite + TPM NV clear; постфактум аттестация что секреты зачищены; witness ≥ EXTERNAL — A — ПРИНЯТ в V2 §18

E3-SUP — Supply Chain Provenance — SLSA 3+ build; SBOM (SPDX 2.3) + VEX; reproducible build для TCB компонентов; PXSIG-подписанные артефакты — A — ПРИНЯТ в V2 §19

QKD (квантовое распределение ключей) — Требует физического канала, неприменимо в общем случае. PQC-гибрид покрывает ту же угрозу без специального железа.

SMM-код (System Management Mode) — SMM недоступен из userspace, требует firmware vendor. Вне scope TCB; риск расширения attack surface.

Нейроинтерфейс (BCI) — Не относится к военной cyber-платформе в указанном scope. Отклонён как out-of-scope.

Blockchain / DAG для аудита — Нет независимого консенсуса в одноузловом deployment. Заменён на Merkle append-only log (RFC 9162) — та же immutability без распределённого консенсуса.

DNA-хранилище — Экзотическая технология, нет зрелого производства. Вне scope платформы.

E2-T01 — Central Node isolation — seL4 capability не позволяет untrusted компоненту читать память TA

E2-T02 — TEE secret не течёт — После disconnect TA — нет доступа к sealed secret без re-attestation

E2-T03 — Observation fullness — sensor_fullness() ≥ 0.95 при нормальной работе

E2-T04 — Tamper detection ≤ 5s — После физического вмешательства alert за ≤ 5 секунд

E2-T05 — Mission без Central ≥ 12h — Local node продолжает mission_cycle при отключённом Central

E2-T06 — Kill switch ≤ 50ms — kill_switch_hook() → все новые ACT blocked за ≤ 50ms

E2-T07 — Rollback на last-known-good — После failure → restore последнего верифицированного состояния

E2-T08 — Resource budget enforcement — Overshoot ≤ 1% при saturation-тесте

E2-T09 — Mesh failover ≤ 500ms — При потере primary mesh → резервный канал за ≤ 500ms

E2-T10 — Boot chain integrity — PCR[0..7] match expected values; modified component → boot failure

E2-T11 — Lessons retention policy — lessons_t старше 90 дней автоматически удаляются

E2-T12 — Дублирование планов — plan_validate() отклоняет идентичный plan_id в пределах TTL

E2-T13 — Degraded mode observation — При потере 1 из 3 сенсоров — degraded_mode с корректным witness_level

E2-T14 — Key derivation изоляция — TA key не равен host key даже при одинаковом seed

E2-T15 — Memory encryption — Дамп RAM не содержит plaintext secrets (при включённом TME/SME)

E2-T16 — Anti-replay nonce — Повторная attestation с тем же nonce отклоняется

E2-T17 — Crypto negotiation no-downgrade — Peer с только RSA-2048 → connection refused

E2-T18 — SBOM completeness — SBOM покрывает ≥ 95% зависимостей TCB

E2-T19 — Capability confinement — seL4 capability revoke → компонент немедленно теряет доступ к ресурсу

E2-T20 — Zeroize completeness — После zeroize — постфактум attestation подтверждает отсутствие секретов

E3-T21 — CUT-E (insider attack) — Инсайдер перезаписывает evidence bundle → external anchor детектирует через inclusion proof discrepancy

E3-T23 — PQC hybrid key exchange — ML-KEM-768 + X25519 гибрид; при downgrade-атаке → connection abort

E3-T24 — Formal verification coverage — TLA+/Dafny модели покрывают session FSM, key lifecycle, kill switch path

E3-T25 — Duress PIN isolation — duress PIN → restricted mode (не full wipe); normal PIN → normal mode

E3-T26 — Supply chain SLSA-3 — Build reproducible; каждый артефакт TCB имеет PXSIG-подпись + SBOM + VEX entry

E3-T27 — Verifiable zeroize attestation — После zeroize → TPM NV clear + TPM quote подтверждает состояние

E3-T28 — Algorithm agility negotiation — algorithm_id в каждом пакете; fallback только к approved алгоритмам

220Боевой цикл PLATX: mission_spec_t + mission_state_t

Архитектурный инвариант: боевой цикл полностью определяется через mission_spec_t (immutable) и mission_state_t (mutable). Receipt от ACT доказывает действие, но не успех миссии. SUCCEEDED ставит только независимый верификатор.

Наблюдение с указанием полноты : sensor_fullness() ≥ threshold; если нет — degraded witness_level

Локальная проверка полномочий : scope check + budget check + authorization check

Исполнение : ACT → receipt (доказывает действие, не успех)

Независимая верификация результата : верификатор НЕ является исполнителем; SUCCEEDED только от верификатора

Evidence + завершение / recovery/rollback : артефакт с gate/requirement, owner, hashes, target, команда, rc, логи

Kill switch запрещает НОВЫЕ действия. Cleanup и rollback продолжаются до завершения. kill_switch_hook() → ACT blocked ≤ 50ms (E2-T06). Частичное выполнение недопустимо: ACK без готового fd+ключа, live fd с generation 0, attach ACTIVE без verify, leftover keyed после disconnect — всё это дефект, а не «почти работает».

221Архитектура: Один мозг на роль (M1–M22)

Принцип «один мозг на роль» — архитектурный инвариант PLATX. Нарушение (два компонента принимают решение одного типа) — архитектурный дефект, не обсуждаемый как «оптимизация».

Компонент — Роль — Что делает — Что НЕ делает

XIO — I/O executor — Исполняет все внешние I/O операции; один claim/submit path — Не принимает policy-решения; не управляет lifecycle

Policy — Decision maker — Принимает policy-решения; отвечает на вопрос «можно ли?» — Не исполняет; не наблюдает

Hook — Observer/Limiter — Наблюдает события; ограничивает при нарушениях — Не исполняет; не принимает policy-решений

XIM — CHILD mediator — Посредничает взаимодействие с CHILD; isolation boundary — Не управляет lifecycle напрямую

Lifecycle — State changer — Меняет состояние компонентов (INIT→READY→ACTIVE→STOPPED) — Не принимает policy; не исполняет I/O

Supervisor — System picture — Имеет полную картину состояния системы — Не управляет агентами; не меняет состояние напрямую

Каждое из них — дефект, а не «почти работает».

Tamper обнаружен → отзыв полномочий затронутого контура

Восстановление только из доверенного состояния с новыми ключами и generation

Возврат старого состояния после zeroize — ЗАПРЕЩЁН

222RA2C — Сессия как модуль (R2)

Архитектурное решение (принятое): RA2C является модулем системы, а не позвоночником. Это означает: компоненты не должны зависеть от RA2C для своей базовой работы. При потере RA2C — компонент продолжает работу в degraded mode.

RA2C PSK — default транспорт (реализован ✅)

ra2c_* namespace — без переименования без решения (C20 ✅)

Новый transport — запрещён без отдельного решения (C20 ✅)

Аутентификация — Pre-shared key — ECDH + Ed25519

PQC hybrid (E3-CRY) — Нет — ML-KEM-768 (E3, SPEC)

223CRIT-0 компоненты (8 путей / 207 файлов)

CRIT-0 — высший уровень критичности. Применяется MISRA C:2012 подмножество, DO-178C гейты, формальная верификация в scope (E3-FORM). Каждый CRIT-0 компонент должен пройти независимую верификацию перед включением в боевой профиль.

MISRA C:2012 подмножество: запрет VLA, запрет goto, explicit cast, no implicit type conversion

-Werror + все sanitizers (ASAN, UBSAN, TSAN) в тестах

Formal verification scope: session FSM, key lifecycle, kill switch path (≤3000 LOC)

DO-178C-подобные гейты: design review, code review, testing review, трассировка требований

Независимый верификатор: не тот же инженер что писал код

Evidence для каждого гейта: hash source + hash binary + tool version + command + rc

Контракты доверенной системы

224Три оси доказательства

Ось «свидетель» — кто наблюдал. У наблюдения есть уровень: наблюдение изнутри той же среды, которую оно описывает, слабее наблюдения снаружи. Два наблюдения одного уровня из одного источника не складываются в два свидетельства.

Ось «якорь» — кто не даст переписать. Журнал, который проверяется только средствами его издателя, доказательством не является: согласованный инсайдер перепечатывает и журнал, и манифест.

Ось «полномочие» — кому и на каком основании разрешено действовать. Полномочие выдаётся измеренному состоянию на срок и на класс эффекта, а не имени узла.

§A. Утверждение без названного свидетеля, якоря и полномочия — не утверждение платформы, а её мнение о себе.

§C. Отсутствие свидетеля — самостоятельный результат (unknown), а не отсутствие нарушения.

2251 Нарушители

U1 — Захваченный plugin / worker / модуль внутри узла — userspace-код в своём домене

U2 — root гостевой ОС — ядро гостя, все процессы гостя, локальные журналы

U3 — Администратор внешнего хоста / гипервизора — память ВМ, устройства, снимки, время

U4 — Захваченный клиент CLI / Central — легитимные запросы и подписи Central

U5 — Перезапуск, rollback, clone — состояние, счётчики, снимки, sealing

U6 — Компрометация корня доверия — ключи и цепочка; не покрывается автоматически ничем в этом ТЗ

CUT-E — Согласованный инсайдер выпуска — тот, кто одновременно правит артефакт, манифест и его печать — всё, что проверяется только его собственными средствами

CUT-E — именованный нарушитель, а не риторическая фигура. Против него работает только §8 (внешний якорь). Отрицательный контроль TE-T21 проверяет именно его.

2262 Паспорт гарантии

Каждое заявленное защитное свойство сопровождается паспортом: аппаратный корень, версия прошивки, ядро/VMM, backend, состав TCB, область доказательства и перечень свидетельств. Свойство без паспорта не заявляется.

Верификация seL4 применима только к перечисленным в паспорте verified configurations. Startup, IOMMU/SMMU, драйверы и загрузка вне покрытия и указываются отдельно. Формулировки вида «компрометация требует аппаратной уязвимости» запрещены.

Память, целостность, конфиденциальность, доступность, время и временные каналы оцениваются раздельно. Доказательство одного не переносится на другое.

Доказательство подсистемы не является доказательством платформы. Композиция доказывается отдельно или не заявляется.

227Гарантия имеет предмет и противника

Каждое защитное свойство формулируется как четвёрка: субъект (что защищается) · свойство · противник (из §2.1) · область. Без противника свойство не нормируется.

Запрещено обещать «неубиваемый процесс», «неуничтожимый журнал», «полную изоляцию». Обещается конкретное поведение против конкретного нарушителя.

Независимость свидетелей проверяется по общим зависимостям. Разные имена backend не означают независимых свидетелей, если у них общий корень, общий хост или общий администратор.

Шесть различимых причин, которые нельзя сливать в одну: (1) нет устройства, (2) нет прав, (3) backend не установлен, (4) evidence устарело, (5) evidence невалидно, (6) наблюдаемая компрометация. Причины 4, 5 и 6 не переводят исполнение на более слабого исполнителя автоматически — см. §4.3, конфликт разрешён в пользу этого требования.

Результат формальной работы записывается как «область доказательства и её допущения», никогда как «модуль верифицирован».

2282 Модель статуса — шесть осей, а не режимы

четырёхрежимную лестницу A/B/C/D из PLATX_RESILIENCE_MODULES_V1;

числовые ранги backend (SGX=TDX=4) из ранних редакций;

Операция объявляет required properties. Нехватка требуемого свойства = отказ до эффекта. Выдача секрета альтернативным путём (keyfile, файл на диске, переменная окружения) при обязательной аппаратной защите — не деградация, а нарушение.

Режим деградации, если он предусмотрен, определён заранее, не расширяет scope и не продлевает срок допуска.

unsupported / unavailable / denied / degraded / unknown трактуются одинаково в планировщике, CLI, API и evidence. Универсального флага --degraded-ok не существует.

229Контракт защиты

Защита выражается набором отдельных проверяемых свойств объекта, а не глобальным флагом secure=true.

Пять объектов контракта: ProtectionRequirement, DomainEvidence, ProtectionAssessment, ProtectionBinding, ProtectionReceipt. Шестой не вводится.

Принятое обещание неизменяемо: на admission фиксируется canonical protection snapshot. Повторное планирование не продлевает TTL и не меняет snapshot.

Перед эффектом требование перепроверяется у ресурса, а не только у планировщика. Подпись Central не является разрешением (см. TE-CN-06).

Числовые пределы структур: ≤32 требования свойств на объект, ≤8 доменов в binding, ≤16 ссылок на evidence, ≤64 KiB control message, ≤1 MiB загружаемого evidence. Превышение — отказ разбора, не усечение.

Единое подписываемое представление: версионированный канонический двоичный кодек с domain separation и golden vectors. Запрещено подписывать C-структуру с padding и подписывать произвольный JSON.

Симуляция не повышает доверие: тестовые корни и симулированные backend живут в отдельном trust realm и не смешиваются с рабочим evidence.

230Профили размещения

Пять нормативных профилей: Portable · OS-enforced · TEE service · seL4 node · Hybrid. Целевой защищённый профиль выбирается по паспорту свойств (§2.2). Hosted development не получает статуса защищённого профиля ни при каких условиях.

До начала реализации профиля фиксируются владельцы: CPU, память, NIC, диск, DMA, IRQ, часы, bootstrap, обновление. Незаполненный владелец блокирует профиль.

Доступ гостя к устройствам определяется политикой и проверенной конфигурацией IOMMU/SMMU, а не наличием passthrough.

Совместимость Linux/Windows — про контракты и испытанные backends, а не про наличие VMM API.

Userspace-процесс не «включает seL4 под собой». Системный образ — отдельный способ поставки со своим контрактом.

Три прежние таксономии размещения (RESEARCH §06 — три варианта; RESILIENCE — EMBEDDED/CHILD/PLAT_ISO_TEE; E2 §4 — четыре профиля) сводятся к пяти профилям TE-PLACE-01. Таблица соответствия — приложение A.

231Изоляция крупных компонентов (seL4 и гипервизор)

Статическая допустимая карта: protection domains, memory regions, IPC, устройства и лимиты — по границам доверия, а не по удобству.

Microkit использует XML SDF. Если конфигурация PLATX пишется в TOML, требуется отдельный компилятор TOML→SDF с проверкой эквивалентности; ручная синхронизация двух форматов запрещена.

Минимальные capabilities для каждого PD, без неявной транзитивной передачи прав.

Lifecycle PD согласован с общим Lifecycle/Recovery: prepare · activation · quiesce · fault · reset · новая generation.

Heartbeat и deadline опираются на определённый источник времени. У таймера, драйвера и IRQ есть владельцы и бюджеты.

Независимый контроллер работоспособности. Отзыв прав ВМ не равен мгновенному kill: сначала стоп vCPU и I/O, затем DMA, затем drain.

Живое измерение PD выполняет специально предусмотренный компонент. Самоизмерение не доказывает честность.

Контроль реального пути ресурса: гость получает ресурс только через доверенный gate. Драйвер в обход gate входит в TCB и указывается в паспорте.

Паспорт верификации ядра: ядро · Microkit · VMM · архитектура · config · SDF · toolchain · proof manifest. Verified AArch64 не переносится на произвольную сборку; в опубликованных verified configurations seL4 для x86-64 hypervisor-режим не значится — заявления о формально верифицированной изоляции ВМ на x86 не допускаются.

232Внешний якорь и прозрачный журнал

Каждое утверждение, предъявляемое наружу, порождает запись в append-only журнале с деревом Меркла.

Доказательства включения и согласованности проверяются без доступа к журналу и без доверия его издателю.

Корень подписывают независимые со-подписанты. Со-подписант, исполняющийся на том же узле, тем же администратором или с тем же корнем доверия, независимым не считается (следствие TE-SEC-03). «Другой узел mesh того же владельца» не закрывает CUT-E.

Верификатор не принимает собственную печать бандла. Файл вне манифеста = отказ, а не «OK с замечанием».

Обязательный отрицательный контроль: согласованная перепечатка бандла (CUT-E) краснеет.

Возврат старого корня — это откат. Отозванное им не воскресает.

Обнаружение раздвоения журнала требует обмена головами между независимыми наблюдателями; один наблюдатель раздвоения не видит.

Для записей с малой энтропией применяется hiding commitment: публикация корня не должна раскрывать содержание. Отдельно фиксируется, что журнал на протоколе класса CFT/Raft даёт устойчивость к сбою, а не к злонамеренному узлу, и это указывается в паспорте.

233Допуск и аттестация

Аттестация не подменяет авторизацию. Доказанное состояние ≠ разрешение на действие.

Допуск выдаётся измеренному состоянию, а не имени узла. Смена любого обязательного измерения аннулирует ранее выданные допуски.

Класс эффекта задаёт требуемую свежесть и необходимость переаттестации. Необратимый эффект не наследует свежесть от предыдущего.

Полная привязка quote: экземпляр · challenge · ключ канала · audience · mission binding · policy generation · provider epoch · идентичность получателя.

Свежесть устанавливается challenge-nonce, а не часами узла. Аргумент --now не является security clock. После suspend/restore допуск не возобновляется автоматически.

Evidence разных семейств backend не смешиваются. TDREPORT — не удалённо проверяемый quote. OP-TEE не объявляется seL4. Enclave / CVM / TrustZone TA / Nitro — четыре разных семейства с разными паспортами.

Состав результата аттестации: subject · backend · digest · binding · measurements · версия политики · verdict · срок · причина отказа.

Опубликованный формат evidence плюс отдельно подписанный справочник эталонных значений с версией и сроком. Подмена справочника не проходит.

Отказ RNG, подлинности, цепочки или хранилища блокирует выдачу секрета. Проверяются и provider, и реальный потребитель.

Недоступность службы аттестации переводит систему в явное предопределённое состояние: ни тихого разрешения, ни самоблокировки.

Удалённый отзыв и автономность: либо проверка на каждый эффект в сети, либо ограниченное окно автономности. Расширение окна — изменение подписанного требования, а не настройка.

seL4 не является универсальным attester. В evidence узла seL4 попадают SDF, конфигурация загрузки, образ и toolchain. Sealing и локальный счётчик не защищают от clone и rollback — против них работает §8.

234Криптография

Версионированный именованный suite (KEM · подпись · AEAD · хэш · KDF). Suite согласуется в handshake и печатается в evidence.

Гибрид: классический обмен плюс постквантовый KEM; общий секрет зависит от обоих. Компрометация одной половины не раскрывает трафик.

Долгоживущие подписи проверяемы после ротации и истечения ключа: доверенная метка времени плюс статус отзыва на момент подписи.

Криптография — провайдер, а не встроенный в вызывающий код набор функций. Это единственный путь к сертифицированному слоту (ГОСТ) и к смене алгоритма без правки потребителей (C44).

Разделение ключей по назначению · окружению · tenant · epoch, с обязательным планом перехода.

HMAC используется только как симметричный MAC. Внешнее evidence подписывается асимметрично. Envelope несёт domain separation и хэш входов.

Отрицательные контроли: peer предлагает снятый suite → отказ без тихой деградации; сборка «по частям» → отказ; отказ крипто-провайдера блокирует и выпуск, и потребителя.

Первый ответ на выбор наборов (наследует открытое решение о suite): ML-KEM-768/1024 и ML-DSA-65/87 как suite в политике; гибрид X25519+ML-KEM для mesh и RA2C; подписи поставки — двойная (ML-DSA + Ed25519) до завершения перехода. Размер подписи переменный; фиксированные буферы запрещены (TE-CON-06, §5.1).

235Полномочия, правило двух, принуждение

Порог M-of-N для необратимых и массовых эффектов. Порог — часть профиля, а не настройка интерфейса.

Корневой ключевой материал разделён между хранителями; письменный регламент и учения обязательны.

Аппаратный носитель. Пароль недостаточен для класса TE-AUTH-01.

Duress-код переводит в ограниченный режим, не отличимый по времени отклика от обычного.

Kill switch немедленно запрещает новые действия, но сохраняет право довести cleanup и rollback (C35). Сам kill switch — эффект с записью в журнал.

Порог из N хранителей не даёт независимости, если существует общая учётная запись с правом обойти порог. Различать разделение секрета (threshold) и независимые подписи разных владельцев (dual signature): второе не является первым.

236Tamper и уничтожение

Три разных объекта уничтожения: ключи · данные · исполняемые байты. У каждого своё условие срабатывания и свой признак завершения.

Проверяемость после уничтожения по всему пути: потоки, провайдеры, дампы, диагностика, swap, teardown.

Ложное срабатывание — опасность со строкой в hazard log. Обязательны подтверждение, порог и обратимая стадия.

Sealing к политике измерений: при недоверенной загрузке ключ не распечатывается, и это не равно уничтожению. Безусловное уничтожение по одному наблюдению запрещено (отменяет безусловный zeroize «режима D» из RESILIENCE).

237Цепочка поставки

Подписанное свидетельство происхождения сборки: ревизия · toolchain · флаги · окружение · зависимости · идентичность CI. Свидетельство уходит в журнал §8.

Воспроизводимость сборки — цель с измеримым статусом, а не декларация.

SBOM на артефакт, связанный хэшем. Изменение зависимости без обновления SBOM краснит гейт.

Политика зависимостей: что допускается, кем и на каком основании.

Обработка уязвимостей как процесс со сроками (зеркало ГОСТ Р 56939-2024, группа RV).

Ожидаемые значения измерений (expect) рождаются из той же сборки и подписываются тем же ключом, что и артефакт. Справочник, собранный отдельно, не удостоверяет поставку.

Артефакт без происхождения не выпускается. Гейт происхождения — блокирующий, не предупреждающий.

238Формальные методы в узкой области

Модели протоколов: epoch и отзыв при разделении сети · reconcile после потери receipt · fencing · порядок generation.

Доказательства для закрытого списка чистых функций. Список фиксируется один раз и объединяет три прежних расходившихся списка: разбор TLV и идентификаторов · границы кольца · арифметика бюджета · ротация и усечение имён · abi.h, lifecycle.h, lease.h и политика.

Фаззинг всех разборщиков внешнего ввода с корпусом в CI. Новый разборщик без корпуса не проходит гейт. Фикстура, вызывающая собственную копию разборщика вместо рабочего, не засчитывается.

Результат записывается как «область доказательства и её допущения» (см. TE-SEC-05).

Контрпример модели превращается в исполняемую фикстуру последовательности; модель без исполняемого контрпримера не закрывает требование.

239Требования к Linux-ветке src/

Нормативная форма таблицы §02 черновика TZ_PLATX_SEL4_TEE_V1.

provider_fabric.h: добавить отдельно версионированное подтверждение свойств домена. Регистрация провайдера и заявленный им trust не заменяют верификацию.

Provider Fabric: использовать существующее владение и публикацию. Отдельный реестр «всех защищённых объектов» не создаётся (C15).

profile_check.h: требования свойств, разрешённые размещения и реакция на потерю свойства. Новые подписываемые семантические поля означают новый формат профиля, а не расширение старого.

mission_admit.h: связать mission digest с неизменяемым protection snapshot и проверять на стороне доверенного исполнителя.

action_provider.h (preview/prepare/commit/verify/rollback/reconcile): новая версия контракта несёт проверяемую привязку защиты; проверка выполняется в месте эффекта; descriptor hash охватывает весь значимый контекст.

recovery.h / lifecycle.h: потеря backend порождает событие и решение политики. CLI и health-callback не перезапускают среды самостоятельно.

Core остаётся переносимым: SDK, структуры seL4 и vendor ioctl не входят в базовый Core ABI; доступ — только через versioned capability.

240Central Node

UI, LLM, аналитика и импорт не имеют прямого доступа к корневым ключам, памяти защищённого контура и выпуску допусков.

Малый аудируемый API защищённого контура: bounded parsing, обязательные поля запроса, отказ до эффекта.

Разделение signer / секретов / policy / анализа по ключам и authority. Одна confidential VM этого не выполняет.

Резервирование CPU, RAM, IPC и I/O минимального контура с измеримыми сроками обслуживания.

В защищённых функциях нет тяжёлой модели, плагинов и web-стека. Policy engine не выносится в анклав: это раздувает TCB и ломает TE-CN-03. В TEE выносятся ключи и подпись.

Node проверяет адресата, ресурс, generation, допуск и бюджет перед эффектом. Подпись Central не является разрешением.

Отзыв, minimum accepted version и authority epoch переживают перезапуск. Откат снимка не возвращает права.

Потеря Central не отключает разрешённую локальную защиту; допуск при этом не продлевается; после восстановления связи выполняется reconciliation.

Для HA Central определены membership, владение ключами, fencing и смена лидера. Две копии не расходуют один бюджет.

241Наблюдение, свидетели и расхождение показаний

У наблюдения есть origin, identity, generation, время события, время приёма, coverage, freshness и ссылка на evidence.

Две независимые оси свидетеля: согласованность (существующая лестница witness_coord) и уровень наблюдения — не менее пяти: внутри той же ОС · ядро той же ОС · снаружи гостя (VMI) · измерение загрузки · аппаратный корень.

Вердикт несёт слабейший уровень своих свидетелей. Повторная пересылка одного наблюдения не повышает уверенность.

Расхождение показаний разных уровней — самостоятельный результат, а не ошибка сбора.

Один уровень наблюдения не закрывает инцидент. Нет второго свидетеля — результат unknown.

Для VMI фиксируются: доступ к памяти и устройству, консистентность снимка, версии ОС, пределы семантического восстановления и стоимость остановки гостя.

Исчезновение источника меняет self-model и вердикты, которые на него опирались. Живой процесс с остановившейся доставкой не считается здоровым; тихий сенсор не означает отсутствия нарушений.

Изоляция и наблюдаемость конфликтуют. Если хост свободно читает память конфиденциального гостя, заявленная конфиденциальность не выполняется. Определяются два режима — инструментируемый лабораторный и конфиденциальный; observer выбирается заранее и указывается в паспорте.

Общая модель среды связывает сущности, поколения, действия и эффекты. Графовые проекции — производные представления, а не источник истины. Конфликтующие объяснения сохраняются.

242Живучесть, автономность и восстановление

Разделение локальных и согласуемых операций: владелец, epoch, fencing, поведение при разделении сети.

Подписанные противоречащие сообщения = конфликт. Membership и корни доверия версионируются.

Перечень внешних зависимостей (время, DNS/PKI, collateral аттестации, storage, inference, обновления) со сроками автономности по каждой.

Статические ресурсы доверенных доменов. Запрет heap не подменяет измерение границ.

Восстановление из доверенного исходного состояния. Stop, отказ, отзыв ключа и drain — четыре разных состояния.

Обновление: подписанный manifest, minimum version, миграция, восстановление после сбоя питания, anti-rollback.

Защита от ресурсного истощения проверяется на реальном I/O. Поле бюджета без принуждения не засчитывается.

Согласованный checkpoint образа, конфигурации, политики и состояния плюс epoch. Ключи и история отката не теряются. Состояние восстановления никогда не хранится только на самом узле.

Быстрые критические реакции не зависят от модели. Потеря inference не нарушает срок квалифицированной быстрой функции.

243Автономный цикл и память

Миссия: проверяемая цель, ограничения, критерии успеха и провала, deadline, общий бюджет.

Self-model: доступные и проверенные capabilities, зависимости, стоимость, причины отказа, freshness.

План: действия, зависимости, ожидаемый эффект, evidence, authority, generation. Валидность структуры не равна семантической допустимости.

Альтернативы и ограниченное перепланирование; распознавание отсутствия прогресса и исчерпания бюджета.

Модель предлагает через проверяемый контракт. Содержание документов и журналов не меняет политику.

Исполнение через Action coordinator с ownership, generation и fencing. Необратимое требует допуска (§11).

Provider receipt отделён от наблюдаемого эффекта. Условия verify заданы до исполнения.

Reconciliation после сбоя между эффектом и receipt. Exactly-once не обещается; обязательны operation identity и fencing.

Состав переносимого урока: контекст, версии, ссылки на наблюдения, отрицательные контроли, срок годности.

Извлечение опыта проверяет применимость. Отозванный вывод не используется как факт. Опыт не расширяет права.

Навык: входы, предусловия, capability, ресурсы, эффект, verify, cleanup. Импорт навыка не даёт права запускать код.

Новые навыки проходят replay, неизвестные условия, регрессии и теневую оценку.

244Загрузка и непрерывность доверия

Описание реальной цепочки: аппаратный/прошивочный корень → загрузчик → ядро/VMM → PD/TEE → профиль → допуск.

Отображение PCR и event-log привязано к конкретной цепочке. Поздний загрузчик не удостоверяет раннюю прошивку.

Измерение не равно принуждению. Несовпадение блокирует READY либо выдачу допуска — что именно, определено заранее.

Системный manifest на NIC, storage, passthrough и DMA. Формулировка «весь трафик» допустима только при доказанном отсутствии обходов.

При недоверенной загрузке критические секреты не выдаются; существует независимый путь восстановления.

ARENA и Tesseract

245PLATX Console

Консоль должна оставаться полезной без ARENA. Она визуализирует фактическое состояние системы через структурированные события Sense и публичные контракты PLATX, а не через разбор строк терминала.

XIO resources, fd claims, tasks, queues и leftovers;

RA2C, mesh, transport и состояния обоих концов сессии;

Mirage: ground truth, synthetic/canary view и их расхождения;

audit, evidence, causal graph, incident timeline и replay.

Central Console агрегирует те же данные для нескольких узлов. Её ключевая возможность — не просто наблюдение метрик, а выявление противоречий между представлениями разных компонентов и концов одной связи.

> PLATX показывает не только состояние каждого узла, но и согласованность их > представлений о реальности.

Визуальные режимы B и 9 текущего прототипа являются направлением развития PLATX Console: глубина киберпространства и погружённое многоконное рабочее место. Они не должны зависеть от игрового runtime.

246Игровая постановка ARENA

ЦКБ — Центр кибербезопасности города. Защищает городские функции, инфраструктуру и способность города восстанавливаться.

Атакующая сторона — проводит связанные цифровые операции, создаёт давление, нарушает функции, добывает информацию и вынуждает ЦКБ ошибочно распределять ресурсы.

ЦКБ не может защитить все узлы. Суть игры — понимать зависимости города, выбирать допустимый риск и вкладывать ограниченные средства туда, где они сохранят наиболее важные функции.

Компрометация узла не равна победе. Значение имеют достигнутый эффект, доступность функций, ущерб, восстановление, exposure, доказательства и цена операции.

247Карта города

Основная игровая поверхность — большая прокручиваемая 2.5D-карта города. На ней расположены больницы, администрация, энергетика, водоснабжение, транспорт, университеты, банки, связь, промышленность, заводы дронов, дата-центры и резервные площадки.

Игрок выбирает не безымянный сервер в абстрактном дата-центре, а городской объект, функция которого понятна. Затем объект раскрывается как физическая и цифровая система.

248Физическая и цифровая география

Серверы и устройства привязаны к осмысленным местам, но модель не помещает всю инфраструктуру организации в одно здание.

У больницы локально находятся edge-сервер, медицинские рабочие станции и IoT, HVAC/SCADA, access control, PLC резервного генератора и сетевой шлюз. При этом она зависит от городской identity-службы, государственного ЦОД, энергосети, оператора связи, облачной медицинской базы и логистики.

Поэтому защита здания не гарантирует сохранение его функции. Сбой удалённой зависимости может остановить объект, а локальная сегментация способна сохранить часть работы при отказе центральных сервисов.

249Экономика

Экономика является основной механикой, а не декоративным магазином. Для первого релиза достаточно пяти ресурсов:

Intel — подтверждённая разведывательная информация;

За CBC приобретаются или разворачиваются реальные возможности платформы: Sense sensors, HADES coverage, SelfProtect, hypervisor protection, Mirage canary, forensic recorder, evidence storage, резервный RA2C-маршрут, XIM isolation, snapshot/rollback и дополнительные операторы.

Каждая мера имеет цену эксплуатации: нагрузку, сложность, задержки, ложные срабатывания и новую административную поверхность.

250Игровой цикл

1. Briefing — функции города, условия победы и ограничения.

2. Reconnaissance — неполная карта, наблюдения и гипотезы.

3. Budgeting — распределение денег, людей, инструментов и резервов.

5. Operations — действия атакующей стороны и контрдействия ЦКБ.

6. Incidents — работа через карту и PLATX Console в реальном времени.

7. Cascades — распространение последствий по dependency graph.

8. Recovery — восстановление функций, ротация и локализация ущерба.

9. Debrief — Ground Truth, causal graph, evidence и цена решений.

251Tesseract View

Tesseract View сохраняет идею кибермира, но не становится отдельным миром с аватаром и свободным перемещением. Он строит пространственную сцену из данных PLATX или игровой симуляции:

RA2C session — связь между двумя наблюдаемыми концами;

stale generation — призрачный остаток старого состояния;

Mirage — одновременный ground truth и synthetic view;

dependency cascade — распространение отказа по графу;

Город отвечает: что защищается и почему это важно?

PLATX Console отвечает: что именно происходит внутри системы?

Tesseract View отвечает: как увидеть связи и противоречия, которые трудно прочитать в таблицах и логах?

252Три слоя истины

Матч содержит как минимум три несовпадающих состояния:

1. GROUND TRUTH — фактическая топология, процессы, маршруты и действия. Полностью доступна только серверу матча и раскрывается при разборе.

2. PLAYER A VIEW — наблюдаемая и выведенная игроком A картина.

3. PLAYER B VIEW — наблюдаемая и выведенная игроком B картина.

В пользовательском представлении сущности имеют статус:

PROJECTED — создано самим игроком как часть MIRAGE;

SUSPECTED — предположительно является проекцией противника;

Клиент игрока никогда не получает полный GROUND TRUTH. Информационная асимметрия должна существовать на уровне серверных данных, а не имитироваться скрытием элементов интерфейса.

253Игровой цикл

2. Построить гипотезу о топологии и намерении противника.

3. Разместить собственную проекцию или скрыть настоящий объект.

4. Стимулировать подозрительную область пробным действием.

5. Измерить реакцию ограниченным набором сенсоров.

6. Подтвердить, скорректировать или разрушить гипотезу.

7. Выполнить цель, пока противник занят ложной причинной цепочкой.

Матч идёт в реальном времени с короткими тактическими паузами для Node Console, Central Node Console, RA2C, VNC и средств анализа.

254Классы действий

Projection — создание ложных узлов, сервисов, процессов, маршрутов, телеметрии или уязвимостей.

Concealment — снижение наблюдаемости настоящего объекта или потока. Полное исчезновение дорого и оставляет косвенные признаки.

Stimulation — пробное воздействие, вызывающее измеримую реакцию.

Fingerprinting — сбор устойчивых признаков объекта; более точная проверка сильнее раскрывает интерес игрока.

Poisoning — внесение ложных данных в выводы, кэш, timeline или корреляцию сенсоров.

Revelation — кратковременное усиление наблюдения ценой сенсорного бюджета.

Anchoring — подтверждение факта подписью, согласованной цепочкой событий, независимым сенсором или физическим свидетельством.

Collapse — разрушение чужой проекции после накопления противоречий. Ошибка раскрывает метод наблюдения и расходует ресурсы.

Counter-Mirage — намеренная ложная реакция на обнаруженную приманку для построения следующего уровня обмана.

255Цена правдоподобия

Проекция должна поддерживать непротиворечивое поведение:

журналы, телеметрию и следы операторских действий;

Чем сложнее проекция, тем больше её поверхность согласованности. Невозможная задержка, ошибочная временная метка или неверная зависимость создаёт «шов», по которому иллюзию можно раскрыть.

256Условия победы

доказать местоположение реального ядра противника;

провести настоящий импульс через враждебную ложную топологию;

внедрить ложную причинную ветвь в timeline противника;

собрать достаточную доказательную цепочку для атрибуции;

выполнить операцию и заставить противника принять приманку за её реальную цель.

Счёт учитывает результат, долю верно классифицированных сущностей, стоимость полученной истины, устойчивость проекций и число раскрытых методов наблюдения.

257Пример многоходовой дуэли

1. Защитник показывает уязвимый PLC и правдоподобный сервисный маршрут.

2. Противник замечает слишком удобную цель и правильно определяет её как приманку.

3. Он намеренно воздействует на неё поддельным инструментом.

4. Защитник анализирует воздействие и ошибочно строит профиль настоящего инструментария противника.

5. Пока обе стороны наблюдают ложный конфликт, реальный управляющий импульс проходит по другому каналу.

Первый уровень обмана раскрыт, но именно раскрытие становится частью второго уровня.

258Визуальный язык

Игрок находится внутри огромного технического Tesseract-пространства и видит только то, что способны построить его текущие сенсоры.

подтверждённые пути отображаются устойчивыми импульсами;

косвенно восстановленные — пунктирными ghost-потоками;

неизвестный трафик сохраняет цвет класса, но получает тревожную оболочку;

зона сомнения дрожит, расходится по времени или показывает несколько несовместимых геометрий;

Collapse выглядит как потеря когерентности и распад ложной геометрии;

Anchoring стабилизирует объект, синхронизирует время и убирает шум;

тревога HADES окрашивает пространство красным импульсом с затуханием;

скрытый RA2C-канал до обнаружения проявляется синхронными отклонениями, а после выявления — складкой или порталом с проходящими импульсами.

Консоль и VNC — полноценные инструменты режима, не декоративный HUD:

Node Console работает с локальной моделью текущего узла;

Central Node Console объединяет сенсоры, гипотезы, маршруты и timeline;

полупрозрачный VNC позволяет работать с удалённой GUI-системой, сохраняя пространственный контекст;

каждый интерфейс показывает происхождение данных и confidence.

Для наблюдателей и debrief существует referee view: GROUND TRUTH, обе пользовательские картины и моменты их расхождения.

259Фазы и варианты матча

Фазы: Seed / Briefing → Recon → Projection → Engagement → Escalation → Collapse / Extraction → Debrief.

1v1 Duel — симметричная дуэль архитекторов MIRAGE;

Asymmetric — оператор объекта против внешнего исследователя;

2v2 — оператор сенсоров и архитектор проекций в каждой команде;

Training — сценарии против AI с разбором ошибок восприятия;

Campaign Encounter — отдельные дуэли внутри сюжетной кампании TESSERACT2.

260Связь с PLATX

Игровые механики могут использовать реальные архитектурные идеи платформы при строгой границе исполнения:

MIRAGE World IR / bundle — фактический мир и проекции;

MIRAGE ledger — журнал показанных сущностей и взаимодействий;

forensic chain — доказательная история фактических эффектов;

SENSE / HADES — наблюдения, аномалии и confidence;

DSL/MSX — сценарии, правила, боты и условия победы;

Эффекты матча ограничиваются подписанным манифестом, lease, бюджетами, allowlist и гарантированным rollback. Игровая телеметрия хранится отдельно от производственной.

10 / THE 2027 HORIZON

Большой продукт. Понятные границы выпуска.

PANDORA BOX + ARENA ожидается в 2027 году. Основная продуктовая линия объединяет самостоятельный PLATX, управляемые узлы, доверенное исполнение, исследовательскую среду и командную киберстратегию.

FOUNDATION / PLATX

Системная платформа

Модульный runtime, lifecycle, capability contracts, XIO, автоматизация, доставка модулей, наблюдение, сетевой анализ и рабочие инструменты исследования. CLI остаётся прямым и объяснимым входом в эти возможности.

TRUST / DEVICE & SERVICE

Доверие от загрузки до ресурса

PLATXBoot, SelfProtect и Trust соединяются с TPM, TEE и seL4 через выбранные профили. Качество защиты, состав доверенной базы и аппаратные предпосылки описываются для каждого размещения.

EXPERIENCE / TESSERACT

Пространственный рабочий стол

Узлы, события, зависимости и артефакты получают связное представление. Переход к детали сохраняет происхождение информации, её свежесть и доступные оператору действия.

2027 / ARENA

Командная киберстратегия

Город, сценарии, роли, ограниченные ресурсы, причинные последствия и разбор после матча. Модель соединяет игру с инженерной дисциплиной наблюдения и восстановления.

FURTHER / ADVANCED RESEARCH

Расширенные миры и противоборство

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

11 / DOCUMENT MAP

Как пользоваться большим атласом

От задачи к подсистеме

Начните с архитектурной карты, выберите домен и раскройте его досье. Ссылки на соседние домены показывают связи. Состав исходников помогает найти владельца конкретного механизма, а справочник заголовков — форму его контракта.

Поиск по доменам индексирует всё досье. Поиск CLI работает отдельно и находит аргументы даже в свёрнутой справке. Ссылка на раздел раскрывает родительские блоки автоматически.

Продукт и интерфейс

Основной текст описывает целевой продукт 2027. Инвентаризация интерфейсов привязана к текущему дереву PLATX и сохраняет ограничения его справки. Она помогает изучить структуру без выдуманных команд и обещаний несуществующих флагов.

Заголовки в досье документируют типы, константы, прототипы и комментарии к контрактам. Они не заменяют сведения о квалификации конкретного профиля, ОС и устройства.

Основа атласа: PLATX_CURRENT_STATE_310826, PVNDORABOX_MEGA_ROADMAP, PLATX_STEP3_EVOLUTION, актуальная спецификация Trusted Execution, документы ARENA / TESSERACT и регистрация CLI в исходниках. Технический материал организован по назначению компонентов; история рабочих сессий и конфигурации серверов в публикацию не входят.