Files
logos-liblogos/docs/spec.md
2026-04-17 09:40:36 -04:00

239 lines
15 KiB
Markdown

# Logos Core Library
## Overall Description
Logos Core is a modular runtime platform for hosting and orchestrating independently developed modules (plugins). It provides a C-API shared library (`liblogos_core`) and an accompanying module host (`logos_host`) that together enable a plug-in-based architecture for decentralised applications.
The platform is designed to:
- Load, start, stop, and introspect modules at runtime
- Isolate each module in its own process for robustness and security
- Provide transparent inter-module RPC via a remote object registry
- Support token-based authentication for secure module-to-module communication
- Expose a C API so that host applications in any language can drive the runtime
## Definitions & Acronyms
| Term | Definition |
|------|------------|
| **Module** | An independently developed plugin that implements `PluginInterface` and is dynamically loaded by the core |
| **Core Library** | `liblogos_core` — the shared library that provides the C API for module management |
| **Module Host** | `logos_host` — a lightweight executable that loads a single module in its own process |
| **Core Manager** | A built-in module that exposes core functionality via RPC, allowing modules to manage the core without linking against the C API |
| **Capability Module** | A built-in module that handles authorization tokens for inter-module communication |
| **RPC** | Remote Procedure Call — the mechanism by which modules invoke methods on each other |
| **IPC** | Inter-Process Communication — the underlying transport |
| **Token** | A UUID-based authentication credential issued by the core or capability module for securing RPC calls |
| **SDK** | The [logos-cpp-sdk](https://github.com/logos-co/logos-cpp-sdk) — client library that abstracts connection management, token handling, and asynchronous invocation |
## Domain Model
### System Architecture
At a high level, the Logos Core consists of:
**Core Library** — The C/C++ shared library (`liblogos_core`) that provides the API functions for lifecycle management, module loading/unloading, and introspection.
**Core Manager** — A built-in module that runs in the core process and exposes core functionality as RPC methods, allowing remote modules to manage the core without linking against the C API directly.
**Module Host** — A lightweight executable (`logos_host`) that loads a single module in its own process. It communicates with the core over a local socket to receive an authentication token and registers the module's object with the remote registry.
**Capability Module** — A built-in module that handles authorization for inter-module communication by issuing tokens and notifying both communicating parties.
**Remote Object Registry** — A registry that maintains a mapping of module names to remote object replicas and forwards method calls/events.
### Process Architecture
Each module runs in its own process for isolation (using the default `qt-subprocess` runtime):
```
┌─────────────────────────────────────────────────┐
│ Host Application │
│ ┌───────────────────────────────────────────┐ │
│ │ liblogos_core │ │
│ │ ├─ Core Manager (built-in module) │ │
│ │ ├─ Capability Module (built-in module) │ │
│ │ └─ Remote Object Registry │ │
│ └───────────────────────────────────────────┘ │
│ │ IPC (local socket) │
│ ┌─────┼─────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │host │ │host │ │host │ (logos_host processes) │
│ │mod A│ │mod B│ │mod C│ │
│ └─────┘ └─────┘ └─────┘ │
└─────────────────────────────────────────────────┘
```
- The core spawns a `logos_host` process per module via the `QtSubprocessRuntime`
- Communication happens via the Logos API (currently uses Qt Remote Objects over local sockets)
- Faulty or untrusted modules cannot crash the core or other modules
- The `ModuleRuntime` abstraction allows swapping the loading strategy per-module: in-process, WASM (Extism), Docker, gRPC, etc.
### Token-Based Authentication
Since the remote object registry has no built-in security mechanisms, all RPC calls require an authentication token. This is transparent to module developers when using the SDK:
1. **Core → Module**: When a module is loaded, the core generates a UUID token and sends it to the module process via local socket. The module uses this token to authenticate calls from the core.
2. **Module → Module**: When modules need to communicate, they request authorization from the Capability Module, which issues a token and notifies both parties. The modules then use this token for subsequent requests.
3. **Token Storage**: Each module stores tokens in a thread-safe `TokenManager` (part of the SDK). `ModuleProxy` validates tokens before dispatching method calls.
### Module Metadata
Every module ships a `metadata.json` referenced by Qt's `Q_PLUGIN_METADATA` macro. Required fields:
| Field | Purpose |
|-------|---------|
| `name` | Unique module identifier (must match `name()` return value) |
| `version` | Semantic version string |
| `description` | Human-readable description |
| `author` | Module author or organization |
| `type` | Module type (e.g., `"core"`) |
| `category` | Module category for organization |
| `main` | Main plugin class name |
| `dependencies` | Array of required module names |
| `capabilities` | Array of capabilities this module provides |
| `include` | Optional array of extra files (shared libs, resources) to bundle |
## Features & Requirements
### Module Lifecycle
#### Discovery
1. Core scans configured plugin directories for `.so`, `.dylib`, or `.dll` files
2. For each file, metadata is extracted via `QPluginLoader`
3. Modules are added to the "known" list without being loaded
4. Multiple plugin directories can be configured
#### Loading
1. Core locates the plugin file for the requested module name
2. Core resolves dependencies and loads them first (topological sort with circular dependency detection)
3. If a persistence base path is configured, core resolves an instance ID and persistence directory for the module
4. Core builds a `ModuleDescriptor` and selects the appropriate `ModuleRuntime` via `RuntimeRegistry`
5. The selected runtime's `load()` spawns (or otherwise starts) the module and returns a `LoadedModuleHandle`
6. Core generates a UUID authentication token and calls `runtime->sendToken()`
7. For the default `qt-subprocess` runtime: `logos_host` process loads the plugin, calls `initLogos(LogosAPI*)`, and registers with the remote object registry
8. Core records the module as loaded, storing the runtime and handle in `PluginRegistry`
#### Unloading
1. The module's host process is terminated
2. The module is removed from the loaded modules list
3. Associated tokens and state are cleaned up
#### Cascade Unloading
`logos_core_unload_plugin_with_dependents()` unloads the named module together with every currently loaded module that transitively depends on it. Teardown order is leaves-first (dependents before dependencies) so no process is left briefly pointing at a terminated parent. The call is serialised with ordinary load/unload operations under a single lock span — a late-arriving load cannot interleave between tearing down the dependents and the target.
### Dependency Resolution
- Dependencies are declared in each module's `metadata.json`
- `logos_core_load_plugin_with_dependencies()` performs topological sort
- Circular dependencies are detected and reported as errors
- Missing dependencies produce warnings
- Dependencies are loaded in correct order before the requesting module
- The core maintains an in-process dependency graph with both forward and reverse edges. The reverse edges are re-derived from the forward edges at the tail of every discovery or metadata-processing pass, so cascade unload and dependent queries answer from memory without re-reading manifests from disk.
### Process Monitoring
- CPU percentage, CPU time, and memory usage tracked per module process
- Statistics returned as JSON via `logos_core_get_module_stats()`
- Core Manager process is excluded from stats
- Not available on iOS
### Thread Safety
The C API is designed to be safe for use from multi-threaded host applications:
- **Load/unload operations** (`load_plugin`, `load_plugin_with_dependencies`, `unload_plugin`, `unload_plugin_with_dependents`) are serialised — only one runs at a time, so rapid concurrent load/unload cycles on the same or different modules do not produce data races. The cascade variant holds the lock for its full leaves-first teardown.
- **Read-only queries** (`get_known_plugins`, `get_loaded_plugins`) use a shared reader-writer lock and may execute concurrently with each other and with load/unload operations.
- **Plugin discovery** (`refresh_plugins`) is protected by the registry's own write lock.
- **Lifecycle functions** (`init`, `start`, `cleanup`) are not thread-safe and must be called from a single thread.
### Dev vs Portable Builds
The platform supports two build variants:
- **Dev build** (default): Plugin loading looks for LGX variants with `-dev` suffix (e.g., `linux-amd64-dev`). Used in Nix/development environments.
- **Portable build**: Looks for portable variants without suffix (e.g., `linux-amd64`). Used in self-contained distributed applications.
## API Description
### Core Lifecycle
| Function | Purpose |
|----------|---------|
| `logos_core_init(argc, argv)` | Initialize global state, optionally set plugin directory. Creates a QCoreApplication if one does not exist. |
| `logos_core_set_plugins_dir(path)` | Set the plugin directory. Must be called before starting. |
| `logos_core_add_plugins_dir(path)` | Add an additional plugin directory to scan. |
| `logos_core_start()` | Scan plugin directories, process metadata, create Core Manager, load built-in modules, start remote object registry. |
| `logos_core_exec()` | Run the Qt event loop. Returns when the application exits. |
| `logos_core_cleanup()` | Unload all modules, stop processes, clean up global state. |
| `logos_core_process_events()` | Process Qt events without blocking, for integration with external event loops. |
### Plugin Management
| Function | Purpose |
|----------|---------|
| `logos_core_get_loaded_plugins() → char**` | Return null-terminated array of loaded module names. Caller must free. |
| `logos_core_get_known_plugins() → char**` | Return null-terminated array of all discovered modules. Caller must free. |
| `logos_core_load_plugin(name) → int` | Load a module by name. Returns 1 on success, 0 on failure. |
| `logos_core_load_plugin_with_dependencies(name) → int` | Load a module and all its dependencies in correct order. Returns 1 if all succeed. |
| `logos_core_unload_plugin(name) → int` | Terminate the module's process and remove it. Returns 1 on success. |
| `logos_core_unload_plugin_with_dependents(name) → int` | Cascade unload: terminate the module together with every currently loaded transitive dependent, leaves-first. Returns 1 only if every step succeeded. |
| `logos_core_get_module_dependencies(name, recursive) → char**` | Return null-terminated array of modules that `name` depends on (forward edges). With `recursive=true`, walks the forward dependency graph transitively via BFS. Unknown names yield an empty array. Caller must free. |
| `logos_core_get_module_dependents(name, recursive) → char**` | Return null-terminated array of modules that depend on `name` (reverse edges). With `recursive=true`, walks the reverse dependency graph transitively via BFS. Unknown names yield an empty array. Caller must free. |
| `logos_core_process_plugin(path) → char*` | Read a module file's metadata and register it as known without loading. Returns the module name or NULL. Caller must free. |
### Token and Monitoring
| Function | Purpose |
|----------|---------|
| `logos_core_get_token(key) → char*` | Return the auth token for a key. Caller must free. NULL if not found. |
| `logos_core_get_module_stats() → char*` | Return JSON array of CPU/memory stats per loaded module. Caller must free. Not available on iOS. |
### Core Manager Module (RPC Surface)
The Core Manager is a built-in module exposing core functionality to remote modules:
| Method | Purpose |
|--------|---------|
| `setPluginsDirectory(directory)` | Set the module search directory. |
| `start()` | Start the core's registry and load built-in modules. |
| `cleanup()` | Unload all modules and shut down. |
| `getLoadedPlugins() → std::vector<std::string>` | Return names of loaded modules. |
| `getKnownPlugins() → QJsonArray` | Return all known modules with `loaded` flag. |
| `loadPlugin(name) → bool` | Load a plugin by name. |
| `unloadPlugin(name) → bool` | Unload a plugin by name. |
| `processPlugin(filePath) → std::string` | Read a plugin file's metadata and register it. |
| `getPluginMethods(name) → QJsonArray` | Introspect a module's methods via Qt meta-object system. |
## Module Implementation
A complete module must implement the Logos Module interface.
### Inter-Module Communication
Modules communicate using the SDK's generated C++ wrappers:
```
logos-><module_name>.<method>(args...) // call a remote method
logos-><module_name>.on("event", callback) // subscribe to events
logos-><module_name>.trigger("event", data) // emit an event
```
The SDK abstracts away registry lookup, token management, and async invocation.
## Supported Platforms
- macOS (aarch64-darwin, x86_64-darwin)
- Linux (aarch64-linux, x86_64-linux)
## Future Work
- **Signature support** — Signing and verifying module packages
- **Cross-language modules** — Modules in languages other than C++ (enabled by `ModuleRuntime` abstraction)
- **Alternative runtimes** — In-process loading, Extism/WASM modules, Docker container isolation, gRPC transport (all implementable as new `ModuleRuntime` subclasses)
- **Move away from Qt** — Logos API will move away from Qt. `logos_core`'s internal code (`PluginManager`, `RuntimeRegistry`, `ModuleRuntime`) is already Qt-free. The `qt_subprocess` runtime and `logos_host` encapsulate the remaining Qt dependencies (event loop, plugin loading, Qt Remote Objects). New runtimes can avoid Qt entirely.