# Нативные плагины (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)]`-структуры с фиксированным
лейаутом.

Поэтому:

- **исходники ОС не нужны** — только заголовок ABI и контракт;
- **версия rustc не важна** (это C ABI, а не ABI Rust);
- язык может быть любым (C/C++/Zig/Go/cgo/Rust `cdylib`).

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

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

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

[dependencies]
agent_os_plugin_api = "0.1"   # заголовок ABI — и всё
```

```rust
// 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`.

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

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

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

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

```rust
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`)

```rust
// память
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)
```

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

- **Строки/буферы, которые возвращает плагин** (`content`, `data`, `visual`,
  `notice`, `reply`, `cursor`, тело ответа, массивы `inject`/`items`): плагин
  аллоцирует их через `host.str_alloc` / `host.slice_alloc` **ровно нужной
  длины**; хост освобождает сам. Статичные строковые литералы возвращать нельзя.
- **Строки, которые возвращает хост** (`value_str`, `llm`→`out`,
  `state_keys`, `run_turn`→`out`, `rows_col_name`): borrowed, валидны только в
  течение вызова — копируй, если сохраняешь.
- Пустая строка (`len == 0` / `ptr == null`) = «нет значения».

## Паники

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

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

### Загрузка DLL

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

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

### Тул

```yaml
# 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-указателе).

### Хук

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

### Синк

```yaml
# в notify: cron-задачи, монитора, воркфлоу или канала
- type: plugin
  kind: collect
  config: { ... }
```

### Источник монитора

```yaml
# config/monitors.yaml
monitors:
  - name: example
    source:
      type: plugin
      kind: counter
      config: { ... }
    template: ngu-agent
    prompt: "{{items}}"
```

### Канал

```yaml
# config/channels.yaml
channels:
  - kind: echo_channel
    config:
      template: ngu-agent
      token_env: MY_TOKEN
```

### HTTP-роут

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

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

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

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

- `AO_ABI_VERSION` — рукопожатие в `ao_plugin_init`; хост отказывает при
  несовпадении.
- Поля в `#[repr(C)]`-структурах только **добавляются в конец**; никогда не
  удаляются и не переставляются.
- Новые сервисы появляются новыми полями `AoHostApi`; старые плагины, собранные
  против старого заголовка, продолжают работать (они читают только известные им
  поля).
