Skip to content

Add trusted in-process dynamic modules - #5

Merged
senamakel merged 220 commits into
mainfrom
feature/modules-dlopen
Aug 10, 2026
Merged

senamakel merged 220 commits into
mainfrom
feature/modules-dlopen

Conversation

@senamakel

@senamakel senamakel commented Aug 10, 2026 •

Copy link
Copy Markdown
Member

Summary

  • add the versioned, borrow-only module ABI, manifest schema, module SDK, per-module Tokio runtime, panic conversion, and raw Unix/Windows loaders
  • add host admission, dependency resolution, eager and lazy lifecycle handling, terminal-state routing, module control bus members, CLI commands, state signals, and documentation
  • add typed JSON initialization configuration through ModuleHost, LoadModule, the CLI, and module_export! { config = Type }

Security boundary

The modules feature remains off by default. Loaded modules are explicitly treated as trusted code inside the host address space. Admission checks cover directory ownership/modes or Windows ACLs, parent components on Unix, O_NOFOLLOW, regular files, platform extensions, a 512 MiB cap, and an authoritative SHA-256 modules.toml allowlist when present. The ABI hard gate runs before the manifest function or init entrypoint.

Libraries are opened locally/eagerly and are never unloaded. The security-boundary amendment and operational constraints are documented in this change.

Validation

  • cargo fmt --all -- --check
  • cargo clippy --locked --all-targets --all-features -- -D warnings
  • cargo test --locked --all-features
  • cargo check --locked --no-default-features
  • cargo check --locked --no-default-features --features modules
  • Windows cross-check: cargo check --locked --target x86_64-pc-windows-gnu --no-default-features --features modules,macros
  • real loader suite with three cdylibs: normal configured calls and panic detach, handle-local duplicate symbols, wrong-target refusal, and refusal isolation

Closes #4

Summary by CodeRabbit

  • New Features

    • Added optional trusted in-process module support with cross-platform dynamic loading.
    • Added module discovery, inspection, loading, stopping, enabling, disabling, rescanning, and diagnostics through the CLI and connection APIs.
    • Added module manifests, dependency ordering, lifecycle states, configuration handling, and compatibility checks.
    • Added example clock modules and clear reporting for unavailable or refused modules.
  • Documentation

    • Documented module installation, ABI requirements, lifecycle behavior, security boundaries, and protocol operations.
    • Clarified that in-process modules share the host’s privileges and crash boundary.

senamakel and others added 30 commits August 10, 2026 14:33
Add the `tinybus-module` crate as a workspace member and introduce the `modules` feature flag for loading in-process cdylib modules. This change lays the groundwork for trusted, in-process module support while keeping the module-side C ABI versioned in lockstep with the host to prevent definition drift.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
This change introduces the tinybus-module crate with its Cargo.toml and lib.rs files, providing the foundational structure for the module system.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Changed the tinybus dependency in tinybus-module from a workspace reference with default-features disabled to an explicit path and version specification. This ensures the dependency resolves correctly when the crate is used outside the workspace context.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a new example demonstrating how to use the tinybus library with a clock module, providing users with a practical reference for implementing time-based functionality in their applications.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The lock file is updated to include the new `tinybus-module` crate, which is now a dependency of the main `tinybus` package. This addition enables the module system functionality within the project.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
…ailures

Introduce a new error variant to represent when a dynamic module is rejected by a fixed admission rule, along with a constructor that sanitizes the artifact path to a safe basename. This allows the host to report refusal reasons without leaking untrusted path or descriptor bytes.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Added a new `sanitize_untrusted` function that filters untrusted input to only allow ASCII alphanumeric characters and a small set of safe punctuation, while capping the result at 32 characters. This provides a reusable utility for producing log-safe strings from potentially malicious or malformed data.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add the transport.rs file to the module directory, which was previously untracked and missing from the crate. This resolves a build error caused by the absent source file.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
When a module file is present in the filesystem but not tracked by the module registry, the loader now treats it as a valid untracked module rather than failing. This allows external tools to drop module files into the monitored directory without requiring prior registration.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The module host file was previously untracked, causing it to be ignored during builds and runtime operations. This change adds the file to version control and ensures it is properly included in the module system.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
…t style

