> For the complete documentation index, see [llms.txt](https://docs.iexexchanger.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.iexexchanger.com/razrabotchikam/razrabotka-plaginov.md).

# Разработка плагинов

Плагин iEXExchanger — ZIP-пакет с файлом `iex-plugin.json`, который описывает назначение интеграции, способ выполнения, запрашиваемые доступы и настройки. Для исходного кода используется собственный проект плагина. Установкой, подключением к обменнику и жизненным циклом управляет PluginManager.

{% content-ref url="/spaces/YuqSN6CIJoIeh8EPb0uE/pages/gafsPx8xZymoPUFsodha" %}
[Установка плагинов](/guide/sait/ustanovka-plaginov.md)
{% endcontent-ref %}

Эта инструкция описывает формат `iex.plugin.v2`: от подготовки манифеста и обработчиков до проверки, установки, обновления и удаления. Примеры с названиями `example` и доменами `example.com` демонстрационные. Замените их данными своей интеграции.

### Как пакет становится работающей интеграцией

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

{% hint style="info" %}
После установки, обновления и отката плагин остаётся отключённым. Наличие пакета в каталоге или базе данных само по себе не означает, что его функции доступны обменнику.
{% endhint %}

### Основные понятия

<table><thead><tr><th width="212.69140625">Понятие</th><th>Значение</th></tr></thead><tbody><tr><td>Пакет</td><td>Плагин с постоянной идентичностью <code>namespace/name</code>.</td></tr><tr><td>Релиз</td><td>Отдельная установленная версия пакета с проверенным содержимым.</td></tr><tr><td>Runtime</td><td>Способ выполнения обработчика. Для всего пакета выбирается один runtime.</td></tr><tr><td>Capability</td><td>Возможность, через которую плагин подключается к определённому процессу ядра.</td></tr><tr><td>Instance key</td><td>Постоянный идентификатор экземпляра capability внутри пакета.</td></tr><tr><td>Alias</td><td>Имя реализации capability, по которому её выбирают адаптеры и интеграционный код.</td></tr><tr><td>Permission</td><td>Запрашиваемое разрешение, например <code>rates:write</code>.</td></tr><tr><td>Scope</td><td>Область разрешения: конкретные хосты, ключи секретов или имена событий.</td></tr><tr><td>Action</td><td>Именованная операция обработчика, например <code>rates.fetch</code>.</td></tr></tbody></table>

## Перед началом разработки

### Подготовьте исходные данные

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

Разрабатывайте и проверяйте плагин на отдельной тестовой установке той версии iEXExchanger, для которой выпускаете пакет. Совместимость с PHP, Laravel и расширениями проверяется на стороне Backend. Версии среды удалённого сервиса задаются в его собственном проекте.

В примерах адрес Backend обозначается как `https://app.ваш_домен`. Это адрес обменника, предоставляющего Runtime API. `runtime.endpoint_url` — адрес обработчика плагина; он может находиться на другом сервере.

### Выберите способ выполнения

| Runtime           | Где выполняется логика                     | Применение                                                          |
| ----------------- | ------------------------------------------ | ------------------------------------------------------------------- |
| `declarative`     | Ядро возвращает заранее объявленный JSON.  | Статические представления и проверочные пакеты.                     |
| `remote_http`     | Ваш HTTPS-сервис.                          | Сторонние интеграции и обработчики на любом языке.                  |
| `wasm`            | Внешний настроенный worker.                | Модуль WebAssembly при наличии совместимого исполнителя.            |
| `isolated_worker` | Внешний worker с закреплённым OCI-образом. | Контейнерный обработчик при наличии соответствующей инфраструктуры. |
| `native_php`      | Среда PHP/Laravel обменника.               | Официальные пакеты с проверенной подписью.                          |

Выбор `wasm` или `isolated_worker` в JSON не создаёт worker и не настраивает его изоляцию. Возможность запуска зависит от конфигурации целевого сервера и реализации исполнителя.

{% hint style="warning" %}
Обычная загрузка ZIP или установка по URL получает уровень доверия `unverified`. Такой пакет не может использовать `native_php` и выполнять PHP-миграции. Для официального пакета нужна проверяемая подпись из каталога готовых плагинов.
{% endhint %}

## Структура ZIP-пакета

### Файлы внутри архива

Минимальному `remote_http` или `declarative` пакету достаточно `iex-plugin.json`. Код удалённого сервиса размещается отдельно.

```
example-rates/
├── iex-plugin.json
├── src/
│   └── Parser.php
├── assets/
│   └── icon.png
└── database/
    └── migrations/
        └── 2026_09_08_000000_create_example_cache.php
```

В этом дереве `src` требуется только выбранному файловому runtime, `assets` — при использовании иконки, `database/migrations` — при объявлении разрешённых миграций. Не добавляйте пустые каталоги ради соответствия примеру.

Размещайте манифест в корне ZIP. Установщик также распознаёт одну внешнюю папку с пакетом, но дополнительная вложенность не нужна.

### Правила упаковки

* Используйте точное имя `iex-plugin.json` и UTF-8 JSON без комментариев.
* Все пути в манифесте задавайте относительно корня пакета.
* Включайте в ZIP каждый файл, на который ссылаются entrypoint, иконка или миграции.
* Не используйте абсолютные пути, `.` и `..` в сегментах пути.
* Не добавляйте `.env`, `.git`, токены, закрытые ключи и данные клиентов.
* Не рассчитывайте на выполнение Composer, npm, установочных shell-скриптов или автоматическую регистрацию собственного service provider.

Служебные записи macOS `.DS_Store`, `__MACOSX` и `._*` игнорируются при распаковке. Остальные скрытые сегменты пути отклоняются.

### Каталоги на сервере

По умолчанию установленные релизы находятся в `plugins/installed`, а готовые архивы — в `plugins/ready`, относительно корня Backend.

```
plugins/
├── installed/
│   └── <type>/
│       └── <slug>/
│           └── releases/
│               └── <releaseKey>/
└── ready/
```

Пути можно задать серверными переменными `PLUGIN_MANAGER_INSTALLED_DIRECTORY` и `PLUGIN_MANAGER_READY_DIRECTORY`. Эти каталоги должны оставаться раздельными.

Файлы установленного релиза нельзя использовать как рабочее хранилище. Изменение содержимого нарушает проверку целостности. Для состояния интеграции используйте Storage API, для обычных настроек — `config_schema`, для секретов — `settings`.

## Манифест плагина

### Корневые разделы

<table><thead><tr><th width="247.046875">Раздел</th><th>Назначение</th></tr></thead><tbody><tr><td><code>schema</code></td><td>Обязательное точное значение <code>iex.plugin.v2</code>.</td></tr><tr><td><code>package</code></td><td>Обязательные namespace, имя и версия; необязательный короткий ключ.</td></tr><tr><td><code>metadata</code></td><td>Обязательные название и описание; сведения об авторе и совместимости.</td></tr><tr><td><code>runtime</code></td><td>Общий способ выполнения и объявления действий.</td></tr><tr><td><code>capabilities</code></td><td>Непустой список возможностей пакета.</td></tr><tr><td><code>dependencies</code></td><td>Необязательные зависимости и конфликты пакетов.</td></tr><tr><td><code>rateParser</code></td><td>Список пар и настройки их обнаружения для источника курсов.</td></tr><tr><td><code>adminNavigation</code></td><td>Вкладки и пункты навигации административного интерфейса.</td></tr></tbody></table>

Неизвестные корневые поля отклоняются. Не переносите из прежних форматов корневые `type`, `name`, `entry`, `class`, `modules`, `activeByDefault` и `standard`.

### Идентичность, версия и короткий ключ

```json
{
  "namespace": "example",
  "name": "example-rates",
  "version": "1.0.0",
  "key": "rates.example"
}
```

Это содержимое `package`, а не отдельный манифест.

`namespace` и `name` должны состоять из 2–100 символов, начинаться с латинской буквы в нижнем регистре и содержать только `a-z`, `0-9`, `_`, `-`. Версия соответствует SemVer, например `1.0.0` или `1.1.0-beta.1`.

Основная идентичность примера — `example/example-rates`. Необязательный `package.key`, здесь `rates.example`, нужен для короткого поиска пакета. Он не заменяет основную идентичность в зависимостях.

`config.alias` относится к capability. Это отдельное имя, которое используют адаптеры ядра. Изменять namespace, имя пакета, короткий ключ, alias и instance key при обычном обновлении не следует: на них могут ссылаться настройки и код интеграций.

### Метаданные и совместимость

В `metadata` обязательны непустые `title` и `description`. Дополнительно поддерживаются `author`, `homepage`, `support_url`, `license`, `icon`, `translations`, `compatibility` и расширения с префиксом `x-`.

Пример фрагмента совместимости:

```json
{
  "compatibility": {
    "core": ">=11.5.5 <12.0.0",
    "php": ">=8.4 <9.0",
    "laravel": "^13.0",
    "extensions": ["json", "curl"]
  }
}
```

Это пример синтаксиса, а не обещание совместимости с указанными версиями. Указывайте диапазоны, которые проверили для своего пакета. Вместо `core` можно использовать `core_min` и `core_max`; границы включаются в допустимый диапазон. Отсутствующий `core_max` не устанавливает верхнюю границу.

Для расширений допустим список имён или объект с ограничениями версий. Указывайте только расширения, которые действительно нужны Backend для работы пакета.

### Описание capability

Каждая запись содержит `key`, `api_version`, `instance_key` и `permissions`. Дополнительные разделы описывают alias и параметры адаптера, поля формы, секреты, миграции, состояние, события и расписания.

```json
{
  "key": "rates.source",
  "api_version": "v1",
  "instance_key": "default",
  "permissions": ["rates:write"],
  "config": {
    "alias": "example-rates"
  }
}
```

В текущем установщике один пакет может содержать разные типы capability, но один и тот же `key` нельзя повторять, даже с разными `instance_key`. Для нескольких самостоятельных источников одного типа выпускайте отдельные пакеты.

`instance_key` и alias начинаются с латинской буквы в нижнем регистре, имеют длину до 120 символов и допускают `a-z`, `0-9`, `_`, `-`. Активные реализации одной capability не могут иметь одинаковый alias.

Для источника курсов также нельзя занимать alias встроенного источника. Название пакета выбирайте достаточно отличимым: namespace не отменяет проверки конфликтов slug и владельца установленного пакета.

## Возможности и разрешения

### Каталог capabilities

Все перечисленные возможности используют `api_version: "v1"`.

| Capability              | Назначение                                             | Обязательные permissions             |
| ----------------------- | ------------------------------------------------------ | ------------------------------------ |
| `rates.source`          | Источник курсов.                                       | `rates:write`                        |
| `payments.merchant`     | Приём платежей.                                        | `payments:initiate`, `payments:read` |
| `payments.payout`       | Выплаты.                                               | `payouts:initiate`, `payouts:read`   |
| `compliance.kyc`        | Проверка личности.                                     | `kyc:verify`                         |
| `compliance.aml`        | AML-проверка адреса или транзакции.                    | `aml:screen`                         |
| `notifications.channel` | Доставка уведомлений.                                  | `notifications:send`                 |
| `network.proxy`         | Получение прокси для соединения.                       | `proxy:resolve`                      |
| `ui.admin`              | Административная страница.                             | `admin-ui:contribute`                |
| `events.consumer`       | Обработка событий.                                     | `events:subscribe`                   |
| `integration.generic`   | Интеграция с собственными явно вызываемыми действиями. | `core:read`                          |

`geoip.provider` в текущем каталоге отсутствует. Произвольное новое имя capability нельзя подключить одним манифестом: для него потребуется поддержка со стороны ядра.

### Совместимость capabilities с runtime

| Возможность                                                                                                          | Допустимые runtime                                       |
| -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `rates.source`                                                                                                       | Все пять runtime.                                        |
| `payments.merchant`, `payments.payout`, `compliance.kyc`, `compliance.aml`, `notifications.channel`, `network.proxy` | `remote_http`, `wasm`, `isolated_worker`, `native_php`.  |
| `ui.admin`                                                                                                           | `declarative`, `remote_http`.                            |
| `events.consumer`                                                                                                    | `remote_http`, `wasm`, `isolated_worker`.                |
| `integration.generic`                                                                                                | `declarative`, `remote_http`, `wasm`, `isolated_worker`. |

Общий runtime должен поддерживаться каждой возможностью пакета. Например, `native_php` источник курсов и `ui.admin` нельзя объединить в одном пакете.

### Дополнительные permissions

| Capability                               | Допустимые дополнительные permissions                                                                                                                                                                                                                   |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rates.source`                           | `rates:read`, `currencies:read`, `http:egress`, `secrets:read`, `events:publish`, `storage:read`, `storage:write`, `schedules:manage`.                                                                                                                  |
| `payments.merchant`                      | `orders:read`, `orders:write`, `currencies:read`, `http:egress`, `secrets:read`, `events:publish`, `storage:read`, `storage:write`.                                                                                                                     |
| `payments.payout`                        | `orders:read`, `currencies:read`, `http:egress`, `secrets:read`, `events:publish`, `storage:read`, `storage:write`.                                                                                                                                     |
| `compliance.kyc`                         | `customers:pii.read`, `http:egress`, `secrets:read`, `events:publish`, `storage:read`, `storage:write`.                                                                                                                                                 |
| `compliance.aml`                         | `orders:read`, `customers:pii.read`, `http:egress`, `secrets:read`, `events:publish`, `storage:read`, `storage:write`.                                                                                                                                  |
| `notifications.channel`, `network.proxy` | `http:egress`, `secrets:read`, `storage:read`, `storage:write`.                                                                                                                                                                                         |
| `ui.admin`                               | `core:read`.                                                                                                                                                                                                                                            |
| `events.consumer`                        | `core:read`, `rates:read`, `orders:read`, `payments:read`, `payouts:read`, `customers:pii.read`, `events:publish`, `storage:read`, `storage:write`.                                                                                                     |
| `integration.generic`                    | `rates:read`, `currencies:read`, `orders:read`, `payments:read`, `payouts:read`, `customers:pii.read`, `http:egress`, `secrets:read`, `storage:read`, `storage:write`, `events:subscribe`, `events:publish`, `schedules:manage`, `admin-ui:contribute`. |

Наличие имени разрешения в системном справочнике не означает, что его можно запросить любой capability. Например, `payments:refund` не входит в разрешённые списки этих возможностей.

### Permissions, scopes и abilities

`permissions` описывает запрашиваемые права capability. При включении пакета система оформляет разрешённые grants. Перед каждой операцией проверяются состояние возможности, активный релиз и выданные права.

Scope ограничивает конкретное разрешение. Его можно указать рядом с permission:

```json
{
  "permissions": [
    "rates:write",
    {
      "permission": "http:egress",
      "scope": {
        "host": ["api.example.com"]
      }
    },
    {
      "permission": "secrets:read",
      "scope": {
        "key": ["provider.api_key"]
      }
    }
  ]
}
```

Другая допустимая форма — строковые `permissions` и объект `permission_scopes` с теми же областями по имени разрешения. Используйте одну понятную форму описания.

Для `http:egress` задайте `host`, для `secrets:read` — `key`, для `events:publish` — `event_name`. Списки содержат точные значения; не рассчитывайте на wildcard `*`. Область `events:subscribe` может быть получена из объявленных подписок.

`runtime.config.actions.<action>.abilities` определяет дополнительные права токена конкретного вызова. Они должны быть запрошены пакетом и доступны вызываемой capability. Право, выданное соседней capability, автоматически не передаётся.

{% hint style="info" %}
Permission разрешает предусмотренную операцию, но не создаёт API-маршрут. `orders:write`, `payments:initiate` или `rates:write` не предоставляют универсальный доступ к таблицам обменника.
{% endhint %}

## Первый проверочный пакет

### Демонстрационный источник без внешнего API

Этот полный манифест позволяет проверить упаковку, импорт пары и подключение `rates.source`. Курс зафиксирован в JSON и не обновляется с рынка. Используйте пакет только на тестовой установке.

{% code title="iex-plugin.json — демонстрационный declarative-пакет" overflow="wrap" %}

```json
{
  "schema": "iex.plugin.v2",
  "package": {
    "namespace": "example",
    "name": "demo-rates",
    "version": "1.0.0",
    "key": "rates.demo"
  },
  "metadata": {
    "title": "Демонстрационный источник",
    "description": "Фиксированная тестовая котировка для проверки подключения плагина."
  },
  "runtime": {
    "kind": "declarative",
    "config": {
      "actions": {
        "health.check": {
          "abilities": [],
          "result": {
            "status": "ok",
            "mode": "demo"
          }
        },
        "rates.fetch": {
          "abilities": [],
          "result": {
            "rates": [
              {
                "from": "USDT",
                "to": "USD",
                "buy": "1.00000000"
              }
            ]
          }
        }
      }
    }
  },
  "capabilities": [
    {
      "key": "rates.source",
      "api_version": "v1",
      "instance_key": "default",
      "permissions": ["rates:write"],
      "config": {
        "alias": "demo-rates"
      }
    }
  ],
  "rateParser": {
    "discoverOnInstall": false,
    "pairs": [
      {
        "from": "USDT",
        "to": "USD",
        "number_format": 8,
        "status": false
      }
    ]
  }
}
```

{% endcode %}

`declarative` возвращает `result` как `data` успешного ответа. Внутри `result` не работают переменные, HTTP-запросы, шаблоны, PHP, JavaScript и вычисляемые выражения.

Сохраните файл в отдельной папке и соберите ZIP из её содержимого:

```bash
jq empty iex-plugin.json
zip ../demo-rates-1.0.0.zip iex-plugin.json
unzip -l ../demo-rates-1.0.0.zip
```

Дальнейшая установка выполняется через «Проверить пакет» в панели управления. Последовательность проверки приведена ниже.

## Удалённый обработчик remote\_http

### Полный манифест источника курсов

В примере обработчик находится на `plugins.example.com`, а API поставщика — на `api.example.com`. Это разные адреса с разным назначением. Замените оба домена перед проверкой работы.

{% code title="iex-plugin.json — удалённый источник курсов" overflow="wrap" %}

```json
{
  "schema": "iex.plugin.v2",
  "package": {
    "namespace": "example",
    "name": "example-rates",
    "version": "1.0.0",
    "key": "rates.example"
  },
  "metadata": {
    "title": "Example Rates",
    "description": "Получение курсов через отдельный HTTPS-обработчик."
  },
  "runtime": {
    "kind": "remote_http",
    "endpoint_url": "https://plugins.example.com/iex/invoke",
    "config": {
      "actions": {
        "health.check": {
          "abilities": ["http:egress", "secrets:read"]
        },
        "rates.fetch": {
          "abilities": ["http:egress", "secrets:read"]
        }
      }
    }
  },
  "capabilities": [
    {
      "key": "rates.source",
      "api_version": "v1",
      "instance_key": "default",
      "permissions": [
        "rates:write",
        {
          "permission": "http:egress",
          "scope": {
            "host": ["api.example.com"]
          }
        },
        {
          "permission": "secrets:read",
          "scope": {
            "key": ["provider.api_key"]
          }
        }
      ],
      "config": {
        "alias": "example-rates"
      },
      "settings": [
        {
          "key": "provider.api_key",
          "label": "API-ключ поставщика",
          "type": "secret",
          "required": true
        }
      ]
    }
  ],
  "rateParser": {
    "discoverOnInstall": false,
    "pairs": [
      {
        "from": "BTC",
        "to": "USDT",
        "number_format": 8,
        "status": false
      }
    ]
  }
}
```

{% endcode %}

Обработчик для этого пакета должен реализовать `health.check` и `rates.fetch`, прочитать секрет через Runtime API и получить котировки. Сам ZIP не разворачивает удалённый сервис.

### Запрос от обменника

Обменник отправляет `POST` на `runtime.endpoint_url` с JSON:

```json
{
  "protocol": "iex.plugin.invoke.v1",
  "capability": {
    "key": "rates.source",
    "api_version": "v1",
    "instance_key": "default"
  },
  "invocation": {
    "action": "rates.fetch",
    "payload": {
      "options": []
    },
    "context": [],
    "idempotency_key": "opaque-instance-scoped-key",
    "correlation_id": "7d4feaa1-cf6f-4cf6-9874-7239b264c888"
  }
}
```

Пустые `payload`, `context` и вложенные коллекции могут сериализоваться как `[]`. Обработчик должен учитывать фактический контракт вызывающего адаптера и не требовать непредусмотренных полей.

Передаются заголовки:

```http
Authorization: Bearer <краткоживущий токен вызова>
X-iEX-Capability: rates.source
X-iEX-Capability-Version: v1
X-iEX-Correlation-ID: <идентификатор запроса>
Idempotency-Key: <ключ операции>
X-iEX-Gateway-URL: https://app.ваш_домен/api/plugin-platform/v1
Accept-Encoding: identity
```

Адрес обработчика должен использовать HTTPS и порт 443, не содержать логин, пароль, query или fragment. При соединении проверяется публичный адрес назначения. Перенаправления и сжатые ответы не поддерживаются.

### Ответ обработчика

Успешный ответ источника курсов:

```json
{
  "successful": true,
  "data": {
    "rates": [
      {
        "from": "BTC",
        "to": "USDT",
        "buy": "62000.12345678",
        "sell": "62010.12345678"
      }
    ]
  },
  "metadata": {
    "provider": "example"
  }
}
```

Ответ с ошибкой:

```json
{
  "successful": false,
  "data": {},
  "error": {
    "code": "provider_unavailable",
    "message": "Источник временно недоступен."
  },
  "metadata": {}
}
```

Для обработанного запроса с результатом используйте успешный HTTP-статус и корректный JSON-контракт. Ответ HTTP 4xx/5xx считается транспортной ошибкой. Значение `successful` передавайте как JSON boolean, а не строку `"true"`.

В текущем шлюзе неуспешный результат передаётся потребителю с безопасным сообщением об ошибке и пустыми `data` и `metadata`. Не рассчитывайте на передачу бизнес-данных, статуса ожидания или указаний о повторе через эти блоки при `successful: false`.

#### Проверка подлинности вызова

Токен относится к конкретной capability, установленному релизу и вызову. Если удалённый сервис обслуживает несколько обменников, сопоставляйте подключение с заранее разрешённым адресом Gateway и проверяйте токен через `GET /me` этого Gateway.

Не пересылайте Bearer-токен на произвольный адрес из входящего `X-iEX-Gateway-URL`. Сначала сопоставьте адрес со своей конфигурацией доверенных установок. Не записывайте токен и ответы чтения секретов в журнал.

Токен отзывается после завершения вызова. Его нельзя использовать как постоянный API-ключ или сохранять для фонового задания, которое начнётся после возврата ответа.

#### Повторы и идемпотентность

Используйте `idempotency_key` для повторов одной операции. Шлюз связывает ключ с экземпляром capability и содержимым вызова. Повтор с тем же ключом и другим действием, payload или контекстом отклоняется. Для завершившегося успешного вызова может возвращаться сохранённый результат.

Сохраняйте собственную защиту от повторов на стороне обработчика и поставщика. Если внешний сервис выполнил платёж, а HTTP-ответ потерялся, одна только запись шлюза не гарантирует отсутствие повторного платежа.

## Источники курсов

### Путь данных до направления обмена

При установке `rates.source` система формирует список предлагаемых пар, регистрирует источник и запускает импорт строк. Затем установленный источник включается в реестр источников курсов. При обновлении курсов ядро получает значения из runtime, сопоставляет их с настройками пар и использует результат в расчётах направлений.

Список пар и текущие котировки — разные данные. `rateParser.pairs` описывает, какие строки предложить к импорту. `rates.fetch` или PHP-метод `rates()` возвращает актуальные значения.

### Формат котировки

```json
{
  "from": "BTC",
  "to": "USDT",
  "buy": "62000.12345678",
  "sell": "62010.12345678"
}
```

`from`, `to` и `buy` обязательны. `sell` необязателен. Денежные значения передавайте десятичными строками: преобразование через `float` может потерять точность ещё до получения ответа обменником.

Коды валют имеют длину до 64 символов. Первый символ — буква или цифра; далее допускаются буквы, цифры, `.`, `_`, `:`, `+`, `-`. Коды `from` и `to` должны различаться без учёта регистра. Курс должен быть положительным.

Поля `base`, `quote`, `rate`, `price`, `bid`, `ask` автоматически не преобразуются. Выполните явный маппинг ответа своего поставщика в `from`, `to`, `buy`, `sell`.

{% hint style="warning" %}
Не определяйте пару произвольным разрезанием `BTCUSDT` или `ETHBTC`. Используйте метаданные рынка, отдельные коды валют или подтверждённое правило конкретного API. Неправильное направление пары может дать численно корректный, но экономически неверный курс.
{% endhint %}

Некорректные runtime-строки могут быть пропущены. Пустой итоговый набор вызывает ошибку получения курсов. Если поставщик должен возвращать определённый обязательный набор, проверяйте его полноту в самом обработчике.

Для одной пары возвращайте однозначную итоговую котировку. При формировании runtime-карты повтор одной пары может заменить ранее полученное значение. Это отличается от импорта строк пар, где одинаковые записи учитываются как отдельные вхождения.

### Описание импортируемых пар

```json
{
  "rateParser": {
    "discoverOnInstall": false,
    "pairs": [
      {
        "from": "BTC",
        "to": "USDT",
        "type": 0,
        "type_price": "buy",
        "number_format": 8,
        "amount": null,
        "status": false
      }
    ]
  }
}
```

`type: 0` получает курс непосредственно из источника. `type: 1` используется для обратного расчёта по связанной паре; проверьте наличие соответствующей прямой пары и её настройки.

`type_price` выбирает значение котировки, например `buy` или `sell`; для значения по умолчанию можно не задавать это поле.

`number_format` задаёт точность отображения строки от 0 до 18, по умолчанию 10. Он не должен использоваться обработчиком для предварительного усечения точности исходной котировки. Необязательный `amount` — неотрицательное десятичное значение.

Новые пары создаются отключёнными независимо от попытки передать `status: true`. Включение плагина включает источник, но необходимые пары оператор включает отдельно.

Импорт работает через подготовленный снимок и очередь. В карточке показываются состояния «Ожидает загрузки», «Загружаются данные», «Данные загружены», «Ошибка загрузки» и прогресс количества строк. Завершение установки ZIP не означает завершение импорта всех пар.

### Обнаружение пар при установке

`discoverOnInstall: true` применяется к доверенному PHP-источнику. После подтверждения архива установщик выполняет парсер и добавляет найденные пары к явно указанным. Если котировка содержит `sell`, может быть создана отдельная строка для этого значения.

Во время предпросмотра discovery не запускается. Для `remote_http`, `declarative`, `wasm` и `isolated_worker` используйте явно подготовленный `rateParser.pairs`; не рассчитывайте на установочное обнаружение через этот флаг.

{% hint style="warning" %}
Discovery выполняется до сохранения установленного плагина и ввода его секретов. В этот момент парсер может опираться на значения настроек по умолчанию, но не на сохранённый API-ключ. Для приватного API используйте `discoverOnInstall: false` и включите список пар в релиз.
{% endhint %}

## Официальный PHP-источник курсов

### Манифест и контракт

Обработчик реализует `iEXPackages\PluginManager\Contracts\RateParserPluginInterface` с методом `rates(RateParserContext $context): iterable`.

Ниже полный учебный пакет, читающий фиксированный файл котировок. Он показывает контракт PHP без зависимости от неизвестного API поставщика. Для выпуска рабочего источника замените чтение файла обращением к документированному API.

{% code title="iex-plugin.json — официальный PHP-источник" overflow="wrap" %}

```json
{
  "schema": "iex.plugin.v2",
  "package": {
    "namespace": "example",
    "name": "native-demo-rates",
    "version": "1.0.0"
  },
  "metadata": {
    "title": "PHP-источник для проверки",
    "description": "Учебный обработчик с фиксированным файлом котировок."
  },
  "runtime": {
    "kind": "native_php",
    "entrypoint": "src/Parser.php",
    "config": {
      "class": "Example\\NativeDemoRates\\Parser",
      "actions": {
        "health.check": {
          "abilities": []
        },
        "rates.fetch": {
          "abilities": []
        }
      }
    }
  },
  "capabilities": [
    {
      "key": "rates.source",
      "api_version": "v1",
      "instance_key": "default",
      "permissions": ["rates:write"],
      "config": {
        "alias": "native-demo-rates"
      }
    }
  ],
  "rateParser": {
    "discoverOnInstall": false,
    "pairs": [
      {
        "from": "USDT",
        "to": "USD",
        "number_format": 8,
        "status": false
      }
    ]
  }
}
```

{% endcode %}

{% code title="src/Parser.php" overflow="wrap" %}

```php
<?php

declare(strict_types=1);

namespace Example\NativeDemoRates;

use iEXPackages\PluginManager\Contracts\RateParserPluginInterface;
use iEXPackages\PluginManager\DTO\RateParserContext;
use iEXPackages\PluginManager\DTO\RateParserRuntimeRate;
use RuntimeException;

final class Parser implements RateParserPluginInterface
{
    public function rates(RateParserContext $context): iterable
    {
        $json = file_get_contents($context->pluginPath('data/rates.json'));

        if ($json === false) {
            throw new RuntimeException('Не удалось прочитать котировки.');
        }

        $rows = json_decode($json, true, 32, JSON_THROW_ON_ERROR);

        if (! is_array($rows) || ! array_is_list($rows) || $rows === []) {
            throw new RuntimeException('Источник вернул пустой или неверный список.');
        }

        $rates = [];

        foreach ($rows as $row) {
            $rate = is_array($row)
                ? RateParserRuntimeRate::fromArray($row)
                : null;

            if ($rate === null) {
                throw new RuntimeException('Источник вернул некорректную котировку.');
            }

            $rates[] = $rate;
        }

        return $rates;
    }
}
```

{% endcode %}

{% code title="data/rates.json" %}

```json
[
  {
    "from": "USDT",
    "to": "USD",
    "buy": "1.00000000"
  }
]
```

{% endcode %}

В ZIP должны находиться все три файла. Проверка манифеста не назначает пакету официальный статус: для установки этого runtime дополнительно требуется официальный подписанный канал поставки.

### Доступный контекст PHP-парсера

| Поле или метод                            | Назначение                                                                    |
| ----------------------------------------- | ----------------------------------------------------------------------------- |
| `$context->plugin`                        | Контекст установленного плагина; при discovery это ещё не сохранённая запись. |
| `$context->manifest`                      | Проверенное описание пакета.                                                  |
| `$context->options`                       | Параметры текущего получения курсов.                                          |
| `$context->extra`                         | Дополнительный подготовленный контекст.                                       |
| `$context->config`                        | Обычные настройки, сгруппированные по ключу capability.                       |
| `$context->secrets`                       | Секреты, сгруппированные по ключу capability.                                 |
| `$context->pluginPath('data/rates.json')` | Построение пути относительно релиза.                                          |

Пример чтения объявленных настроек:

```php
$timeout = (int) ($context->config['rates.source']['timeout'] ?? 15);

$apiKey = $context->secrets['rates.source']['provider.api_key'] ?? null;

$isHealthCheck = (bool) ($context->options['health_check'] ?? false);
```

`pluginPath()` соединяет путь с каталогом релиза. Этот метод не проверяет произвольный пользовательский ввод: передавайте ему фиксированные известные пути своего пакета.

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

### Запуск PHP-парсера

Парсер курсов запускается отдельным CLI-процессом PHP через внутреннюю команду `iex:plugin-runtime-php`. Установщик и рабочие сервисы формируют её входные файлы самостоятельно; вручную вызывать её при установке не требуется.

По умолчанию применяются ограничения: 20 секунд выполнения, 100 000 котировок и 64 MiB выходных данных. До и после запуска проверяется допустимость текущего релиза. Для CLI нужен исполняемый PHP, а не бинарный файл PHP-FPM; при необходимости сервер задаёт `PLUGIN_MANAGER_PHP_BINARY`.

{% hint style="danger" %}
Отдельный процесс PHP-парсера не является песочницей. Код работает с возможностями среды обменника. Разрешения манифеста не превращают прямой PHP-доступ к файлам, сети и Laravel в изолированный Runtime API.
{% endhint %}

## Настройки и данные подключения

### Обычные настройки

`config_schema` описывает поля раздела «Настройки плагина». Значения сохраняются отдельно от файлов релиза.

```json
{
  "config_schema": [
    {
      "key": "timeout",
      "label": "Время ожидания, секунд",
      "type": "integer",
      "required": true,
      "default": 15,
      "metadata": {
        "min": 1,
        "max": 20
      }
    },
    {
      "key": "price_side",
      "label": "Сторона котировки",
      "type": "select",
      "default": "buy",
      "options": [
        {
          "value": "buy",
          "label": "Покупка"
        },
        {
          "value": "sell",
          "label": "Продажа"
        }
      ]
    }
  ]
}
```

Поддерживаются типы `string`, `text`, `textarea`, `url`, `email`, `number`, `integer`, `boolean`, `select`, `json`.

Для `select` нужны допустимые значения `options`. Числовые границы и ограничения длины задаются через поддерживаемые поля `metadata`, в том числе `min`, `max`, `max_length`.

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

{% hint style="info" %}
`config_schema` создаёт поля настройки, но не означает автоматическую передачу всех значений в любой runtime. PHP-парсер получает их через `RateParserContext`. Для других обработчиков проверяйте payload конкретного адаптера. Общего Runtime API для чтения `config_schema` сейчас нет.
{% endhint %}

Не смешивайте `config_schema` с `config`: последний содержит объявленные разработчиком параметры адаптера, например alias, описание платёжного шлюза или схему административной страницы.

### Секретные значения

`settings` описывает поля «Данные подключения». Значения хранятся в зашифрованном виде и не должны присутствовать в ZIP.

```json
{
  "settings": [
    {
      "key": "provider.api_key",
      "label": "API-ключ поставщика",
      "description": "Ключ для доступа к API интеграции.",
      "type": "secret",
      "required": true
    }
  ]
}
```

Объявите `secrets:read` с областью нужного ключа и добавьте эту ability действиям, которые читают секрет через Runtime API. Поле `required: true` участвует в проверке готовности перед включением.

Не храните секрет в `config`, `config_schema.default`, `metadata`, `extra`, сообщениях об ошибке и тестовых ответах.

## Runtime API обменника

### Адрес и авторизация

Базовый путь:

```
https://app.ваш_домен/api/plugin-platform/v1
```

Используйте выданный токен вызова в `Authorization: Bearer ...`. Пакет, capability и релиз определяются по токену. Передача собственного `plugin_id` не предоставляет доступ к другому плагину.

### Доступные маршруты

| Метод и путь            | Назначение                                   | Ability            |
| ----------------------- | -------------------------------------------- | ------------------ |
| `GET /me`               | Проверить контекст токена.                   | Действующий токен. |
| `POST /secrets/read`    | Прочитать объявленный секрет по `key`.       | `secrets:read`     |
| `GET /storage`          | Получить список ключей namespace.            | `storage:read`     |
| `POST /storage/get`     | Получить одно значение.                      | `storage:read`     |
| `PUT /storage`          | Создать или изменить значение.               | `storage:write`    |
| `DELETE /storage`       | Удалить значение.                            | `storage:write`    |
| `POST /events`          | Опубликовать событие своего пакета.          | `events:publish`   |
| `POST /http/request`    | Выполнить разрешённый исходящий HTTP-запрос. | `http:egress`      |
| `GET /core/orders/{id}` | Прочитать разрешённую заявку.                | `orders:read`      |
| `GET /core/currencies`  | Получить доступный перечень валют.           | `currencies:read`  |

Ответы этих маршрутов используют оболочку `data`. Это API обменника; его ответы отличаются от контракта `successful/data/error` удалённого обработчика.

### Чтение секрета

Тело `POST /secrets/read`:

```json
{
  "key": "provider.api_key"
}
```

Значение возвращается в `data.value`. Выполняйте чтение из серверного обработчика в рамках действующего вызова. Не показывайте ответ в административной форме и не отправляйте его браузеру.

### Работа с хранилищем

Тело `PUT /storage`:

```json
{
  "namespace": "sync",
  "key": "last_cursor",
  "value": {
    "cursor": "page-12"
  },
  "expected_version": 0,
  "ttl_seconds": 3600
}
```

`expected_version: 0` означает создание отсутствующей записи. Для изменения с защитой от конкурентной записи передайте текущую версию. При несовпадении операция отклоняется. Без `expected_version` запись выполняется без этой проверки.

Ответ содержит `data.value` и новую `data.version`. Версии записей доступны также в списке `GET /storage`.

Тело `POST /storage/get` и `DELETE /storage`:

```json
{
  "namespace": "sync",
  "key": "last_cursor"
}
```

Для списка передайте query-параметры `namespace`, необязательные `cursor` и `limit`. По умолчанию возвращается до 50 записей, максимум — 100; следующая страница обозначается `data.next_cursor`.

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

### Исходящие HTTP-запросы

Тело `POST /http/request`:

```json
{
  "method": "GET",
  "url": "https://api.example.com/v1/tickers",
  "headers": {
    "Accept": "application/json"
  }
}
```

Если API требует авторизацию, обработчик получает секрет через `/secrets/read` и добавляет необходимый заголовок в запрос. Формат авторизации определяется документацией поставщика.

Поддерживаются `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, необязательные `headers` и `body`. Ответ содержит `data.status`, ограниченный набор `data.headers` и `data.body`. Проверьте HTTP-статус поставщика и отдельно разберите его тело.

Разрешены публичные HTTPS-адреса на порту 443 в объявленной области `host`. Перенаправления и сжатые ответы запрещены.

Нельзя передавать служебные заголовки `Host`, `Cookie`, `Set-Cookie`, `Content-Length`, `Connection`, `X-Forwarded-For`, `Accept-Encoding`.

Ограничения этого broker относятся к запросам через Runtime API. Сетевую политику вашего удалённого сервера необходимо обеспечивать в его собственной среде.

### Чтение заявки и валют

Для `GET /core/orders/{id}` недостаточно одного имени `orders:read`. Идентификатор заявки должен соответствовать доверенному контексту вызова, содержащему `order_id` или `task_id`. Нельзя использовать произвольный номер из клиентского ввода для обхода этой области.

Ответ содержит предусмотренные контрактом сведения о заявке: идентификаторы, статус, суммы, курс, связанные сущности, платёжные сведения и даты. Дополнительные персональные сведения требуют `customers:pii.read` в токене и разрешениях capability.

`GET /core/currencies` возвращает предусмотренные ядром данные активных валют: идентификаторы, коды, обозначения, сети и точность. Этот маршрут не предоставляет редактирование валют.

## Подключение к рабочим процессам

### Приём платежей и выплаты

Возможности `payments.merchant` и `payments.payout` подключаются к существующему менеджеру платёжных шлюзов по alias. После установки требуется настройка соответствующего платёжного процесса обменника; ZIP не создаёт готовые правила направлений сам по себе.

Адаптер вызывает action в формате `payments.<operation>` для обеих возможностей. Конкретное `<operation>` определяется вызывающим платёжным процессом. Не заменяйте этот префикс на `payouts.*` только из-за имени capability.

В payload передаются `operation`, `parameters`, `task_id`, ограниченные `headers` и `merchant_config_keys`. Последнее поле содержит имена параметров подключения, а не их секретные значения. В `context` передаётся операция и, при наличии заявки, её `order_id`.

Параметры платёжного адаптера описываются в `config.gateway`: предусмотрены метаданные, операции и входные поля. Объявляйте операции, которые действительно реализует обработчик и вызывает выбранный сценарий ядра. Все соответствующие actions должны присутствовать в `runtime.config.actions`.

{% hint style="warning" %}
Платёжный адаптер учитывает `successful: true` как успешный результат операции, а также распознаёт статусы `success`, `successful`, `paid`, `completed`. Не используйте `successful: true` только для обозначения «HTTP-запрос принят», если платёж ещё ожидается. Сочетание этого флага со статусом `pending` требует особой проверки: методы определения успеха и ожидания могут одновременно вернуть положительный результат.
{% endhint %}

Передача `successful: false` с `data.status: "pending"` также не решает эту задачу: шлюз очищает `data` неуспешного ответа. Для асинхронной платёжной интеграции сначала согласуйте и проверьте поддержку промежуточных состояний во всей цепочке ядра. Одного манифеста для исправления этого ограничения недостаточно.

Для каждого используемого сценария проверьте создание операции, ожидание, окончательный успех, отказ, отмену, повтор запроса и потерю ответа. Имена возвращаемых полей должны соответствовать обращениям потребителя: динамический getter `getTransactionId()` читает `transaction_id`.

### KYC

`compliance.kyc` использует actions `kyc.issue` и `kyc.status`. Обработчик получает разрешённые поля пользователя, описание сервиса и подготовленный контекст проверки.

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

Возвращаемые данные должны согласовывать `success`, `status`, `message`, `external_id`, `completed` и данные результата с состояниями внешнего сервиса. Получение ссылки на проверку не означает, что клиент её успешно прошёл.

### AML

`compliance.aml` использует `aml.transaction.check` и `aml.address.check`. Payload содержит предусмотренные адаптером поля `currency`, `address`, `tx`, `amount`, `direction`, `client_id`, `extra`.

Ответ включает состояние проверки, `successful`, `risk_score`, `risk_signals`, `external_id`. Приводите риск к шкале 0–100 и согласуйте сигналы с настройками порогов `risk_level` и `max_risk_<signal>`.

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

### Уведомления

`notifications.channel` реализует `notifications.send` и, для проверки подключения, `notifications.test`.

Отправка получает `event`, `model`, `recipient`, `message`, `payload`. В `recipient` передаются канал, аудитория, адрес назначения и предусмотренные сведения получателя. В успешном ответе используются `message_id`, `status` и дополнительные `metadata`.

Адаптер предусматривает признаки `retryable` и `retry_after_seconds` для ошибки, однако текущий шлюз очищает `metadata` неуспешного ответа. Не рассчитывайте, что эти указания удалённого обработчика дойдут до механизма повторной доставки.

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

Параметры подключения канала описываются поддерживаемой адаптером `config.connection_schema`. Они не заменяют секретные поля `settings` и разрешения на их чтение.

### Прокси

`network.proxy` выполняет `proxy.resolve`. В payload передаётся контекст соединения; в контексте прав используются предусмотренные ядром значения назначения, страны и сети.

Ответ описывает `type`, `host`, `port` и при необходимости `username` или `login`, `password`. Поддерживаются `http`, `https`, `socks4`, `socks5`.

`host` должен быть публичным IP-адресом, а не доменным именем; порт — от 1 до 65535. Адаптер формирует подключение для текущего использования и не создаёт постоянную запись прокси. Сервер выбирает провайдера через `PROXY_PLUGIN_PROVIDER_ALIAS`.

### Универсальные интеграции

`integration.generic` подходит для собственных действий, расписаний и интеграционной логики в рамках её разрешений. Само объявление capability не означает автоматический вызов обработчика.

Свяжите action с конкретным потребителем: серверным вызовом через фасад плагинов, подпиской на событие или расписанием. Добавление `admin-ui:contribute` к универсальной интеграции само по себе не создаёт страницу: для страницы нужна `ui.admin`.

## События

### Подписка на события ядра

Подписки объявляются в `events` capability с доступом `events:subscribe`. Обработчик должен реализовать `events.deliver`.

| Событие                                   | Дополнительное разрешение |
| ----------------------------------------- | ------------------------- |
| `orders.created`, `orders.status_changed` | `orders:read`             |
| `payments.updated`                        | `payments:read`           |
| `payouts.updated`                         | `payouts:read`            |
| `customers.verified`                      | `customers:pii.read`      |
| `rates.updated`                           | `rates:read`              |
| `plugin.lifecycle`                        | `core:read`               |

Пример содержимого capability для подписчика:

```json
{
  "key": "events.consumer",
  "api_version": "v1",
  "instance_key": "default",
  "permissions": [
    "events:subscribe",
    "orders:read",
    "storage:read",
    "storage:write"
  ],
  "config": {
    "alias": "example-order-events"
  },
  "events": [
    {
      "name": "orders.status_changed",
      "api_version": "v1",
      "max_attempts": 8,
      "timeout_seconds": 15
    }
  ]
}
```

Для этого примера объявите `health.check` и `events.deliver` в runtime. Если обработчик использует хранилище или читает заявку через Runtime API, добавьте соответствующие `storage:read`, `storage:write`, `orders:read` в abilities действия `events.deliver`.

### Формат доставки

В `invocation.payload` передаётся объект:

```json
{
  "event": {
    "id": "7d4feaa1-cf6f-4cf6-9874-7239b264c888",
    "name": "orders.status_changed",
    "api_version": "v1",
    "aggregate_type": "order",
    "aggregate_id": "123",
    "occurred_at": "2026-09-08T10:00:00+00:00",
    "source": {
      "core": true
    },
    "payload": {
      "order": {
        "id": 123,
        "from_status": 1,
        "to_status": 2
      }
    }
  }
}
```

Числа в примере условные. `from_status` и `to_status` содержат идентификаторы статусов конкретной заявки; не приписывайте им назначение по порядковому номеру из примера.

Доставка проходит через outbox и очередь. При обработке применяются подписки текущего релиза, состояние capability, фильтры и разрешения. Фильтры подписки используют пути к полям документа события.

Доставка может повторяться. Учитывайте `event.id` и ключ идемпотентности. Повторы ограничены количеством попыток; терминальные состояния доставки включают доставленное, отфильтрованное и окончательно не доставленное событие.

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

### Публикация собственного события

Имя начинается с `plugins.<namespace>.<name>.`. Для пакета `example/example-rates` допустим собственный префикс `plugins.example.example-rates.`.

Тело `POST /events`:

```json
{
  "event_name": "plugins.example.example-rates.sync.completed",
  "api_version": "v1",
  "payload": {
    "imported": 25
  },
  "aggregate_type": "sync",
  "aggregate_id": "run-42",
  "deduplication_key": "sync-run-42"
}
```

Запрос требует `events:publish`, точного имени события в scope и соответствующей ability действия. Событие принимается с HTTP 202; принятие не означает завершение обработки всеми подписчиками.

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

Не включайте секреты и лишние персональные данные в payload. Система удаляет определённые чувствительные поля, но это не заменяет правильный состав события у отправителя.

## Расписания

### Объявление задания

Расписание задаётся в `schedules` capability с разрешением `schedules:manage`. Action должен присутствовать в `runtime.config.actions`.

```json
{
  "schedules": [
    {
      "name": "refresh-cache",
      "cron": "*/5 * * * *",
      "timezone": "UTC",
      "action": "integration.refresh",
      "payload": {
        "mode": "incremental"
      },
      "enabled": true,
      "max_concurrency": 1
    }
  ]
}
```

Это фрагмент capability, например `integration.generic`. Добавьте требуемые permissions и abilities в тот же пакет. Расписание не предоставляет дополнительные права само по себе.

Используется cron с часовым поясом. По умолчанию `timezone` — `UTC`, `enabled` — `true`, `max_concurrency` — `1`; допустимый диапазон параллелизма — 1–10.

### Выполнение и повтор

Система сохраняет отдельную запись запуска и затем передаёт её в очередь. Обработчик получает объявленный payload и контекст с `schedule`, `schedule_run_id`, `due_at`. Повтор одного запуска сохраняет его идентичность.

Для работы нужны планировщик Laravel и обработчик очереди `plugins`, если сервер не переопределил её название. По умолчанию повтор ограничен восемью попытками с увеличением задержки.

Не добавляйте собственный системный cron в ZIP. Расписания PluginManager управляются манифестом; отдельного публичного Runtime API для их создания и изменения сейчас нет.

## Административный интерфейс

### Как создаётся страница

Плагин описывает страницу через JSON DSL в `config.views` capability `ui.admin`. Ядро проверяет описание и отображает поддерживаемые элементы. Загрузка собственного HTML, JavaScript или Vue-компонента из ZIP этим механизмом не предусмотрена.

Нужны `ui.admin`, permission `admin-ui:contribute`, runtime `declarative` или `remote_http`, описание страницы, разрешённые actions и пункт `adminNavigation`.

| Часть страницы | Поддерживаемые значения                                                |
| -------------- | ---------------------------------------------------------------------- |
| Layout         | `dashboard`, `form`, `table`, `detail`.                                |
| Block          | `stat`, `text`, `alert`, `table`, `form`, `actions`.                   |
| Alert variant  | `default`, `info`, `success`, `warning`, `danger`.                     |
| Table format   | `text`, `number`, `money`, `date`, `datetime`, `boolean`, `badge`.     |
| Form field     | `text`, `textarea`, `number`, `boolean`, `select`, `date`, `datetime`. |
| Button variant | `default`, `outline`, `danger`.                                        |

### Пример страницы состояния

Следующий фрагмент добавляется в `capabilities` отдельного пакета `example/example-admin` с `remote_http`.

```json
{
  "key": "ui.admin",
  "api_version": "v1",
  "instance_key": "default",
  "permissions": ["admin-ui:contribute"],
  "config": {
    "alias": "example-admin",
    "views": [
      {
        "id": "dashboard",
        "title": "Состояние интеграции",
        "layout": "dashboard",
        "load_action": "admin.dashboard.load",
        "refresh_seconds": 60,
        "blocks": [
          {
            "id": "status",
            "type": "stat",
            "title": "Состояние",
            "data_key": "status"
          },
          {
            "id": "notice",
            "type": "alert",
            "variant": "info",
            "text": "Данные загружаются из обработчика интеграции."
          },
          {
            "id": "check",
            "type": "actions",
            "actions": [
              {
                "action": "admin.connection.check",
                "label": "Проверить соединение",
                "variant": "outline"
              }
            ]
          }
        ]
      }
    ]
  }
}
```

В `runtime.config` этого пакета объявите:

```json
{
  "actions": {
    "health.check": {
      "abilities": []
    },
    "admin.dashboard.load": {
      "abilities": []
    },
    "admin.connection.check": {
      "abilities": []
    }
  }
}
```

Load action получает:

```json
{
  "view_id": "dashboard",
  "parameters": []
}
```

Пример ответа:

```json
{
  "successful": true,
  "data": {
    "status": "Доступен"
  }
}
```

Действие кнопки или формы получает `view_id` и `input`.

Проверка соединения в этом примере выполняется вашим удалённым обработчиком. Не добавляйте `storage:write`, `secrets:read` или `http:egress` к `ui.admin`: эти permissions ей недоступны.

Для другой логики предусмотрите отдельную допустимую capability и явный способ её вызова; права разных возможностей не объединяются.

### Навигация

Корневой `adminNavigation` поддерживает `category`, `menu`, `node`. Категория группирует меню, `menu` добавляет корневой пункт, `node` — узел в существующий родительский пункт.

Фрагмент для отдельного пакета `example/example-admin`, тип которого определяется как `external`:

```json
{
  "adminNavigation": {
    "version": 1,
    "contributions": [
      {
        "kind": "category",
        "id": "example-tools",
        "tab": "plugins",
        "order": 100,
        "definition": {
          "id": "example-tools",
          "name": {
            "ru": "Example",
            "en": "Example"
          },
          "icon": "plugin",
          "children": []
        }
      },
      {
        "kind": "menu",
        "id": "example-dashboard",
        "categoryId": "example-tools",
        "definition": {
          "id": "example-dashboard",
          "name": {
            "ru": "Состояние интеграции",
            "en": "Integration status"
          },
          "icon": "chart",
          "permission": "admin_plugin_view|admin_plugin_install",
          "link": "/iexadmin/plugins/extensions/external/example-admin/default/dashboard",
          "activeMatch": {
            "path": "/iexadmin/plugins/extensions/external/example-admin/default/dashboard",
            "mode": "prefix"
          }
        }
      }
    ]
  }
}
```

Ссылка должна вести на собственный extension route:

```
/iexadmin/plugins/extensions/<pluginType>/<pluginSlug>/<instanceKey>/<viewId>
```

`pluginType` определяется возможностями пакета; это не произвольный корневой параметр манифеста. Если объединяете страницу с другой capability, проверьте итоговый тип и соответствующий путь. Внешние ссылки запрещены.

Допустимые имена иконок:

```
bolt
chart
cloud-update
code
database
folder
folders
gear
globe
link
list
plug
plugin
puzzle
shop
user
```

### Права администратора и обновление данных

Просмотр схемы страницы и выполнение обработчика — разные действия. Для просмотра используется `admin_plugin_view` или `admin_plugin_install`.

Для запуска загрузки данных, формы и кнопок требуется `admin_plugin_execute` или `admin_plugin_install`; управляющие маршруты также применяют предусмотренные системой проверки чувствительных действий.

GET страницы возвращает её схему и сам по себе не запускает runtime. Данные загружаются отдельным POST-запросом. Параллельные загрузки одной страницы одним администратором в пределах интервала могут использовать один результат.

`refresh_seconds: 0` отключает автоматический опрос. Положительный интервал ограничивается минимальным значением 60 секунд по умолчанию. Не превращайте административную страницу в непрерывный поток частых запросов.

## Локализация

Метаданные и capability могут содержать `translations` с переводами `title` и `description`:

```json
{
  "translations": {
    "ru": {
      "title": "Источник курсов",
      "description": "Получение котировок поставщика."
    },
    "en": {
      "title": "Rate source",
      "description": "Provider exchange rates."
    }
  }
}
```

У полей `settings`, `config_schema` и вариантов выбора используются переводы `label` и `description`. Административная DSL поддерживает переводы отображаемых подписей. Для пунктов навигации пример выше использует локализованный объект `name`.

Не переводите `package.name`, `package.key`, alias, instance key, ключи полей, action и permission. Это технические идентификаторы, на которых основаны связи и сохранённые значения.

## Зависимости между плагинами

### Обязательные, дополнительные и конфликтующие пакеты

```json
{
  "dependencies": {
    "requires": [
      {
        "package": "example/common-integration",
        "version": "^2.0",
        "state": "enabled"
      }
    ],
    "optional": [
      {
        "package": "example/report-tools",
        "version": "^1.0",
        "state": "enabled"
      }
    ],
    "conflicts": [
      {
        "package": "example/legacy-integration",
        "state": "installed"
      }
    ]
  }
}
```

В `package` зависимости передаётся полная идентичность `namespace/name`.

`version` использует ограничения Composer Semver и может отсутствовать. `state: installed` означает достаточность установленного пакета; `enabled` требует включённого пакета.

По умолчанию `requires` и `optional` используют `enabled`, а `conflicts` — `installed`. В примерах лучше указывать состояние явно.

`requires` блокирует включение при невыполненном условии. `optional` описывает дополнительную интеграцию и не блокирует включение. `conflicts` блокирует включение при наличии пакета, подходящего под заданное состояние и версию.

### Поведение при установке и отключении

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

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

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

## Использование плагина из кода обменника

### Проверка наличия и доступности в PHP

Для серверного интеграционного кода предусмотрен фасад `iEXPackages\PluginManager\Facades\Plugins`.

```php
use iEXPackages\PluginManager\Facades\Plugins;

$installed = Plugins::installed('example/example-rates');

$enabled = Plugins::enabled('rates.example');

$status = Plugins::status('example/example-rates')->value;

$details = Plugins::inspect('example/example-rates');
```

`installed()` проверяет факт установки и может вернуть `true` для отключённого или неисправного пакета.

`enabled()` проверяет доступность пакета по реестру и активному релизу. Эти методы используют кеш и не запускают обработчик.

Для массовой проверки используйте `inspectMany()`. Состояние `degraded` может допускать выполнение. Проверка пакета не заменяет разрешение конкретной capability и реальную проверку её результата.

### Загрузка и вызов capability

Пример для отдельного пакета `example/example-tools`, объявляющего `integration.generic`, alias `example-tools`, permission `core:read` и action `integration.status`:

```php
use iEXPackages\PluginManager\Facades\Plugins;

$plugin = Plugins::loadPlugin('example/example-tools');

if (
    $plugin === null
    || ! $plugin->hasCapability('integration.generic', 'example-tools')
) {
    return null;
}

$result = $plugin->invoke(
    capability: 'integration.generic',
    alias: 'example-tools',
    requiredPermission: 'core:read',
    action: 'integration.status',
    payload: ['mode' => 'summary'],
    context: [],
);

return $result->successful ? $result->data : null;
```

`loadPlugin()` возвращает объект доступного пакета или `null`; само получение объекта не подключает PHP-код из ZIP.

`requireEnabledPlugin()` предназначен для обязательной интеграции и сообщает об отсутствии доступного пакета исключением.

Каждый вызов снова разрешает capability и проходит через шлюз. Не передавайте непроверенный клиентский ввод в доверенный `context`. Для изменяющих операций задавайте устойчивый `idempotencyKey`.

Эти примеры относятся к коду Backend. Они не предоставляют неподписанному ZIP право исполнять PHP внутри обменника.

### Проверка доступности во Vue

В интерфейсе Backend предусмотрен `usePlugin()`:

```vue
<script setup lang="ts">
import { usePlugin } from '@/modules/plugin-manager/usePlugin';

const { enabled, loading, error, refresh } = usePlugin('rates.example');
</script>

<template>
  <p v-if="loading">Проверяется доступность плагина…</p>
  <p v-else-if="error">Не удалось получить состояние плагина.</p>
  <p v-else-if="enabled">Интеграция доступна.</p>
  <p v-else>Интеграция недоступна.</p>

  <button type="button" @click="refresh()">
    Обновить состояние
  </button>
</template>
```

Composable возвращает состояние, сведения о пакете, признаки установки и доступности, ошибку и метод обновления. Общий loader объединяет проверки и использует кеш.

Это механизм существующего Vue-приложения обменника. Он не загружает Vue-компоненты из ZIP. Для страницы самого плагина используйте `ui.admin`.

## WASM и isolated\_worker

### Описание runtime

Для WASM укажите entrypoint существующего файла модуля и необходимые actions:

```json
{
  "kind": "wasm",
  "entrypoint": "dist/plugin.wasm",
  "config": {
    "actions": {
      "health.check": {
        "abilities": []
      },
      "integration.status": {
        "abilities": []
      }
    }
  }
}
```

Это содержимое `runtime` пакета с подходящей capability, например `integration.generic`.

Для `isolated_worker` дополнительно требуется `runtime.config.image` — OCI-образ с точным digest:

```
registry.example.com/plugins/example-tools@sha256:<64 шестнадцатеричных символа digest>
```

Строка выше показывает формат. Подставьте реальный digest собранного образа; `latest` или один изменяемый tag не подходят. Entrypoint, команда, аргументы и доступность артефакта должны соответствовать реализации worker.

### Протокол внешнего исполнителя

Backend отправляет `POST <worker_url>/v1/invocations` с протоколом `iex.plugin.worker.v1`.

Запрос включает runtime, ссылку на артефакт и его хеши, entrypoint, runtime config, сведения о capability, данные Gateway, ограничения и invocation.

Worker настраивается серверными параметрами `PLUGIN_PLATFORM_WORKER_URL`, `PLUGIN_PLATFORM_WORKER_TOKEN`, `PLUGIN_PLATFORM_WORKER_AUDIENCE`.

Машинный секрет worker и краткоживущий токен Runtime API имеют разное назначение.

Запрос содержит заголовки `X-iEX-Timestamp`, `X-iEX-Nonce`, `X-iEX-Signature`, `X-iEX-Worker-Audience`. Подпись запроса вычисляется над точными байтами:

```
sha256=HMAC-SHA256(worker_token, timestamp + "\n" + nonce + "\n" + request_body)
```

Ответ возвращает контракт `successful/data/error/metadata` и заголовок `X-iEX-Response-Signature`:

```
sha256=HMAC-SHA256(worker_token, request_timestamp + "\n" + request_nonce + "\n" + response_body)
```

Согласуйте с исполнителем ABI модуля, загрузку артефакта, проверку хешей, пределы памяти и времени, сетевые ограничения и защиту от повторного выполнения запросов.

Не предполагается универсальная команда сборки WASM или готовая контейнерная песочница только на основании формата манифеста.

## Другие официальные PHP-обработчики

### Контракт NativePluginCapabilityInterface

Для официальной native capability, которая использует общий шлюз, применяется `iEXPackages\PluginManager\Contracts\NativePluginCapabilityInterface`:

```php
public function invoke(
    \iEXPackages\PluginManager\DTO\PluginInvocation $invocation,
): \iEXPackages\PluginManager\DTO\PluginInvocationResult;
```

`PluginInvocation` содержит `action`, `payload`, `context`, `idempotencyKey`, `correlationId`, `timeoutSeconds`.

Обработчик возвращает `PluginInvocationResult` с результатом или безопасной ошибкой неподдерживаемого действия.

Entrypoint подключается через `require_once`, а класс из `runtime.config.class` создаётся Laravel-контейнером.

Не используйте один класс как источник курсов и общий обработчик, если он не реализует оба необходимых контракта и их поведение.

### Отличие от PHP-парсера курсов

Общий native-обработчик выполняется в текущем процессе приложения. На него нельзя переносить гарантию отдельного CLI-процесса и принудительного таймаута PHP-парсера курсов.

`timeoutSeconds` в DTO не останавливает произвольный синхронный PHP-код сам по себе. Задавайте собственные ограниченные таймауты сетевых операций, избегайте длительных циклов и учитывайте возможности среды Laravel.

Этот runtime разрешён только совместимым capability из каталога и только при уровне доверия `official`. Например, `integration.generic` и `ui.admin` не поддерживают `native_php`.

## Миграции данных

### Объявление миграции

PHP-миграции поставляются только в официальном релизе. Путь объявляется внутри capability:

```json
{
  "migrations": [
    "database/migrations/2026_09_08_000000_create_example_plugin_cache.php"
  ]
}
```

Файл должен возвращать объект с методом `up()`. Пример для PostgreSQL:

{% code title="database/migrations/2026\_09\_08\_000000\_create\_example\_plugin\_cache.php" overflow="wrap" %}

```php
<?php

declare(strict_types=1);

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class {
    public function up(): void
    {
        if (Schema::hasTable('example_plugin_cache')) {
            return;
        }

        Schema::create('example_plugin_cache', static function (Blueprint $table): void {
            $table->id();
            $table->string('cache_key')->unique();
            $table->jsonb('payload');
            $table->timestamps();
        });
    }
};
```

{% endcode %}

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

### Запуск и повтор

Установка регистрирует миграции. Выполнение запускается отдельно действием «Применить обновление данных». Перед запуском снова проверяются официальный уровень доверия и контрольная сумма файла.

Миграция вызывается через `up()`. Автоматический вызов `down()` при откате версии или удалении плагина не предусмотрен.

Не считайте весь код `up()` автоматически защищённым единой транзакцией только потому, что система ведёт журнал миграций.

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

Перед применением изменений к рабочей базе подготовьте резервную копию и проверьте миграцию на тестовой копии данных.

## Проверка и установка

### Проверка файлов и сборка ZIP

{% stepper %}
{% step %}

#### Проверьте JSON

Выполните в каталоге исходников пакета:

```bash
jq empty iex-plugin.json
```

Команда проверяет только синтаксис JSON. Для проверки структуры используйте актуальную JSON Schema формата v2 из целевой версии PluginManager.

Затем проверьте манифест средствами самого установщика: схема не заменяет проверки runtime, прав и содержимого ZIP.
{% endstep %}

{% step %}

#### Проверьте код обработчика

Для PHP-пакета:

```bash
php -l src/Parser.php
```

Повторите проверку для каждого PHP-файла, включая миграции.

Для удалённого обработчика выполните его собственные тесты протокола, авторизации, таймаутов, повторов и маппинга ответа поставщика.
{% endstep %}

{% step %}

#### Соберите архив

Для приведённого PHP-примера:

```bash
zip -r ../native-demo-rates-1.0.0.zip iex-plugin.json src data
unzip -l ../native-demo-rates-1.0.0.zip
unzip -p ../native-demo-rates-1.0.0.zip iex-plugin.json | jq
```

Добавляйте `assets` и `database` только при использовании этих файлов. Для удалённого примера достаточно включить `iex-plugin.json`.
{% endstep %}

{% step %}

#### Рассчитайте SHA-256

{% tabs %}
{% tab title="Linux" %}

```bash
sha256sum ../native-demo-rates-1.0.0.zip
```

{% endtab %}

{% tab title="macOS" %}

```bash
shasum -a 256 ../native-demo-rates-1.0.0.zip
```

{% endtab %}
{% endtabs %}

Контрольная сумма относится к окончательному ZIP. Любое изменение архива требует нового расчёта и, для официальной поставки, новой подписи.
{% endstep %}
{% endstepper %}

### Предпросмотр и установка в панели

{% stepper %}
{% step %}

#### Выберите источник пакета

Для ZIP или ссылки откройте **«Плагины» — «Установленные плагины»** и форму установки. Для пакета из серверного каталога используйте **«Плагины» — «Готовые плагины»**.

Установка по ссылке загружает архив в Backend. Адрес должен соответствовать сетевой политике загрузчика.

Ключ доступа к закрытому архиву, если нужен, вводится в предусмотренное поле формы; не включайте его в манифест.
{% endstep %}

{% step %}

#### Выполните «Проверить пакет»

Сверьте пакет, версию, runtime, совместимость, capabilities, permissions, scopes, зависимости и план установки.

Предпросмотр не запускает entrypoint, discovery, миграции или удалённый обработчик.

Подтверждение привязано к SHA-256 архива, проверенному манифесту и плану. По умолчанию оно одноразовое и действует 10 минут.

Если изменился архив, план или истёк срок, выполните проверку заново.
{% endstep %}

{% step %}

#### Подтвердите установку

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

Для источника курсов дождитесь завершения импорта пар. Для официального PHP-парсера учитывайте возможный запуск объявленного discovery после подтверждения пакета.
{% endstep %}

{% step %}

#### Подготовьте настройки

Заполните «Настройки плагина» и «Данные подключения», подготовьте зависимости.

При наличии необходимых миграций выполните «Применить обновление данных» и проверьте результат.

Подготовьте тестовый сценарий подключения, чтобы включение не запустило непроверенные платежи, события или расписания в рабочей среде.
{% endstep %}

{% step %}

#### Включите и проверьте на тестовой установке

Включение проверяет совместимость, обязательные настройки, зависимости, права, состояние capability и целостность релиза.

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

Для вызовов через общий gateway, включая `remote_http` и `declarative`, health-check требует исполняемого состояния capability и выданных прав.

Проверка такого обработчика до первого включения не подтверждает его доступность. У PHP-источника курсов отдельный путь проверки через CLI-парсер.
{% endstep %}
{% endstepper %}

### Что означает успешная проверка работы

У каждого пакета должен быть объявлен `health.check`. Для обычных capabilities он выполняется через runtime. У native-источника курсов проверка вызывает `rates()` с опцией `health_check`.

Health-check должен быть ограниченным по времени и не создавать платёж, выплату или другую необратимую операцию. Успешный статический ответ показывает лишь доступность обработчика.

Для источника курсов через общий gateway успешный `health.check` не гарантирует, что `rates.fetch` вернёт непустой корректный набор. Выполните получение котировок отдельно.

Перед выпуском источника проверьте импорт, ожидаемые пары, их ручное включение, выбор источника направлением, точность, направление расчёта и поведение при недоступности API.

### Проверка после обновления ядра

Из корня Backend выполните:

```bash
php artisan iex:plugins-check
```

Для машинного результата:

```bash
php artisan iex:plugins-check --json
```

Команда проверяет описания уже установленных плагинов и их совместимость с текущим ядром. Она не устанавливает ZIP и не заменяет runtime-проверку.

{% content-ref url="<https://docs.iexexchanger.com/guide/sait/ustanovka-plaginov>" %}
<https://docs.iexexchanger.com/guide/sait/ustanovka-plaginov>
{% endcontent-ref %}

## Каталог готовых пакетов и подпись

### Структура каталога

```
plugins/ready/
└── rates/
    ├── category.json
    └── demo-rates/
        ├── plugin.json
        └── demo-rates-1.0.0.zip
```

В папке конкретного готового пакета находятся ровно descriptor `plugin.json` и объявленный ZIP. Исходники, дополнительные подпапки и соседние файлы подписи туда не добавляются.

{% code title="category.json" %}

```json
{
  "schema": "iex.plugin-category.v1",
  "key": "rates",
  "version": "1.0.0",
  "title": "Источники курсов",
  "description": "Пакеты для подключения источников курсов.",
  "sort": 100
}
```

{% endcode %}

`key` должен совпадать с именем папки категории. Название и описание самого плагина читаются из `iex-plugin.json` внутри ZIP.

### Descriptor обычного готового пакета

Пример структуры `plugin.json`:

```json
{
  "schema": "iex.ready-plugin.v1",
  "archive": "demo-rates-1.0.0.zip",
  "artifact_sha256": "<SHA-256 окончательного ZIP: 64 символа>",
  "signature": null,
  "sbom": null,
  "provenance": null
}
```

Подставьте настоящий SHA-256. Строка в угловых скобках показывает место подстановки и не пройдёт проверку descriptor.

Пакет без подписи сохраняет `unverified` даже в готовом каталоге. Размещение файла на сервере не назначает официальный уровень доверия.

### Официальная подпись

Для официального пакета `signature`, `sbom` и `provenance` — JSON-объекты внутри `plugin.json`. При наличии подписи оба объекта подтверждающих данных обязательны.

Подпись использует Ed25519, схему `iex.plugin.detached-signature.v1` и envelope `iex.plugin.release-signature.v1`.

Envelope связывает издателя, пакет, версию, `key_id`, SHA-256 ZIP, проверенного манифеста, SBOM и provenance.

Хеш манифеста рассчитывается из канонического нормализованного представления платформы. Это не просто SHA-256 текстового файла `iex-plugin.json`. SBOM и provenance также хешируются как канонический JSON.

Подписываемое сообщение:

```
iEXExchanger plugin release signature v1\n<канонический JSON envelope>
```

Публичный ключ издателя заранее задаётся владельцем сервера в `plugin-manager.security.trusted_publishers`, по namespace и `key_id`. Закрытый ключ хранится только в процессе выпуска издателя.

Не добавляйте в манифест самоназначенный `trust: official`, собственный доверенный ключ или фиктивную подпись.

Загруженный вручную ZIP не получает официальный статус от наличия похожих файлов внутри архива. Для native-пакета используйте согласованный процесс официальной подписанной поставки.

## Обновление, откат и удаление

### Выпуск новой версии

Сохраняйте постоянную идентичность пакета, используемые alias, instance key и ключи настроек.

Увеличьте `package.version`, обновите ограничения совместимости по результатам тестирования и добавьте объявления новых actions до их использования.

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

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

Не удаляйте и не переименовывайте поля без плана перехода.

### Откат к сохранённому релизу

Действие «Откатить» переключает пакет на выбранный принадлежащий ему сохранённый релиз после проверки файлов, совместимости, идентичности и доверия. Текущий релиз нельзя выбрать как цель отката.

После переключения плагин отключён. Определения синхронизируются с восстановленной версией, затем требуется проверка и повторное включение.

{% hint style="warning" %}
Откат релиза не откатывает базу данных, внешние операции, настройки удалённого сервиса и уже выполненные миграции. Сохраняйте совместимость старого обработчика с данными, которые успела записать новая версия.
{% endhint %}

### Отключение и удаление

Отключение останавливает использование плагина и отзывает его токены. Для источника курсов отключается группа; связанные направления получают состояние ошибки источника. Установленные файлы и настройки сохраняются.

Удаление помечает пакет и capability удалёнными, отзывает токены и закрывает незавершённые запуски расписаний. По выбранной политике удаляются файлы.

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

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

## Контроль состояния и ограничения

### Очереди и health-check

По умолчанию события и расписания используют очередь `plugins`, импорт пар и проверки состояния — `default`. Одной работающей очереди с другим названием недостаточно. Названия могут быть переопределены сервером.

Периодическая проверка состояния по умолчанию запускается с интервалом 5 минут при работающем планировщике и обработчиках очередей.

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

Успешная проверка может вернуть `degraded` в рабочее состояние, но не снимает карантин автоматически.

Простое повторение «Проверить работу» не является процедурой восстановления карантинного релиза. Сначала устраните причину, затем используйте предусмотренный процесс восстановления или установки исправленного релиза.

В диагностике различайте состояние пакета, capability, последнюю health-проверку и состояние очереди. Статус «Включен» не подтверждает доставку уведомления или успешную операцию поставщика.

### Лимиты по умолчанию

| Объект                                                  | Ограничение           |
| ------------------------------------------------------- | --------------------- |
| ZIP / распакованное содержимое                          | 50 MiB / 200 MiB.     |
| Записи ZIP / иконка                                     | 1 000 / 1 MiB.        |
| Файл манифеста / структурированная часть без списка пар | 16 MiB / 2 MiB.       |
| Capabilities / actions                                  | 32 / 128.             |
| Abilities одного action                                 | 32.                   |
| Settings / config fields на capability                  | 128 / 128.            |
| Варианты одного поля                                    | 200.                  |
| Список runtime-котировок                                | 100 000.              |
| PHP-парсер: время / выходные данные                     | 20 секунд / 64 MiB.   |
| Общий runtime: соединение / запрос                      | 5 секунд / 30 секунд. |
| Payload и context вызова / ответ runtime                | 2 MiB / 8 MiB.        |
| Тело egress-запроса                                     | 1 MiB.                |
| События / расписания на capability                      | 100 / 100.            |
| Payload события                                         | 1 MiB.                |
| Глубина / число событий в причинной цепочке             | 8 / 100.              |
| Namespace / ключи Storage на capability                 | 32 / 10 000.          |
| Одно значение Storage                                   | 256 KiB.              |
| Ответ административной страницы                         | 512 KiB.              |
| Страницы / блоки одной страницы                         | 30 / 50.              |
| Navigation contributions / definitions / глубина        | 100 / 500 / 10.       |

Ограничения частоты применяются отдельно к capability и всему пакету.

По умолчанию вызовы runtime ограничены 120 в минуту и 5 000 в час на capability, 300 в минуту и 10 000 в час на пакет.

Запросы Runtime API имеют отдельные квоты: 600 в минуту и 10 000 в час на capability, 1 200 в минуту и 20 000 в час на пакет.

Часть значений изменяется серверной конфигурацией. Планируйте запас по размеру и времени; не обходите лимит дроблением одной операции на множество capabilities или фоновых заданий.

### Где искать причину ошибки

| Проблема                             | Что проверить                                                                   |
| ------------------------------------ | ------------------------------------------------------------------------------- |
| Пакет не проходит описание           | Схему v2, обязательные разделы, неизвестные поля и повторы capability.          |
| Native runtime запрещён              | Официальный подписанный канал поставки и доверенный ключ издателя.              |
| Entry или класс не найден            | Путь в ZIP, регистр символов, namespace, имя класса и нужный интерфейс.         |
| Действие не выполняется              | Объявление action, состояние capability, grants и abilities.                    |
| Ошибка чтения секрета                | Объявление setting, введённое значение, permission, scope и срок токена.        |
| Remote endpoint отклонён             | HTTPS, порт, публичный адрес, отсутствие query, fragment и redirect.            |
| Worker недоступен                    | Настройку исполнителя, машинный секрет, подпись ответа и доступность артефакта. |
| До включения не проходит health      | Требование исполняемого состояния для общего gateway.                           |
| Health успешен, курсы не обновляются | Отдельный `rates.fetch`, формат строк, импорт, очередь и включение нужных пар.  |
| Не включается пакет                  | Обязательные настройки, зависимости, конфликты alias, целостность и карантин.   |
| Не видна административная страница   | Активную `ui.admin`, собственный маршрут, navigation и права администратора.    |
| Не загружаются данные страницы       | `load_action`, его объявление и право выполнения обработчика.                   |
| Обновление данных не применилось     | Официальный trust, путь миграции, checksum и журнал её выполнения.              |
| Повтор операции отклонён             | Совпадение action, payload и context с исходным ключом идемпотентности.         |

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

***

## Промпты для разработки плагинов

### Как использовать примеры

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

Если доступа к исходникам нет, укажите это явно: проверка по статье не равна проверке текущего установщика.

Замените значения в квадратных скобках. Секреты описывайте именами полей и способом получения; реальные ключи в промпт не вставляйте.

### Новый remote\_http-плагин

{% prompt description="Создание удалённой интеграции" icon="rectangle-terminal" defaultExpanded="full" %}

```markdown
Разработай плагин iEXExchanger формата iex.plugin.v2.

Задача: [что должен делать плагин и какой результат нужен].
Целевая версия iEXExchanger: [версия].
Исходники целевого Backend: [путь или предоставленные файлы].
Внешний сервис и официальная документация API: [ссылки].
Примеры ответов без секретов: [данные].
Пакет: [namespace/name], версия [SemVer], короткий ключ [package.key].
Capability: [тип], instance_key: default, alias: [alias].
Runtime: remote_http.
Адрес обработчика: [публичный HTTPS endpoint на порту 443].
Язык и среда обработчика: [язык, версия, способ запуска].

Изучи актуальную схему, валидатор, runtime, права и адаптер-потребитель.
Проверь всю цепочку от установки до использования результата ядром.
Не придумывай capabilities, actions, поля API, команды и права.
Если контракт ядра не позволяет реализовать требуемое поведение,
покажи конкретное ограничение до реализации зависимой части.

Подготовь полный iex-plugin.json, исходники обработчика, тесты,
обезличенные fixtures и инструкцию сборки и настройки.
У пакета один runtime; один тип capability не повторяется.
Объяви health.check и все фактически используемые actions.
Запрашивай минимальные permissions и точные scopes.
Объяви необходимые abilities каждого action.
Секреты опиши в settings, без значений в ZIP и репозитории.
Не предполагай автоматическую передачу config_schema в remote runtime.

Реализуй протокол iex.plugin.invoke.v1 с capability и invocation.
Проверь токен через заранее доверенный Gateway, не пересылай его
на произвольный URL из входящих заголовков.
Реализуй таймауты, проверку входных данных и идемпотентность.
Учитывай очистку data и metadata неуспешного ответа общим шлюзом.

Проверь валидность манифеста и содержимое ZIP.
Отдельно укажи выполненные проверки и то, что требует тестовой установки.
Не устанавливай и не включай пакет на рабочем сервере.
```

{% endprompt %}

### Источник курсов

{% prompt description="Разработка источника курсов" icon="rectangle-terminal" defaultExpanded="full" %}

```markdown
Разработай источник курсов rates.source@v1 для iEXExchanger.

Поставщик: [название и официальная документация API].
Целевая версия и исходники PluginManager: [данные].
Пакет, alias и runtime: [значения].
Нужные пары и сторона котировки: [точные требования].
Примеры метаданных рынка и котировок: [обезличенные JSON/XML/CSV].
Способ авторизации: [публичный API или название секретного поля].

Проследи путь от rateParser.pairs до импорта, реестра источников,
получения курса и расчёта направления обмена.
Явно сопоставь поля поставщика с from, to, buy, необязательным sell.
Не угадывай разбиение BTCUSDT, ETHBTC и других составных тикеров.
Сохраняй десятичную точность строками, не через float.
Не подменяй недоступный курс нулём или вымышленной котировкой.
Проверь направление пары, buy/sell, пустой и частичный ответ,
дубликаты, невалидные строки, таймаут, rate limit и ошибку авторизации.

Для remote_http реализуй health.check и rates.fetch.
Для official native_php реализуй RateParserPluginInterface и
rates(RateParserContext $context): iterable.
Native PHP допустим только через проверенную официальную поставку.
Для приватного API используй discoverOnInstall: false.
Не рассчитывай на discovery до ввода секретов или на remote discovery.
Подготовь список импортируемых пар; новые пары должны быть отключены.

Верни полный манифест, исходники, fixtures, проверки и команды упаковки.
Раздельно опиши проверку ZIP, health-check и реальное получение курсов.
Укажи, как проверить источник в направлении на тестовой установке.
```

{% endprompt %}

### Платёжная интеграция

{% prompt description="Приём платежей или выплаты" icon="rectangle-terminal" defaultExpanded="full" %}

```markdown
Разработай интеграцию [приём платежей / выплаты] для iEXExchanger.

Поставщик и официальная документация: [ссылки].
Тестовая среда поставщика: [адрес и правила, без секретов].
Версия и исходники Backend: [данные].
Пакет, capability, alias и runtime: [значения].
Необходимые операции: [список].
Статусы поставщика и примеры callback: [обезличенные данные].

Сначала изучи GatewayManager, PluginPaymentGateway,
PluginPaymentRequest, PluginPaymentResponse и общий invocation gateway.
Составь подтверждённое соответствие операций, payload, полей ответа,
статусов и обращений конечного потребителя.
Учитывай action payments.<operation> и для merchant, и для payout.

Особенно проверь промежуточное состояние pending: successful: true
может считаться успехом платёжной операции, а неуспешный ответ шлюз
передаёт без data и metadata. Если нужный процесс не поддерживается
корректно, опиши необходимую доработку ядра отдельно и не скрывай её
фиктивным успешным результатом.

Реализуй точные суммы и валюты, проверку подписи callback,
устойчивую идемпотентность, повтор callback, потерю ответа,
проверку окончательного статуса, отказ и отмену.
Не считай приём HTTP-запроса подтверждением оплаты.
Секреты не передавай через merchant_config_keys и не записывай в ZIP.

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

{% endprompt %}

### Административная страница, события и расписания

{% prompt description="Интерфейс и автоматизация плагина" icon="rectangle-terminal" defaultExpanded="full" %}

```markdown
Добавь в плагин iEXExchanger [namespace/name] следующие функции:
[страница, данные, формы, действия, события и периодические задания].

Исходный пакет: [файлы].
Целевая версия и исходники PluginManager: [данные].
Текущий runtime: [значение].

Проверь совместимость всех capabilities с одним runtime пакета.
Страницу реализуй через ui.admin, config.views и adminNavigation.
Используй только поддерживаемую JSON DSL, без HTML, JS и Vue из ZIP.
Ссылки должны вести на собственный extension route.
Объяви health.check, load, form и button actions.
Раздели права просмотра схемы и выполнения обработчика.
Не запрашивай storage:write, secrets:read и http:egress у ui.admin.
Если нужны другие возможности, предложи явное разделение прав и вызовов.

Для событий используй существующие имена, versions и требуемые
профильные read-права; реализуй events.deliver и защиту от повторов.
Не обещай исторический replay и не публикуй событие от имени ядра.
Для schedules задай cron, timezone, action, payload и max_concurrency.
Учитывай работу планировщика, очереди plugins и смену релиза.

Верни согласованный манифест, обработчики, описание интерфейса,
проверки ошибок, повторов и доступа, а также инструкцию тестирования.
```

{% endprompt %}

### Обновление старого плагина

{% prompt description="Перенос и обновление пакета" icon="rectangle-terminal" defaultExpanded="full" %}

```markdown
Перенеси существующий плагин на текущий контракт iex.plugin.v2.

Старый пакет и документация: [файлы].
Целевая версия и исходники Backend: [данные].
Сохранённые настройки и используемые связи без секретов: [описание].
Требуемые изменения: [список].

Сравни старый формат с текущими схемой, DTO, установщиком и адаптерами.
Не ограничивайся переименованием полей JSON.
Проверь runtime, trust, capabilities, permissions, scopes, actions,
настройки, секреты, пары, зависимости, UI, события и расписания.
Удаляй устаревшие поля только после определения их текущего аналога.
Не переноси geoip.provider и отсутствующие команды как действующие API.

Сохрани постоянную идентичность пакета и совместимые ключи настроек.
Объясни последствия каждого изменения alias и instance_key.
Создай новую версию; не редактируй установленный immutable-релиз.
Не изменяй уже выпущенные миграции, добавляй новые при необходимости.
Учти, что обновление и откат оставляют плагин отключённым,
а откат файлов не отменяет изменения БД и внешние операции.

Подготовь новый ZIP, перечень изменений, план перехода и тесты
сохранения настроек, зависимостей и возможности отката.
Не публикуй пакет и не применяй его к рабочему серверу.
```

{% endprompt %}

### Проверка готового ZIP

{% prompt description="Независимая проверка плагина" icon="rectangle-terminal" defaultExpanded="full" %}

```markdown
Проверь готовый ZIP-плагин iEXExchanger без установки на рабочий сервер.

Архив: [путь].
Целевая версия и исходники Backend: [данные].
Документация поставщика: [ссылки].
Ожидаемый результат: [описание].

Проверь содержимое ZIP и точный iex-plugin.json по текущей схеме,
DTO и семантическим правилам установщика.
Проверь все пути, entrypoint, класс, интерфейсы и отсутствие секретов.
Сверь capabilities, единственный runtime, trust, permissions, scopes,
actions и abilities. Проверь зависимости, настройки и миграции.
Проследи фактический вызов каждого action до потребителя результата.
Проверь корректность протокола remote/worker и разделение токенов.

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

Выведи конкретные ошибки с файлами и объяснением последствий.
Отдельно перечисли выполненные проверки и непроверенные зависимости.
Успешные jq, php -l и schema validation не называй доказательством
полной работоспособности. Предложи необходимые тесты установки.
Не меняй исходники, не запускай миграции и внешние операции без запроса.
```

{% endprompt %}

## Готовность к выпуску

* [ ] Манифест соответствует текущему `iex.plugin.v2` и семантическим проверкам установщика.
* [ ] Идентичность пакета, версия, alias и instance key согласованы с потребителями.
* [ ] Все capabilities поддерживают выбранный runtime и не повторяются по типу.
* [ ] Объявлены `health.check` и все действия, которые вызывают адаптеры, страницы, события и расписания.
* [ ] Permissions, scopes и abilities достаточны и не предоставляют лишних доступов.
* [ ] Секреты отсутствуют в ZIP, исходниках, fixtures и выводе тестов.
* [ ] Настройки действительно доступны выбранному обработчику предусмотренным способом.
* [ ] Проверены маппинг ответа, точность, ошибки, таймауты и повторы.
* [ ] Для асинхронных платежей подтверждена корректная передача промежуточных состояний всей цепочкой ядра.
* [ ] Проверены зависимости и порядок обновления связанных пакетов.
* [ ] ZIP содержит все необходимые файлы и прошёл «Проверить пакет».
* [ ] На тестовой установке проверены включение, health-check и конечный результат интеграции.
* [ ] Проверены обновление, сохранение настроек, отключение и ограничения отката.
* [ ] Для официального пакета рассчитаны окончательные хеши и подготовлена проверяемая подпись.
* [ ] В поставку включена инструкция оператору: настройка, проверка, использование и восстановление после ошибки.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.iexexchanger.com/razrabotchikam/razrabotka-plaginov.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
