Модуль на 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": "Аналитика" }));
}
}
Жизненный цикл сборки — три фазы:
register— объявить вклады вModuleBuilder(единственная обязательная фаза).wire(&mut ModuleBuilder, &WireContext)— опциональные вклады, зависящие от итогового графа (ctx.is_enabled,ctx.has_registry,ctx.has_service, …).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):
- метод:
fn name(&self, args...) -> Result<Ret, String>;— синхронный; - аргументы: примитивы (
i8..u64,f32/f64,bool),&str/String,&[u8]/Vec<u8>,#[abi_struct](по значению или&T), handle'ы других трейтов (<Trait>Handle,&<Trait>Handle,Owned<Trait>); Ret:(), примитив,String,Vec<u8>,#[abi_struct](out), handle;- JSON не используется — каждый метод = типизированная
extern-функция.
#[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 }
- linked (
cargo build, featurehost):pub struct AnalyticsModuleсimpl Module— хост включает черезserver.yaml. - lean DLL (
cargo build --no-default-features):cdylibсao_plugin_init(+ao_module_registry, если есть stable-объекты).
Хост грузит оба одинаково — через 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 Куда дальше
- 20-native-plugins — raw ABI,
dll_tool/dll_hook, загрузка DLL, версии ABI. crates/module-system/runtime/agent_os_modules/README.md— живой справочник системы модулей (граф, живой реестр, host services).- Внутренние заметки
docs/22-modules.md…docs/30-dll-ready-modules.md— дизайн: граф, lifecycle, границы ABI, план выноса модулей в DLL.