Reformatted multi-line conditional expressions and error function calls across several modules to use a consistent brace style, improving readability without changing any behavior.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a test module covering module validation scenarios, including rejection of modules compiled with panic=abort, handling of mismatched rustc versions in permissive versus strict mode, and an integration test for loading a real cdylib and making a remote procedure call. These tests improve coverage of the module host's validation logic and ensure that the host correctly enforces safety constraints.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Moved the `Connection` import to maintain alphabetical ordering among the crate imports in the test module, keeping the codebase consistent with the project's import style conventions.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The test for connecting to the clock proxy was failing intermittently because it tried to create the proxy before the clock service had registered its name. Added a polling loop with a two-second timeout that waits until the clock name appears in the list of available names before proceeding.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Introduce a `ModuleControl` trait that exposes the host's module management operations through a weak pointer, allowing the broker to list, load, stop, enable, and rescan modules without holding a strong reference to the host. Add an `enabled` field to `ModuleInfo` so that modules can be marked as disabled during discovery, and track registered directories to support future rescanning.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The strict flag in ModuleHost is changed from a plain bool to an AtomicBool so it can be read without a full lock, and the strict setter no longer requires mutable access to the inner Arc. A new stop_sync method is extracted from the async close implementation to allow synchronous callers to shut down the transport without needing an async context.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds a full implementation of the ModuleControl trait on ModuleHostInner, providing methods to list, load, stop, enable, and rescan modules. This enables external control of module lifecycle through a consistent interface, replacing ad-hoc access to the inner state.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a `modules` field to the Broker that holds a weak reference to a ModuleControl, along with a setter and a dispatch method that handles module-related RPC calls. This allows the broker to forward requests such as ListModules, LoadModule, and StopModule to the module host when the "modules" feature is enabled, without affecting the core broker logic when the feature is absent.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The `host` module was previously private, which prevented other modules within the crate from accessing it when the `modules` feature is enabled. Changing its visibility to `pub(crate)` allows internal crate access while keeping it hidden from external consumers.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Reformat chained method calls in the broker and module host to use consistent indentation, placing each method call on its own line. This improves readability and aligns with the project's coding style without changing any behavior.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
…modules

Add a new `modules` subcommand to the tinybus CLI that provides operations for managing trusted in-process modules, including listing, showing details, scanning, loading, stopping, enabling, disabling, and running diagnostics. This enables operators to inspect and control the module lifecycle at runtime.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The combined match arm for Enable and Disable commands was split into two distinct arms to improve code clarity and remove the conditional logic that determined the enabled state. Each command now directly passes the appropriate boolean value to the EnableModule call.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
…n signature

Reordered the printed fields in the module listing to show the manifest after the rustc version, and collapsed the `run_modules` function signature onto a single line for improved readability.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
…send

The host_send callback is now wrapped in catch_unwind to prevent panics from crossing the FFI boundary, returning TB_BACKPRESSURE on unwind. The inner send path was changed from try_send to blocking_send so that backpressure is applied only to the calling module, ensuring reliable completion without adding a new callback to the frozen v1 ABI.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a custom panic hook to the module runtime that captures the panic location, logs it via the host's log interface, and then triggers a host fault. This ensures that when a module panics, the host receives a structured diagnostic message containing the file, line, and column of the panic, while deliberately omitting the panic payload to avoid leaking sensitive data.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The module_export macro now accepts optional provides, requires, optional, and lazy parameters, allowing modules to declare their interface dependencies and capabilities at compile time. A static OnceLock caches the serialized manifest bytes for the process lifetime, replacing the previous hardcoded empty manifest.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Collapse the unsafe block in the `log` method into a single line to improve readability without changing any behaviour.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add the trusted in-process module ABI to the architecture documentation, update the roadmap to reflect that module loading is now the M3 milestone, and extend the wire protocol with module management methods. The module system lets the broker load selected integrations as cdylib libraries behind a non-default feature flag, with a versioned C-shaped ABI that rejects incompatible artifacts before initialization. The protocol gains Announce, GetManifest, ListPeers, and module control methods while keeping the wire format at version 1 since old peers can still parse every message.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a new CI job that builds and tests the module loader on all three platforms, and extend the existing portability job to test modules without the socket transport. This ensures the in-process module system, which is portable across Unix and Windows, is exercised in CI alongside the existing Unix-only socket adapter.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The LoadModule RPC now accepts an optional second argument containing a JSON configuration object that is passed to the module's setup function. This allows modules to receive initialization parameters without requiring separate configuration files or environment variables. The change also extends the host ABI with a config field that provides the serialized configuration bytes to the module during initialization.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The module vtable is now wrapped in a SendModuleVtable newtype that implements Send, matching the existing pattern used for the host vtable. This ensures the vtable can be safely transferred across async task boundaries during module initialization. Additionally, a redundant mut qualifier is removed from a lock guard and a clone is added to the message delivery path to avoid a move-related ownership issue.

