Rust Module (linked and DLL)
A module is the unit of packaging and extension for Agent OS. It can contribute
element types, registries and elements within them, config bundles, services,
HTTP routes, background tasks, and native libraries — and be packaged either as a
linked crate (linked into the binary) or as a DLL (.dll/.so/.dylib)
loaded at runtime. The same contribute() governs both
transports.
This is a practical guide. The living source of truth for the module system is
crates/module-system/runtime/agent_os_modules/README.md; the design and rationale are in
internal notes 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 (not shown in the site menu).
32.1 Crates
| Crate | Role |
|---|---|
agent_os_modules |
composition model: Module, ModuleBuilder, graph, registries, elements, services, Access, module.yaml, ModuleLifecycle. Without axum/kernel |
agent_os_abi |
stable interfaces: AStr/AoSlice, Handle, OwnedObject, VtableHeader + macros #[abi_trait]/#[abi_struct]/abi_registry! |
agent_os_abi_macro |
implementation of these macros |
agent_os_sdk |
thin SDK: RegistryHost, declare_module!, declare_plugin!, service(id), Value, Json |
agent_os_web |
HTTP routes as a stable object Route + WebModule (owner of the routes registry) |
agent_os_host_api |
host-port contracts (agent_os.Log, .State, .Http, …) + client::* |
agent_os_plugin_api |
stable C-ABI "header" (for raw/legacy plugins) |
agent_os_modules_runtime |
driver: activation/deactivation, state.json, version-manifest, bridge DLL |
agent_os_plugin_host |
DLL loading and ABI → host type adapters |
agent_os_module_install |
artifact installation (download + sha256 + zip/tar.gz), bundle materialization |
Dependency rule for a DLL module: only ABI-safe crates. Forbidden:
agent_os_core, agent_os_tools, agent_web_bot, tokio, axum/hyper.
Use host ports instead. For details, see docs/30-dll-ready-modules.md §2.2.
32.2 The Module trait and 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": "Аналитика" }));
}
}
The build lifecycle has three phases:
register— declare contributions inModuleBuilder(the only mandatory phase).wire(&mut ModuleBuilder, &WireContext)— optional contributions that depend on the final graph (ctx.is_enabled,ctx.has_registry,ctx.has_service, …).bind(&BindContext)— retain services needed during operation (ctx.service(id)→Arc<OwnedObject>), in topological order afterwire.
ModuleBuilder:
| Method | Purpose |
|---|---|
declare_type(RegistryType) |
declare a new element type |
provide_registry(id, type_id) |
declare a registry of type type_id |
provide_data_registry(id) / provide_slot(id) |
registry of JSON data elements |
provide_stable_registry(id, interface) |
registry of stable interface objects |
contribute(id, json) |
contribute JSON data |
contribute_stable(id, OwnedObject) |
contribute a stable object |
contribute_handle(Handle) |
same, but the registry name is taken from the vtable |
provide_service(id, obj) / provide_service_shared(id, Arc<..>) |
declare a service and set the origin |
decorate_service(id, priority, obj) |
add a layer (decorator) to a service |
into_batch() |
snapshot for live Registry |
Additional trait properties: optional_dependencies() (not pulled
transitively), always_on() (always enabled and first; kernel only),
dependencies() (version requirements of the form "admin@^2.1").
32.3 ABI contracts
An interface that crosses the boundary (a registry of stable objects, a service)
is described by a trait with #[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>;
}
The grammar is restricted (otherwise a compile error):
- method:
fn name(&self, args...) -> Result<Ret, String>;— synchronous; - arguments: primitives (
i8..u64,f32/f64,bool),&str/String,&[u8]/Vec<u8>,#[abi_struct](by value or&T), handles of other traits (<Trait>Handle,&<Trait>Handle,Owned<Trait>); Ret:(), primitive,String,Vec<u8>,#[abi_struct](out), handle;- JSON is not used — each method = a typed
externfunction.
#[abi_struct] marks a #[repr(C)] struct that crosses the 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 }
The macro generates #[repr(C)] vtable with VtableHeader { trait_id, name, abi_version, size, destroy }, Copy-handle, owning-обёртку Owned<T>,
Borrowed<T>, dispatch impls, and producer erasure (into_owned()/into_object()).
The handle can be stored in OwnedObject and passed across the boundary.
A module that exports objects (for a DLL) declares their factory:
use agent_os_abi::abi_registry;
abi_registry! {
BuiltinSource as DocSource;
}
abi_registry! generates abi_objects() -> Vec<Handle> (for linked) and the symbol
ao_module_registry (for DLL).
Tools are also exposed by a module as stable objects agent_os.ToolEngine
(agent_os_tool_api) into the tool.engines registry; the host bridge turns each object
into a YAML tool factory. ToolEngine — ABI v2: kind, describe (schema), and
execute, plus execute_ctx(context_json, config, args, progress) with
a callback object agent_os.ToolProgress. The result can be returned as an "envelope"
{ content, success, data, visual, cost }. A module that uses sensitive ports
(e.g. agent_os.Fs/agent_os.Process) declares them in capabilities
(declare_module!(…, capabilities = ["agent_os.Fs"])). An example of a dual-transport
module with tools is agent_os_code (see Module code).
32.4 HTTP routes
A route is a single stable object agent_os_web::Route in the routes registry:
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; the host mounts the route with the required gate
(require_admin / require_user), and an unknown level is not mounted
(fail closed). For typical cases there are ready-made constructors:
contribute_get, contribute_route, contribute_asset (static),
respond_cached/weak_etag (ETag), mime_for. The registry owner is the always-on
module agent_os_web::WebModule; contributions can be made before it (elements
are deferred).
32.5 Services and host ports
A domain service (module ↔ module) is a registry of the form Service with exactly one
origin and N layers:
// 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 ports (kernel → module) are available transport-neutrally via
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");
}
}
Main ports: Log, Config, State, Metrics, System, Agents,
Tools, Llm, Script, Http, Secrets, Fs, Process, Messages,
Events, Sessions, Tasks (+ Job), RequestContext, Extension. The full
list is in agent_os_host_api::ids.
Background tasks do not run their own runtime: the module exposes a synchronous Job::step
(JSON { "continue": bool, "next_ms": u64 }), and the host executes the steps on its own
runtime under the module's capability scope:
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. Sensitive ports (Secrets, Http, Messages,
Sessions, control-plane, etc.) are fail-closed: a module sees them only if it
declared the id in capabilities() / module.yaml → capabilities. The host sets
capability scope around each entry point into the module's code (route handler,
Job::step, event callback).
32.6 Descriptor module.yaml
Unified for linked and DLL modules. A Rust module derives it from Module; a DLL can
read the file or send 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] # в чужие слоты
Enabling — in config/server.yaml:
modules:
enabled: [auth, admin, docs, distrib, analytics]
auto_install: false
enabled ≠ installed: the list is "what to activate on boot"; installing artifacts
(network) is a separate action. requires are pulled in transitively; an unknown id
— warning; a module with a missing bundle/DLL or ABI mismatch — module load
error, the rest of the system starts.
32.7 Two transports and packaging
agent_os_sdk::declare_module! generates both transports from a single function:
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 AnalyticsModulewithimpl Module— the host includes it viaserver.yaml. - lean DLL (
cargo build --no-default-features):cdylibwithao_plugin_init(+ao_module_registry, if there are stable objects).
The host loads both the same way — via 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()?;
Bundle materialization is edit-preserving, in data/builtin/<id>/ with
a 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(()) }
}
All methods are no-ops by default. The runtime activates modules in topological
order and deactivates them in reverse; state — data/modules/<id>/state.json
(version, status, artifacts). An activation failure marks the module failed, but
the system starts. The DLL is not unloaded; deactivate — graceful stop.
32.9 Full example
The canonical dual-transport module is this reference itself (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;
}
The minimal cdylib route engine — crates/modules/fixtures/agent_os_route_demo
(pure impl without unsafe/thunks). More examples: agent_os_widget
(dual-transport surface), agent_os_test_plugin (raw C ABI, all categories),
agent_os_test_host_client (consuming host services from a cdylib).
32.10 Build and tests
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 libraries are staged into a separate directory target/modules/<profile>/ —
otherwise the linked build (including cargo build --workspace) will overwrite the real DLL
with an empty stub without exports. Tests that load a DLL
(agent_os_plugin_host, agent_os_modules_runtime, agent_os_route_abi)
obtain the artifact via agent_os_test_support::require_dll and fail (do not
skip) if it is missing.
Transport parity is enforced by
agent_os_modules_runtime/tests/transport_parity.rs: linked and DLL must produce
the same registry/route catalog.
32.11 Where to go next
- 20-native-plugins — raw ABI,
dll_tool/dll_hook, DLL loading, ABI versions. crates/module-system/runtime/agent_os_modules/README.md— a living reference for the module system (graph, live registry, host services).- Internal notes
docs/22-modules.md…docs/30-dll-ready-modules.md— design: graph, lifecycle, ABI boundaries, plan for moving modules into DLLs.