Модуль на Rust (linked и DLL)

Модуль — единица упаковки и расширения Agent OS. Он может контрибьютить типы элементов, реестры и элементы в них, конфиг-бандлы, сервисы, HTTP-роуты, фоновые задачи и нативные библиотеки — и упаковываться либо как linked-крейт (слинкован в бинарник), либо как DLL (.dll/.so/.dylib), загружаемую в рантайме. Один и тот же contribute() управляет обоими транспортами.

Это практическое руководство. Живой источник истины по системе модулей — crates/module-system/runtime/agent_os_modules/README.md; дизайн и обоснования — внутренние заметки docs/22-modules.md, docs/25-module-abi.md, docs/26-feature-modules.md, docs/27-core-modularization.md, docs/30-dll-ready-modules.md (в меню сайта не выводятся).

32.1 Крейты

Крейт Роль
agent_os_modules модель композиции: Module, ModuleBuilder, граф, реестры, элементы, сервисы, Access, module.yaml, ModuleLifecycle. Без axum/kernel
agent_os_abi стабильные интерфейсы: AStr/AoSlice, Handle, OwnedObject, VtableHeader + макросы #[abi_trait]/#[abi_struct]/abi_registry!
agent_os_abi_macro реализация этих макросов
agent_os_sdk тонкий SDK: RegistryHost, declare_module!, declare_plugin!, service(id), Value, Json
agent_os_web HTTP-роуты как stable-объект Route + WebModule (владелец реестра routes)
agent_os_host_api контракты host-портов (agent_os.Log, .State, .Http, …) + client::*
agent_os_plugin_api стабильный C-ABI «заголовок» (для raw/legacy-плагинов)
agent_os_modules_runtime драйвер: активация/деактивация, state.json, version-manifest, bridge DLL
agent_os_plugin_host загрузка DLL и адаптеры ABI → типы хоста
agent_os_module_install установка артефактов (download + sha256 + zip/tar.gz), материализация бандла

Правило зависимостей DLL-модуля: только ABI-safe крейты. Запрещены agent_os_core, agent_os_tools, agent_web_bot, tokio, axum/hyper. Вместо них — host-порты. Подробно — docs/30-dll-ready-modules.md §2.2.

32.2 Трейт Module и ModuleBuilder

use agent_os_modules::{Module, ModuleBuilder, RegistryType};
use serde_json::json;

struct Analytics;

impl Module for Analytics {
    fn id(&self) -> &str { "analytics" }
    fn version(&self) -> &str { "1.2.0" }          // semver, по умолчанию "0.0.0"
    fn requires(&self) -> Vec<String> { vec!["admin".into()] }
    fn capabilities(&self) -> Vec<String> { vec!["agent_os.Secrets".into()] }

    fn register(&self, b: &mut ModuleBuilder) {
        b.declare_type(RegistryType::data("analytics.panel"));
        b.provide_registry("admin.panels", "analytics.panel");
        b.contribute("admin.panels", json!({ "title": "Аналитика" }));
    }
}

Жизненный цикл сборки — три фазы:

  1. register — объявить вклады в ModuleBuilder (единственная обязательная фаза).
  2. wire(&mut ModuleBuilder, &WireContext) — опциональные вклады, зависящие от итогового графа (ctx.is_enabled, ctx.has_registry, ctx.has_service, …).
  3. bind(&BindContext) — удержать сервисы, которые нужны во время работы (ctx.service(id) → Arc<OwnedObject>), в топологическом порядке после wire.

ModuleBuilder:

Метод Назначение
declare_type(RegistryType) объявить новый тип элемента
provide_registry(id, type_id) объявить реестр типа type_id
provide_data_registry(id) / provide_slot(id) реестр JSON-data-элементов
provide_stable_registry(id, interface) реестр stable-объектов интерфейса
contribute(id, json) контрибутить JSON-данные
contribute_stable(id, OwnedObject) контрибутить stable-объект
contribute_handle(Handle) то же, но имя реестра берётся из vtable
provide_service(id, obj) / provide_service_shared(id, Arc<..>) объявить сервис и положить origin
decorate_service(id, priority, obj) добавить слой (декоратор) к сервису
into_batch() снимок для живого Registry

Дополнительные свойства трейта: optional_dependencies() (не тянутся транзитивно), always_on() (всегда включён и первый; только для ядра), dependencies() (версионные требования вида "admin@^2.1").

32.3 Контракты ABI

Интерфейс, который пересекает границу (реестр stable-объектов, сервис), описывается трейтом с #[abi_trait]:

use agent_os_abi::abi_trait;

#[abi_trait(id = "DocSource", name = "documentation", version = 1)]
pub trait DocSource {
    fn manifest(&self) -> Result<String, String>;
    fn markdown(&self, id: &str, lang: &str) -> Result<String, String>;
}

Грамматика ограничена (иначе compile error):

#[abi_struct] помечает #[repr(C)]-структуру, пересекающую ABI:

use agent_os_abi::abi_struct;

#[abi_struct]
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ItemRow { pub id: i64, pub score: f64, pub name: agent_os_abi::AStr }

