Нативные плагины и модули (Rust)

Agent OS расширяется не только YAML-конфигами и скриптами (JS/Rhai), но и нативными расширениями на Rust — скомпилированным кодом в процессе сервера. Есть две формы, и обе растут из одного и того же contribute():

Форма Что это Как подключается
linked-модуль Rust-крейт, слинкованный в бинарник (rlib) server.yaml → modules.enabled
DLL / плагин динамическая библиотека (.dll / .so / .dylib) config/plugins.yaml, kind: dll_tool, kind: dll_hook

Обе формы контрибутят одни и те же вклады в точки расширения:

Категория Что даёт Как подключается
Тулы kind: dll_tool YAML-файл тула
Хуки kind: dll_hook YAML-файл хука
Синки type: plugin в notify: cron / монитор / канал
Источники type: plugin в source: config/monitors.yaml
Каналы kind: в channels: config/channels.yaml
HTTP-роуты stable-объект Route register_registry* / contribute_route
Реестры и элементы register_registry* JSON-элементы, stable-объекты, сервисы
Config-bundle вкомпилированные файлы конфига data/builtin/<id>/

Как собрать полноценный модуль (граф зависимостей, сервисы, host-порты, упаковка, тесты) — см. 32-building-a-module.

Главная идея: стабильный C ABI + тонкий SDK

