Нативные плагины и модули (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). Поэтому:
- исходники ОС не нужны — только заголовок/SDK и контракт;
- версия rustc не важна для C-ABI — но, поскольку
agent_os_abiгенерирует Rust-native vtable'ы, хост и модуль собираются совместимыми тулчейнами на одну платформу (это то же «per-OS», что и обычная DLL); - язык может быть любым (Rust
cdylib— первый класс).
Минимальный плагин
# 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);
- linked-сборка (
feature = "host", по умолчанию) даётpub struct DemoModuleсimpl Module— хост включает модуль черезserver.yaml → modules.enabled. - lean-сборка (
--no-default-features) даётcdylibсao_plugin_init.
Один и тот же код и 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 (объекты/массивы → текст).
Контракт по памяти
- Строки/буферы, которые возвращает плагин (тело ответа,
content,data,visual,notice,reply,cursor, массивыinject/items): плагин аллоцирует их черезagent_os_abi::{alloc_str, alloc_slice}(илиhost.str_alloc/host.slice_allocв raw ABI) ровно нужной длины; хост освобождает сам. Статичные строковые литералы возвращать нельзя. - Строки, которые возвращает хост (
value_str,llm→out,rows_col_name,run_turn→out): borrowed, валидны только в течение вызова — копируй, если сохраняешь. - Пустая строка (
len == 0/ptr == null) = «нет значения».
Паники
Все указатели функций — 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
AO_ABI_VERSION— рукопожатие вao_plugin_init; хост принимает диапазон[AO_ABI_MIN, AO_ABI_VERSION](сейчас4..=8) и отвергает несовпадение.- ABI v2: в
AoHookVtableдобавлены колбэки перехвата. - ABI v3: добавлен output-guardrail
after_assistant_message. - ABI v4: паритет остальных точек (
on_token,before_context_evict, session lifecycle,on_budget_threshold,before_retry,before_syscall). - ABI v5: модульные расширения — config-driven engines
(
register_tool_engine/register_hook_engine/register_route_engine),register_script_fn, слоты, module lifecycle,host.download, genericregister_registry*. - ABI v6:
json_parse/yaml_parse/value_free— структурированные данные безserde. - ABI v7:
register_registry_stable— передать хосту raw(vtable, handle)stable-объекта (trait id и destroy — в заголовке vtable). - ABI v8 (текущая):
get_service— получить именованный host-сервис. - Поля в
#[repr(C)]-структурах только добавляются в конец; никогда не удаляются и не переставляются. Поэтому старые плагины продолжают работать: они читают только известные им поля.