Макрос генерирует #[repr(C)] vtable с VtableHeader { trait_id, name, abi_version, size, destroy }, Copy-handle, owning-обёртку Owned<T>, Borrowed<T>, dispatch-impl'ы и producer-стирание (into_owned()/into_object()). Хэндл можно хранить в OwnedObject и передавать через границу.

Модуль, экспортирующий объекты (для DLL), объявляет их фабрику:

use agent_os_abi::abi_registry;
abi_registry! {
    BuiltinSource as DocSource;
}

abi_registry! генерирует abi_objects() -> Vec<Handle> (для linked) и символ ao_module_registry (для DLL).

Инструменты модуль тоже отдаёт stable-объектами agent_os.ToolEngine (agent_os_tool_api) в реестр tool.engines; хост-мост превращает каждый объект в фабрику YAML-инструмента. ToolEngine — ABI v2: kind, describe (схема) и execute, плюс execute_ctx(context_json, config, args, progress) с callback-объектом agent_os.ToolProgress. Результат можно вернуть «конвертом» { content, success, data, visual, cost }. Модуль, использующий sensitive-порты (например agent_os.Fs/agent_os.Process), объявляет их в capabilities (declare_module!(…, capabilities = ["agent_os.Fs"])). Пример dual-transport модуля с инструментами — agent_os_code (см. Модуль code).

32.4 HTTP-роуты

Роут — один stable-объект agent_os_web::Route в реестре routes:

use agent_os_web::{Route, RouteMeta, HttpRequest, HttpResponse, ACCESS_ADMIN};

struct ListAgents;

impl Route for ListAgents {
    fn meta(&self) -> Result<RouteMeta, String> {
        Ok(RouteMeta::new("GET", "/v1/analytics/agents", ACCESS_ADMIN))
    }
    fn handle(&self, _req: HttpRequest) -> Result<HttpResponse, String> {
        Ok(HttpResponse::json(200, &serde_json::json!({ "agents": [] })))
    }
}

Access — public / user / admin; хост монтирует роут с нужным gate (require_admin / require_user), а неизвестный уровень не монтируется (fail closed). Для типовых случаев есть готовые конструкторы: contribute_get, contribute_route, contribute_asset (статика), respond_cached/weak_etag (ETag), mime_for. Владелец реестра — always-on модуль agent_os_web::WebModule; контрибутить можно раньше него (элементы откладываются).

32.5 Сервисы и host-порты

Доменный сервис (модуль ↔ модуль) — это реестр вида Service с ровно одним origin и N слоями:

// provider (origin)
b.provide_service("dbsvc", MyDb::new().into_owned().into_object());
// декоратор (слой), priority ниже — ближе к origin
b.decorate_service("dbsvc", 0, MetricsDb::new().into_owned().into_object());

// потребитель: requires: ["db-provider"], затем
fn bind(&self, ctx: &BindContext) {
    if let Some(obj) = ctx.service("dbsvc") {
        self.db.set(SharedDbService::from_object(obj).expect("interface mismatch"));
    }
}

Host-порты (ядро → модуль) доступны транспорт-нейтрально через agent_os_sdk::service(id) + agent_os_host_api::client::*:

if let Some(svc) = agent_os_sdk::service(agent_os_host_api::ids::STATE) {
    if let Some(state) = agent_os_host_api::client::state(svc.handle()) {
        let _ = state.set("analytics", "last_run", "2026-01-01");
    }
}

Основные порты: Log, Config, State, Metrics, System, Agents, Tools, Llm, Script, Http, Secrets, Fs, Process, Messages, Events, Sessions, Tasks (+ Job), RequestContext, Extension. Полный список — agent_os_host_api::ids.

Фоновые задачи не крутят свой рантайм: модуль отдаёт синхронный Job::step (JSON { "continue": bool, "next_ms": u64 }), а хост исполняет шаги на своём рантайме под capability-скоупом модуля:

let tasks = agent_os_host_api::client::tasks(svc.handle()).unwrap();
let handle = tasks.spawn("analytics", my_job.into_owned())?;  // my_job: impl Job
// …
tasks.cancel(&handle)?;

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

32.6 Дескриптор module.yaml

Единый для linked и DLL модулей. Rust-модуль выводит его из Module; DLL может читать файл или прислать register_module_descriptor.

id: analytics
version: 1.2.0
abi: 8                              # требуемая версия ABI для libs
requires: [auth, "admin@^2.1"]      # граф: топосорт + авто-включение
optional: ["gpu@^1"]                # только если цель уже включена
capabilities: [agent_os.Secrets]    # чувствительные порты (fail-closed)
libs:
  linux:   lib/libanalytics.so
  windows: lib/analytics.dll
  macos:   lib/libanalytics.dylib
bundle:
  root: config
  points: [agents, hooks, tools, routes, workflows]
install:
  verify: sha256
  code: true
  artifacts:
    - id: runtime
      dest: runtime
      platforms:
        linux:   { url: "https://…/rt.tgz", sha256: "…", extract: tar.gz }
        windows: { url: "https://…/rt.zip", sha256: "…", extract: zip }
