pim_plugin/lib.rs
1//! Plugin extensibility for `pim-daemon`.
2//!
3//! A plugin runs in-process under the daemon's tokio runtime. The
4//! daemon owns mesh-essential state (identity broadcast, routing,
5//! the peer keystore); plugins consume those services through the
6//! ports defined here and contribute:
7//!
8//! - Inbound [`ControlFrame::PluginPayload`] handling for the
9//! plugin's own `kind` namespace.
10//! - Optional reactions to peer-state changes — currently
11//! [`DaemonPlugin::on_peer_forgotten`].
12//!
13//! JSON-RPC method registration is intentionally NOT routed through
14//! this trait: methods are wired into the daemon's RPC dispatcher at
15//! compile time behind a Cargo feature, so the daemon can be built
16//! entirely without a given plugin.
17
18#![warn(missing_docs)]
19
20use std::path::PathBuf;
21use std::sync::Arc;
22
23use async_trait::async_trait;
24use bytes::Bytes;
25use pim_core::NodeId;
26use pim_protocol::ControlFrame;
27use serde::{Deserialize, Serialize};
28use tokio::sync::broadcast;
29use tokio_util::sync::CancellationToken;
30
31/// Origin of an inbound `PeerInfo` frame.
32///
33/// `Direct` means it arrived on a session we have a Noise handshake
34/// with; `Routed` means it arrived as a multi-hop control payload.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum PeerInfoSource {
38 /// Direct neighbour over an existing session.
39 Direct,
40 /// Routed via the multi-hop control plane (mesh broadcast).
41 Routed,
42}
43
44/// Event published by the daemon's [`PeerDirectory`] when its peer
45/// keystore mutates. Plugins subscribe to react to identity changes
46/// without polling.
47#[derive(Debug, Clone)]
48pub enum PeerDirectoryEvent {
49 /// A peer's identity was learned or refreshed.
50 Seen {
51 /// Stable cryptographic identifier.
52 node_id: NodeId,
53 /// X25519 public key derived from the peer's signing key.
54 x25519_pub: [u8; 32],
55 /// Latest friendly name advertised by the peer (may be empty).
56 name: String,
57 /// Whether the identity arrived directly or through routing.
58 via: PeerInfoSource,
59 },
60 /// A peer was forgotten via `peers.forget`.
61 Forgotten {
62 /// Forgotten peer.
63 node_id: NodeId,
64 },
65}
66
67/// Read-mostly access to the daemon's keystore of known peers.
68///
69/// The implementation lives in `pim-daemon`; plugins receive it as
70/// `Arc<dyn PeerDirectory>` through [`PluginContext`].
71#[async_trait]
72pub trait PeerDirectory: Send + Sync {
73 /// Latest cached X25519 public key for `peer`, if known.
74 async fn lookup_x25519(&self, peer: &NodeId) -> Option<[u8; 32]>;
75
76 /// Latest friendly name observed for `peer`, if known.
77 async fn lookup_name(&self, peer: &NodeId) -> Option<String>;
78
79 /// Subscribe to identity-state events. Each subscriber gets every
80 /// event emitted after the moment of subscription.
81 fn subscribe(&self) -> broadcast::Receiver<PeerDirectoryEvent>;
82}
83
84/// Send a [`ControlFrame`] toward a peer, either over a direct
85/// connected session or via the multi-hop routing table.
86#[async_trait]
87pub trait ControlSender: Send + Sync {
88 /// Send to a directly-connected peer. Best-effort; failures are
89 /// logged by the underlying transport.
90 async fn send_direct(&self, peer: NodeId, frame: ControlFrame);
91
92 /// Send via the routing table. Returns `true` when the next-hop
93 /// send was attempted; `false` when no route exists.
94 async fn send_routed(&self, dst_id: NodeId, frame: ControlFrame) -> bool;
95}
96
97/// Local identity material a plugin may need (typically for ECIES
98/// decrypt of messages addressed to us).
99pub trait IdentitySecrets: Send + Sync {
100 /// Raw bytes of our Ed25519 signing key. Plugins that need an
101 /// X25519 secret derive it deterministically from this seed
102 /// (see `pim_crypto::e2e_decrypt_in_place`, etc.).
103 fn signing_seed(&self) -> [u8; 32];
104}
105
106/// Snapshot of services + scratch space handed to a plugin at startup.
107#[derive(Clone)]
108pub struct PluginContext {
109 /// Read-side access to the peer keystore.
110 pub peers: Arc<dyn PeerDirectory>,
111 /// Outbound control-frame sender.
112 pub control: Arc<dyn ControlSender>,
113 /// Local identity (for ECIES decrypt etc.).
114 pub identity: Arc<dyn IdentitySecrets>,
115 /// Daemon data directory; plugins place their own files
116 /// (databases, snapshots) under it.
117 pub data_dir: PathBuf,
118 /// Daemon-wide cancellation token. Plugins should tie any
119 /// long-running tasks to this so a clean shutdown propagates.
120 pub cancel: CancellationToken,
121}
122
123/// In-process plugin contract.
124///
125/// One instance per plugin per daemon. The daemon takes
126/// `Arc<dyn DaemonPlugin>`, calls [`Self::start`] once, then routes
127/// inbound payloads matching [`Self::payload_kinds`] through
128/// [`Self::handle_payload`]. On clean shutdown, [`Self::shutdown`] is
129/// called once.
130#[async_trait]
131pub trait DaemonPlugin: Send + Sync + 'static {
132 /// Stable identifier — used for log spans and (by convention) as a
133 /// prefix for the plugin's payload kinds.
134 fn name(&self) -> &'static str;
135
136 /// Stable list of `PluginPayload.kind` values this plugin claims.
137 /// The daemon dispatches inbound payloads by matching `kind`
138 /// against each registered plugin in order; the first match wins.
139 fn payload_kinds(&self) -> &'static [&'static str];
140
141 /// Handle an inbound [`ControlFrame::PluginPayload`] whose `kind`
142 /// appeared in [`Self::payload_kinds`].
143 async fn handle_payload(&self, src: NodeId, kind: &str, body: Bytes);
144
145 /// Spawn whatever long-running tasks the plugin needs. Returns
146 /// once startup is complete; long-running work should be on
147 /// detached `tokio::spawn` handles tied to `ctx.cancel`.
148 async fn start(self: Arc<Self>, ctx: PluginContext) -> anyhow::Result<()>;
149
150 /// Notification: the daemon dropped this peer's identity from the
151 /// keystore (e.g. via `peers.forget`). Plugins typically wipe any
152 /// per-peer state of their own here. Failure is logged but does
153 /// not block other plugins from being notified.
154 async fn on_peer_forgotten(&self, peer: NodeId);
155
156 /// Best-effort shutdown — called once at daemon teardown.
157 async fn shutdown(&self);
158}