Skip to main content

aimdb_core/remote/
mod.rs

1//! Remote access subsystem for AimDB (AimX protocol)
2//!
3//! Provides introspection and management APIs over Unix domain sockets,
4//! enabling external tools (CLI, dashboards, MCP adapters) to interact
5//! with running AimDB instances.
6//!
7//! # Protocol
8//!
9//! AimX uses NDJSON (newline-delimited JSON) tagged frames over a session
10//! transport (Unix domain sockets via `aimdb-uds-connector`, serial via
11//! `aimdb-serial-connector`). The envelope codec lives in `session::aimx`
12//! (feature `connector-session`). Compatibility is by major version — see
13//! [`PROTOCOL_VERSION`] and [`version_compatible`].
14//!
15//! # Security
16//!
17//! - **Read-only by default**: No writes unless explicitly enabled
18//! - **UDS permissions**: Primary security mechanism (file permissions)
19//! - **Optional auth tokens**: Additional authentication layer
20//! - **Per-record write permissions**: Explicit opt-in required
21//!
22//! # Usage
23//!
24//! Remote access is registered like any other connector — via `with_connector`
25//! using `aimdb_uds_connector::UdsServer` (this replaced the former
26//! `AimDbBuilder::with_remote_access(config)`). Illustrative (not compiled:
27//! the connector lives in a downstream crate `aimdb-core` cannot depend on):
28//!
29//! ```rust,ignore
30//! use aimdb_core::remote::{AimxConfig, SecurityPolicy};
31//! use aimdb_uds_connector::UdsServer;
32//!
33//! let config = AimxConfig::uds_default()
34//!     .socket_path("/var/run/aimdb/aimdb.sock")
35//!     .security_policy(SecurityPolicy::ReadOnly)
36//!     .max_connections(16)
37//!     .max_subs_per_connection(32);
38//!
39//! let (db, runner) = AimDbBuilder::new()
40//!     .runtime(tokio_adapter)
41//!     .with_connector(UdsServer::from_config(config))
42//!     .build()
43//!     .await?;
44//! ```
45
46mod config;
47mod error;
48mod metadata;
49mod protocol;
50mod query;
51
52pub use config::{AimxConfig, SecurityPolicy};
53pub use error::{RemoteError, RemoteResult};
54pub use metadata::RecordMetadata;
55pub use protocol::{
56    version_compatible, ws_url_with_version, ErrorObject, Event, HelloMessage, Request, Response,
57    WelcomeMessage, PROTOCOL_VERSION, VERSION_PARAM,
58};
59pub use query::{QueryHandlerFn, QueryHandlerParams, QueryRecord, QUERY_ALL_PATTERN};
60
61// Internal exports for implementation
62#[cfg(feature = "connector-session")]
63pub(crate) mod stream;
64
65/// The leaf (entity) segment of a record key — the trailing component after the
66/// last separator. Both `.` and `/` are treated as delimiters, since keys use
67/// either convention (`temp.vienna` or `sensors/temp/vienna`), and both yield
68/// `vienna`. Servers report this as a record's `entity`, which identifies the
69/// node only where the leaf *is* the node: `station.17.temperature` yields
70/// `temperature`, not `17`.
71pub fn topic_leaf(key: &str) -> &str {
72    key.rsplit(['.', '/']).next().unwrap_or(key)
73}
74
75#[cfg(test)]
76mod topic_leaf_tests {
77    use super::topic_leaf;
78
79    #[test]
80    fn leaf_handles_both_separators() {
81        // Dot convention.
82        assert_eq!(topic_leaf("temp.vienna"), "vienna");
83        // Slash convention (the bug: `rsplit('.')` returned the whole key).
84        assert_eq!(topic_leaf("sensors/temp/vienna"), "vienna");
85        // Mixed — the last separator of either kind wins.
86        assert_eq!(topic_leaf("sensors/temp.vienna"), "vienna");
87        assert_eq!(topic_leaf("a.b/c"), "c");
88        // No separator: the whole key is its own leaf.
89        assert_eq!(topic_leaf("vienna"), "vienna");
90    }
91}