provides: [analytics.panel]         # свои слоты/сервисы
contributes: [admin.nav]            # в чужие слоты

Включение — в config/server.yaml:

modules:
  enabled: [auth, admin, docs, distrib, analytics]
  auto_install: false

enabled ≠ installed: список — «что активировать на буте»; установка артефактов (сеть) — отдельное действие. requires дотягиваются транзитивно; неизвестный id — warning; модуль с отсутствующим бандлом/DLL или ABI-mismatch — ошибка загрузки модуля, остальная система стартует.

32.7 Два транспорта и упаковка

agent_os_sdk::declare_module! генерирует оба транспорта из одной функции:

fn contribute(h: &mut dyn RegistryHost) {
    h.data_registry("analytics.items", "{}");
    h.data_item("analytics.items", r#"{"title":"x"}"#);
}
agent_os_sdk::declare_module!(AnalyticsModule, "analytics", contribute);

Cargo:

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

[features]
default = ["host"]
host = ["agent_os_sdk/host", "dep:agent_os_modules", "dep:serde", "dep:serde_json"]

[dependencies]
agent_os_sdk = { path = "…/agent_os_sdk" }
agent_os_abi = { path = "…/agent_os_abi" }
agent_os_modules = { path = "…/agent_os_modules", optional = true }
serde_json = { version = "1", optional = true }

Хост грузит оба одинаково — через ModulePackage:

// internal Rust-модуль
let pkg = agent_os_modules::ModulePackage::internal(AnalyticsModule)
    .with_bundle(files);

// DLL: package собирается из register_module_descriptor + register_bundle_file + регистраций
let pkg = agent_os_modules_runtime::plugin_package("analytics").unwrap();

let rt = ModuleRuntime::build_packages_with_host(
    packages, &enabled, "data/modules", host_services,
)?;
rt.activate()?;

Материализация бандла — edit-preserving, в data/builtin/<id>/ с version-manifest (.manifest.json).

32.8 Lifecycle

use agent_os_modules::{ModuleLifecycle, InstallContext, InstallReport,
                       UninstallContext, RuntimeContext};

impl ModuleLifecycle for AnalyticsLifecycle {
    fn install(&self, ctx: &mut InstallContext) -> Result<InstallReport, String> {
        // скачать/материализовать артефакты в ctx.install_dir
        Ok(InstallReport::default())
    }
    fn activate(&self, _rt: &mut RuntimeContext) -> Result<(), String> { Ok(()) }
    fn deactivate(&self, _rt: &mut RuntimeContext) -> Result<(), String> { Ok(()) }
    fn uninstall(&self, _ctx: &mut UninstallContext) -> Result<(), String> { Ok(()) }
}

Все методы — no-op по умолчанию. Рантайм активирует модули в топологическом порядке и деактивирует в обратном; состояние — data/modules/<id>/state.json (version, status, артефакты). Провал активации помечает модуль failed, но система стартует. DLL не выгружается; deactivate — graceful stop.

32.9 Полный пример

Канонический dual-transport модуль — сам этот справочник (agent_os_docs):

// crates/modules/surfaces/agent_os_docs/src/interface.rs
#[abi_trait(id = "DocSource", name = "documentation", version = 1)]
pub trait DocSource {
    fn manifest(&self) -> Result<String, String>;
    fn markdown(&self, id: &str, lang: &str) -> Result<String, String>;
}

// crates/modules/surfaces/agent_os_docs/src/lib.rs
fn contribute(h: &mut dyn RegistryHost) {
    h.stable_registry(DOCS_REGISTRY, DOC_SOURCE_ID);
}
agent_os_sdk::declare_module!(DocsModule, "docs", contribute, abi_objects);

abi_registry! {
    BuiltinSource as DocSource;
}

Минимальный cdylib-роут-движок — crates/modules/fixtures/agent_os_route_demo (чистый impl без unsafe/thunk'ов). Ещё примеры: agent_os_widget (dual-transport поверхность), agent_os_test_plugin (raw C ABI, все категории), agent_os_test_host_client (потребление host-сервисов из cdylib).

32.10 Сборка и тесты

cargo build -p <module>                      # linked (feature host, по умолчанию)
cargo build -p <module> --no-default-features # lean DLL

./scripts/build-module-dlls.sh               # Windows: scripts\build-module-dlls.ps1
./scripts/test-module-system.sh              # stage DLLs + все module-system тесты

Lean-библиотеки стейджатся в отдельный каталог target/modules/<profile>/ — иначе linked-сборка (в т.ч. cargo build --workspace) затрёт настоящую DLL пустой заглушкой без экспортов. Тесты, грузящие DLL (agent_os_plugin_host, agent_os_modules_runtime, agent_os_route_abi), берут артефакт через agent_os_test_support::require_dll и падают (не skip'ают), если его нет.

Паритет транспортов фиксирует agent_os_modules_runtime/tests/transport_parity.rs: linked и DLL должны давать одинаковый каталог реестров/роутов.

32.11 Куда дальше