🤖 AI-агенты для бизнеса

Нативные плагины (Rust DLL)

Agent OS расширяется не только YAML-конфигами и скриптами (JS/Rhai), но и нативными плагинами — динамическими библиотеками (.dll / .so / .dylib), написанными на Rust (или C/C++/Zig/Go) и подгружаемыми в рантайме.

Плагин может контрибутить «kinds» для всех точек расширения:

Категория Что плагин предоставляет Как подключается
Тулы kind: dll_tool YAML-файл тула
Хуки kind: dll_hook YAML-файл хука
Синки type: plugin в notify: cron / монитор / канал
Источники type: plugin в source: config/monitors.yaml
Каналы kind: в channels: config/channels.yaml
HTTP-роуты register_route регистрируются при загрузке

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

Плагин линкуется только против маленькой крейты-заголовка agent_os_plugin_api (ноль зависимостей, #![no_std]). Всё остальное — сервисы ядра, аксессоры значений, DuckDB — он получает в рантайме как таблицы указателей на функции (AoHostApi) и #[repr(C)]-структуры с фиксированным лейаутом.

Поэтому:

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

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

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

[dependencies]
agent_os_plugin_api = "0.1"   # заголовок ABI — и всё
// src/lib.rs
use agent_os_plugin_api::*;
use std::ffi::c_void;

// тул `echo`
unsafe extern "C-unwind" fn echo_create(_cfg: *const AoValue, _host: *const AoHostApi,
    out: *mut *mut c_void, _err: *mut AoError) -> i32 {
    *out = 1usize as *mut c_void;
    error::OK
}

unsafe extern "C-unwind" fn echo_execute(_inst: *mut c_void, args: *const AoValue,
    host: *const AoHostApi, out: *mut AoToolResult) -> i32 {
    let host = &*host;
    let text = (host.value_get)(host, args, AoStr::from_str("text"));
    let text = (host.value_str)(host, text).as_str();

    let reply = format!("echo: {text}");
    let buf = (host.str_alloc)(host, reply.len());
    core::ptr::copy_nonoverlapping(reply.as_ptr(), buf, reply.len());

    *out = AoToolResult {
        success: true,
        content: AoStr { ptr: buf, len: reply.len() },
        monetary_micros: 0, token_surcharge: 0, duration_ms: 0,
        data: AoStr::null(), visual: AoStr::null(),
    };
    error::OK
}
unsafe extern "C-unwind" fn echo_destroy(_inst: *mut c_void) {}

static ECHO: AoToolVtable = AoToolVtable { create: echo_create, execute: echo_execute, destroy: echo_destroy };

#[no_mangle]
pub extern "C-unwind" fn ao_plugin_init(_host: *const AoHostApi, reg: *const AoRegistrar) -> u32 {
    unsafe {
        let reg = &*reg;
        (reg.register_tool)(reg, AoStr::from_str("echo"),
            AoToolSchema {
                name: AoStr::from_str("echo"),
                description: AoStr::from_str("Echo the text back."),
                parameters_json: AoStr::from_str(r#"{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]}"#),
                ui_json: AoStr::null(),
            },
            &ECHO);
    }
    AO_ABI_VERSION
}

Полный рабочий пример — crate agent_os_test_plugin в репозитории (все шесть категорий). Сборка: cargo build -p agent_os_test_plugin.

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

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

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

reg (AoRegistrar) — таблица функций регистрации kinds:

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

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

Сервисы ядра (AoHostApi)

// память
host.str_alloc(len) -> *mut u8      // аллоцировать возвращаемую строку
host.str_free(s)                    // (хост сам освобождает возвращённые строки)
host.slice_alloc(len) / host.slice_free   // буферы (массивы, body)

// значения (только чтение, borrowed)
host.value_kind(v) / value_get(v, key) / value_index(v, i)
host.value_str(v) / value_bool(v) / value_int(v) / value_float(v) / value_len(v)

// ядро
host.llm(req, out)                  // один инференс, бюджет — на вызывающий процесс
host.state_get(scope, key, out)     // scope = "user" | "session" | "agent"
host.state_set(scope, key, json)    // значение — JSON-текст
host.state_delete(scope, key, out) / host.state_keys(scope, out)

// DuckDB (плагино-собственная БД)
host.db_open(name, out)             // data/plugins/<name>.db
host.db_exec(db, sql, err)          // DDL/DML
host.db_query(db, sql, out, err)    // SELECT → rows_*
host.db_close(db)
host.rows_count/cols/col_name/value

// рантайм
host.run_turn(template, session, user, text, out)  // полный turn через ядро
host.should_stop(inst)              // каналу пора останавливаться
host.sleep_ms(ms) / host.now(out)
host.log(level, msg)

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

Паники

Все указатели функций — extern "C-unwind" (ABI-идентично C, но разрешает unwind). Хост оборачивает каждый вызов в catch_unwind, поэтому паника плагина не уронит процесс. Всё же лучше ловить паники внутри своих extern-функций и возвращать код ошибки.

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

Загрузка DLL

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

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

Тул

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

config: — произвольный YAML/JSON, который хост передаёт в create(config) как AoValue; плагин читает его аксессорами (value_get / value_str / …). config может быть null — проверяй config на null (или читай аксессорами: они возвращают null/пусто на null-указателе).

Хук

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

Синк

# в 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-роут

Регистрируется плагином при загрузке; accessroute_access::PUBLIC (0), TOKEN (1, bearer-токен inbound.token_env) или ADMIN (2).

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

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

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