Auto-committed-on: dragonfly
Move the construction of SendHostVtable outside of the closure passed to spawn_blocking to ensure the host vtable pointer is captured by value before the blocking task begins, preventing a potential use-after-free if the original host reference becomes invalid during the timeout.

Auto-committed-on: dragonfly
…ions

Replaced repeated string comparisons against member names with a single match to an enum, improving readability and reducing the risk of typos. The StopModule path now returns an explicit error instead of relying on an unreachable branch, making the control flow clearer.

Auto-committed-on: dragonfly
The `matches!` macro was consuming the `old` variable by value, which prevented the state from being used after the check. By borrowing with `&old`, the state is now inspected without moving it, allowing subsequent operations to proceed correctly.

Auto-committed-on: dragonfly
The unreachable panic was removed because the preceding match arms already cover all valid module members, making the fallback case dead code that would never be reached in practice.

Auto-committed-on: dragonfly
Updated the module documentation guide to specify that design references should be a focused README when the rationale exceeds what source rustdoc can convey, otherwise the module-level rustdoc itself suffices. This clarifies the documentation strategy and removes ambiguity about when to create separate README files.

Auto-committed-on: dragonfly
Add two new tests: one verifying that an invalid manifest declaration returns a null slice instead of panicking, and another confirming that the module refusal error sanitizes untrusted reason strings by stripping spaces and path separators.

Auto-committed-on: dragonfly
…nsistency

Reformat the large match expression in the broker's dispatch method to reduce nesting depth, and collapse several multi-line function signatures and call sites across the module host, transport, and test files to fit within the project's preferred line length. No behaviour is changed.

Auto-committed-on: dragonfly
The error message for module refusal now sanitizes each word individually instead of treating the entire reason as a single token, so that spaces in the original reason are preserved and each word is independently cleaned of untrusted content.

Auto-committed-on: dragonfly
… disabled

When the `modules` feature is not enabled, the `module_states` variable is unused because the loop that consumes it is conditionally compiled out. Adding a `let _ = module_states` assignment under the `not(feature = "modules")` guard silences the compiler warning without changing runtime behaviour.

Auto-committed-on: dragonfly
Adds assertions to the existing test for stopping one module to confirm that attempting to re-enable a stopped module produces a ModuleUnavailable error and that the module remains in the Stopped state.

Auto-committed-on: dragonfly
When the module transport fails to drain its queue within the deadline, the transport now marks the context as faulted, notifies waiters, and clears the inbound channel. This ensures that any pending or future operations on a stalled transport are properly terminated rather than left in an inconsistent state.

Auto-committed-on: dragonfly
…ect-only error handling

Replace two instances of `map_err` that only performed a side effect and re-raised the same error with `inspect_err`, which is the idiomatic way to observe an error without transforming it. This removes the unnecessary error re-wrapping and makes the intent clearer.

Auto-committed-on: dragonfly
The CI workflow now copies test modules to a private directory with restricted permissions to simulate real-world deployment conditions. On the host side, the module validation function uses `symlink_metadata` on Windows instead of `metadata` to correctly handle files that may be accessed through directory-level access control, and defers the file open call until after the metadata check to avoid permission errors on locked directories.

Auto-committed-on: dragonfly
Replace the legacy `New-Object` calls with the modern PowerShell class constructor syntax for `DirectorySecurity` and `FileSystemAccessRule` in the CI workflow. This improves consistency with current PowerShell practices and avoids potential compatibility issues with newer PowerShell versions.

Auto-committed-on: dragonfly
…ile test

The test for skipping non-regular files was creating a temporary directory in the current working directory, which could fail in environments where the current directory is not writable. The change now creates the temporary directory relative to the module artifact's parent path, ensuring the test works reliably regardless of the working directory.

Auto-committed-on: dragonfly
Reformatted a long line in the test helper to improve code readability without changing any behaviour.

Auto-committed-on: dragonfly

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/tinybus/src/bin/tinybus.rs (1)