Плагин линкуется против маленького заголовка ABI agent_os_plugin_api (ноль зависимостей, #![no_std]), либо против эргономичного SDK agent_os_sdk, который делает то же самое, но транспорт-нейтрально (linked и DLL из одного кода). Язык может быть любым (C/C++/Zig/Go/cgo/Rust cdylib), но на Rust есть SDK.

Крейт Роль
agent_os_plugin_api стабильный C-ABI «заголовок»: #[repr(C)]-типы, AoHostApi, AoRegistrar, AO_ABI_VERSION
agent_os_abi стабильные интерфейсы: AStr/AoSlice, Handle, OwnedObject + макросы #[abi_trait]/#[abi_struct]/abi_registry!
agent_os_sdk тонкий SDK: RegistryHost, declare_module!, declare_plugin!, service(id), Value, Json
agent_os_web HTTP-роуты как stable-объект Route
agent_os_host_api контракты host-сервисов (agent_os.Log, .State, .Http, …)

Текущая версия ABI — v8 (AO_ABI_VERSION в agent_os_plugin_api); хост принимает любой плагин с версией в диапазоне [AO_ABI_MIN, AO_ABI_VERSION] (сейчас 4..=8). Поэтому:

Минимальный плагин

# Cargo.toml
[package]
name = "my_plugin"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[features]
# `host` = linked-транспорт; DLL собирается с `--no-default-features`.
default = ["host"]
host = ["agent_os_sdk/host"]

[dependencies]
agent_os_sdk = { path = "…/agent_os_sdk" }
agent_os_abi = { path = "…/agent_os_abi" }
// src/lib.rs
use agent_os_sdk::{declare_plugin, Registrar};

fn init(reg: &Registrar) {
    // Самоописание модуля (id + версия).
    reg.descriptor(r#"{"id":"demo","version":"0.1.0"}"#);
    // Объявить JSON-реестр и положить в него элемент.
    reg.registry("demo.items", "{}");
    reg.registry_item("demo.items", r#"{"title":"Hello"}"#);
    // Вкомпилировать файл config-bundle (материализуется в data/builtin/demo/).
    reg.bundle_file("config/demo.yaml", b"key: value\n");
}

declare_plugin!(init);

declare_plugin! генерирует единственный экспортируемый символ — ao_plugin_init(host, reg) -> u32; хост вызывает его после загрузки, а возвращаемое значение — версия ABI (несовпадение отвергается до вызова). Символ ao_module_registry (для stable-объектов, см. ниже) добавляет abi_registry!.

Два транспорта из одного contribute()

agent_os_sdk::declare_module! генерирует и DLL-точку входа, и linked-реализацию agent_os_modules::Module из одной функции:

use agent_os_sdk::{declare_module, RegistryHost};

fn contribute(h: &mut dyn RegistryHost) {
    h.data_registry("demo.items", "{}");
    h.data_item("demo.items", r#"{"title":"Hello"}"#);
}

agent_os_sdk::declare_module!(DemoModule, "demo", contribute);

Один и тот же код и module.yaml описывают модуль в обеих формах. Подробный разбор — 32-building-a-module.

Точка входа и регистрация

Плагин экспортирует:

pub extern "C-unwind" fn ao_plugin_init(host: *const AoHostApi, reg: *const AoRegistrar) -> u32

reg — это AoRegistrar, таблица функций регистрации. Ниже — её поля (raw ABI; SDK-обёртка agent_os_sdk::Registrar из примера выше покрывает лишь реестры, элементы, stable-объекты, сервисы, бандлы и дескриптор). Условно регистрации делятся на три группы:

1. «Kinds» (совместимость с v4) — поведение по имени kind:

reg.register_tool(name, schema, vtable)        // тул  (kind: dll_tool)
reg.register_hook(name, vtable)                // хук  (kind: dll_hook)
reg.register_source(name, vtable)              // источник монитора
reg.register_sink(name, vtable)                // синк
reg.register_channel(name, vtable)             // входящий канал
reg.register_route(method, path, access, vtable) // HTTP-роут

2. Config-driven engines (ABI v5) — kind: <name> в YAML резолвится в зарегистрированный vtable:

reg.register_tool_engine(name, vtable)
reg.register_hook_engine(name, vtable)
reg.register_route_engine(name, vtable)
reg.register_script_fn(name, vtable)
reg.register_slot_provider(slot_id, schema_json)
reg.register_contribution(slot_id, item_json)
reg.register_module_lifecycle(module_id, vtable)
reg.register_module_descriptor(descriptor_json)   // self-describing single-DLL
reg.register_bundle_file(rel_path, bytes)

3. Реестры (ABI v5/v7) — generic-точки расширения:

reg.register_registry(name, schema_json)          // объявить реестр
reg.register_registry_item(name, item_json)       // JSON-элемент
reg.register_registry_engine(registry, item, interface, vtable) // поведение по interface
reg.register_registry_stable(registry, item, vtable, handle)    // готовый stable-объект (v7)

Функции возвращают 0 = error::OK, иначе код ошибки. Версия ABI — AO_ABI_VERSION.

Сервисы хоста

Вместо прямого доступа к ядру плагин получает именованные host-сервисы. Транспорт-нейтральный способ — agent_os_sdk::service(id): в linked-модуле он резолвит сервис из графа, в DLL — через ABI get_service. Типизированная обёртка — agent_os_host_api::client::*:

if let Some(svc) = agent_os_sdk::service(agent_os_host_api::ids::LOG) {
    if let Some(log) = agent_os_host_api::client::log(svc.handle()) {
        let _ = log.log("info", "demo", "hello from plugin");
    }
}

Основные порты (полный список — agent_os_host_api::ids):

id Назначение
agent_os.Log / agent_os.Config / agent_os.State / agent_os.Metrics лог, (редактированный) конфиг, KV, метрики
agent_os.System host-интроспекция (version/build/templates/config)
agent_os.Agents / agent_os.Tools / agent_os.Llm / agent_os.Script процессы, инструменты, инференс, скрипты
agent_os.Http / agent_os.Secrets исходящий HTTP (SSRF-политика) и allowlisted-секреты
agent_os.Fs / agent_os.Process ФС проекта в границах корня и запуск команд под песочницей (чувствительные)
agent_os.Messages / agent_os.Events / agent_os.Sessions транскрипты, событийный лог, сессии
agent_os.Tasks + agent_os.Job фоновые задачи (sync step, рантайм — хост)
agent_os.Cron / agent_os.Workflow / agent_os.Connectors фичи-сервисы

Чувствительные порты (Secrets, Http, Messages, Sessions, control-plane и др.) fail-closed: без объявленной capability модуль их не видит. Хост ставит capability-скоуп вокруг кода модуля (обработчик роута, Job::step, колбэки событий).

Значения и JSON без serde

Чтобы читать конфиг/YAML, плагину не нужно линковать serde: хост парсит дерево сам (json_parse / yaml_parse, ABI v6), а SDK даёт обёртку Value:

let v = agent_os_sdk::Value::parse_json(r#"{"prefix":">> "}"#)?;
let prefix = v.get("prefix").and_then(|p| p.as_str()).unwrap_or_default();

Для вывода — крошечный writer agent_os_sdk::Json (объекты/массивы → текст).

Контракт по памяти

Паники

Все указатели функций — extern "C-unwind" (ABI-идентично C, но разрешает unwind). Хост оборачивает каждый вызов в catch_unwind, поэтому паника плагина не уронит процесс. Макросы SDK (declare_plugin! / declare_module!) и #[abi_trait] ставят panic-guard сами; всё же лучше ловить паники внутри своих extern-функций и возвращать код ошибки / Err(String).

Подключение к ОС

DLL: config/plugins.yaml

# config/plugins.yaml
data_dir: data/plugins        # где живут БД плагинов (db_open)
plugins:
  - plugins/echo.dll          # загрузить при старте (регистрирует все kinds)

Загрузка идемпотентна по пути: если несколько агентов (или тул/хук YAML) ссылаются на одну DLL, она загружается один раз, ao_plugin_init выполняется один раз, а kinds регистрируются один раз.

Linked-модули: server.yaml

# config/server.yaml
modules:
  enabled: [docs, widget, analytics]   # активировать на буте (+ requires транзитивно)
  auto_install: false                   # не ходить в сеть на старте

enabled ≠ installed: список лишь «что активировать на буте»; установка артефактов — отдельное действие. Always-on модули (kernel, web) включены неявно.

Тул

# config/agents/<name>/tools/echo.yaml
kind: dll_tool
path: plugins/echo.dll     # относительный путь от YAML
name: echo                 # опционально (по умолчанию — имя из плагина)
config:                    # опционально: доступно в create(config)
  prefix: ">> "
  max_len: 120

config: — произвольный YAML/JSON, который хост передаёт в create(config); плагин читает его (Value в SDK или value_* в raw ABI). config может быть null.

Хук

# config/agents/<name>/hooks/audit.yaml
kind: dll_hook
path: plugins/echo.dll
hook: audit               # имя kind, зарегистрированного плагином

Плагинный хук может реализовать любую точку жизненного цикла (все опциональны, None = no-op): before_user_message, after_turn, after_loop, before_tool_call, after_tool_result, before_inference, after_assistant_message, on_token, before_context_evict, on_session_created, on_session_closed, on_budget_threshold, before_retry, before_syscall, on_spawn, on_terminate, on_error — они зеркалят HookHandler (см. 06-hooks-reference).

Синк / источник / канал

# в notify: cron-задачи, монитора, воркфлоу или канала
- type: plugin
  kind: collect
  config: { ... }
# config/monitors.yaml
monitors:
  - name: example
    source:
      type: plugin
      kind: counter
      config: { ... }
    template: ngu-agent
    prompt: "{{items}}"
# config/channels.yaml
channels:
  - kind: echo_channel
    config:
      template: ngu-agent
      token_env: MY_TOKEN

HTTP-роут

Современный способ — stable-объект agent_os_web::Route (meta() + handle()), который хост монтирует с нужным Access (public / user / admin). Через ABI регистрируется register_registry_stable в реестр routes. Legacy-путь — register_route(method, path, access, vtable) с route_access::PUBLIC (0), TOKEN (1) или ADMIN (2).

Доверие и безопасность

Нативный плагин — это машинный код в процессе сервера, его нельзя песочничить как скрипты (нет лимита операций). Это тот же уровень доверия, что и скомпилированные инструменты. Загружайте плагины только из доверенных источников; держите plugins.yaml / module.yaml под контролем (в репозитории/деплое). Capability-гейт ограничивает доступ к чувствительным портам, но не изолирует код.

Версионирование ABI