Skip to main content

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}