330-367: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Authorization Bypass (CWE-862): Missing Authorization

Reachability: External

Reachability path
● Entry
  crates/tinybus/src/module/host_test.rs:289
  stopping_one_module_leaves_the_other_serving
│
▼
● Sink
  crates/tinybus/src/bin/tinybus.rs

Add caller authorization before module-control RPCs.

module_method dispatches calls to ai.tinyhumans.tinybus.Bus directly into ModuleControl without any caller, peer, or policy check. LoadModule/RescanModules can load and initialize a cdylib in the host process, and StopModule/EnableModule can change module lifecycle independently of the CLI. Artifact validation only authorizes the module path/content, not the RPC caller. Add a non-bypassable check for these members before module_method; if deployment expects all socket peers to be trusted, prove socket permissions make this true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tinybus/src/bin/tinybus.rs` around lines 330 - 367, Add a mandatory
caller/peer authorization check before the module_method dispatch for
LoadModule, RescanModules, StopModule, and EnableModule, using the existing
policy or peer-identity mechanism rather than trusting artifact validation or
CLI origin. Reject unauthorized requests before invoking ModuleControl, and
ensure there is no bypass path; if socket peers are intended to be universally
trusted, enforce and document verifiable socket permissions instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/tinybus/src/bin/tinybus.rs`:
- Around line 330-367: Add a mandatory caller/peer authorization check before
the module_method dispatch for LoadModule, RescanModules, StopModule, and
EnableModule, using the existing policy or peer-identity mechanism rather than
trusting artifact validation or CLI origin. Reject unauthorized requests before
invoking ModuleControl, and ensure there is no bypass path; if socket peers are
intended to be universally trusted, enforce and document verifiable socket
permissions instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 181835b0-4f80-4a0d-b78a-fcf1c1ad222b

📥 Commits

Reviewing files that changed from the base of the PR and between a2f571e and 5ca3bbd.

📒 Files selected for processing (24)
  • .github/workflows/ci.yml
  • Cargo.toml
  • README.md
  • ROADMAP.md
  • crates/tinybus-module/Cargo.toml
  • crates/tinybus-module/src/lib.rs
  • crates/tinybus/build.rs
  • crates/tinybus/examples/module_clock_two.rs
  • crates/tinybus/examples/module_wrong_target.rs
  • crates/tinybus/src/bin/tinybus.rs
  • crates/tinybus/src/broker.rs
  • crates/tinybus/src/connection.rs
  • crates/tinybus/src/error.rs
  • crates/tinybus/src/module/abi.rs
  • crates/tinybus/src/module/hash.rs
  • crates/tinybus/src/module/host.rs
  • crates/tinybus/src/module/host_test.rs
  • crates/tinybus/src/module/loader.rs
  • crates/tinybus/src/module/resolve.rs
  • crates/tinybus/src/module/transport.rs
  • docs/modules/README.md
  • docs/modules/module/README.md
  • docs/modules/module/abi.md
  • docs/protocol.md
🚧 Files skipped from review as they are similar to previous changes (17)
  • Cargo.toml
  • docs/modules/README.md
  • crates/tinybus-module/Cargo.toml
  • crates/tinybus/src/module/hash.rs
  • crates/tinybus/build.rs
  • crates/tinybus/examples/module_wrong_target.rs
  • docs/modules/module/abi.md
  • docs/modules/module/README.md
  • docs/protocol.md
  • README.md
  • crates/tinybus/examples/module_clock_two.rs
  • crates/tinybus/src/module/abi.rs
  • .github/workflows/ci.yml
  • crates/tinybus/src/module/loader.rs
  • crates/tinybus/src/broker.rs
  • crates/tinybus/src/module/host.rs
  • crates/tinybus-module/src/lib.rs

The CI workflow now creates a directory with restricted permissions and copies two module artifacts into it, exposing the test to a scenario where some modules are loadable and others are not. The host test is updated to use this pre-prepared directory when the environment variable is set, falling back to the previous temporary-directory approach otherwise. This allows the integration test to verify that a single refused module does not prevent the remaining modules in the same directory from loading.

Auto-committed-on: dragonfly
@senamakel
senamakel merged commit dd6063a into main Aug 10, 2026
7 checks passed
@senamakel
senamakel deleted the feature/modules-dlopen branch August 14, 2026 07:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

modules: load integrations into the OpenHuman process as dlopen'd cdylibs

1 participant