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}