heldar_kernel/modules.rs
1//! Module manifests — the compile-time half of the plugin platform.
2//!
3//! A [`ModuleManifest`] describes one loaded module (an app crate today; a runtime-registered sidecar
4//! plugin in a later phase). The composing binary collects every module's manifest into
5//! [`crate::state::AppState::modules`], and `GET /api/v1/modules` serves the set so the dashboard
6//! renders its nav + routes from live truth instead of a hardcoded list. The kernel itself ships no
7//! manifest and names no module — it only carries and serves whatever the binary composes.
8
9use serde::{Deserialize, Serialize};
10
11/// Where a module comes from. Drives how the plugin store shelves it and how the dashboard badges it.
12/// Runtime-imported (bring-your-own) plugins use [`ModuleKind::Imported`]; catalog-listed third-party
13/// plugins use [`ModuleKind::Community`].
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum ModuleKind {
17 /// First-party, open (Apache-2.0) module compiled into the build.
18 Core,
19 /// First-party proprietary vertical compiled into the build.
20 Proprietary,
21 /// Third-party plugin listed in a registry catalog.
22 Community,
23 /// A runtime-loaded sidecar plugin (bring-your-own, installed by URL).
24 Imported,
25}
26
27/// A nav destination a module contributes to the dashboard. `icon` is a key the dashboard resolves to
28/// a glyph, falling back to a generic module glyph for unknown keys (so imported plugins still render).
29#[derive(Clone, Debug, Serialize, Deserialize)]
30pub struct NavEntry {
31 /// Client route path, e.g. `/entry`.
32 pub path: String,
33 /// Human label shown in the nav rail.
34 pub label: String,
35 /// Icon key the dashboard maps to a glyph.
36 pub icon: String,
37}
38
39/// Describes one loaded module. Serialized as-is at `GET /api/v1/modules`.
40#[derive(Clone, Debug, Serialize)]
41pub struct ModuleManifest {
42 /// Stable id, e.g. `entry`. The dashboard keys its page registry on this.
43 pub id: String,
44 /// Display name, e.g. `Access Control`.
45 pub name: String,
46 /// Module version (the crate version for compiled modules).
47 pub version: String,
48 /// Who publishes the module.
49 pub publisher: String,
50 /// Provenance (core / proprietary / imported).
51 pub kind: ModuleKind,
52 /// One-line description for the module list / store.
53 pub description: String,
54 /// Nav entries this module contributes (usually one).
55 pub nav: Vec<NavEntry>,
56 /// How the dashboard renders the module's content. Compiled modules use a `bundled` page; runtime
57 /// sidecar plugins are `iframe`-mounted at `/m/{id}/` (the kernel reverse-proxies to the sidecar).
58 pub mount: MountKind,
59 /// Reachability of a sidecar's base URL (`unknown`/`healthy`/`unreachable`); `None` for compiled.
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub health: Option<String>,
62}
63
64/// How the dashboard renders a module's content area.
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
66#[serde(rename_all = "snake_case")]
67pub enum MountKind {
68 /// A page component shipped in the dashboard bundle, keyed by module id (compiled modules).
69 Bundled,
70 /// An iframe to `/m/{id}/`, which the kernel reverse-proxies to the sidecar (imported modules).
71 Iframe,
72 /// No UI — a headless compute plugin (e.g. a sandboxed Wasm DetectionConsumer). Contributes no nav
73 /// route; the store lists it with a "compute" treatment and no Open affordance.
74 Headless,
75}
76
77impl ModuleManifest {
78 /// Convenience builder for a single-nav-entry compiled (bundled) module.
79 pub fn new(
80 id: &str,
81 name: &str,
82 version: &str,
83 publisher: &str,
84 kind: ModuleKind,
85 description: &str,
86 nav: Vec<NavEntry>,
87 ) -> Self {
88 Self {
89 id: id.to_string(),
90 name: name.to_string(),
91 version: version.to_string(),
92 publisher: publisher.to_string(),
93 kind,
94 description: description.to_string(),
95 nav,
96 mount: MountKind::Bundled,
97 health: None,
98 }
99 }
100}
101
102impl NavEntry {
103 pub fn new(path: &str, label: &str, icon: &str) -> Self {
104 Self {
105 path: path.to_string(),
106 label: label.to_string(),
107 icon: icon.to_string(),
108 }
109 }
110}
111
112/* ------------------------------------------------------------------ */
113/* Runtime sidecar registrations (Phase B) */
114/* ------------------------------------------------------------------ */
115
116/// The manifest a sidecar plugin presents to register itself (POST /api/v1/modules). The kernel mints
117/// a scoped API key + a webhook subscription from it and reverse-proxies `/m/{id}/*` to `base_url`.
118#[derive(Clone, Debug, Deserialize)]
119pub struct ModuleRegisterRequest {
120 /// Stable id (slug): the `/m/{id}/` mount + nav key. Must not collide with a compiled module.
121 pub id: String,
122 pub name: String,
123 #[serde(default)]
124 pub version: String,
125 #[serde(default)]
126 pub publisher: String,
127 #[serde(default)]
128 pub description: String,
129 /// The sidecar's origin the kernel reverse-proxies to (http/https), e.g. `http://127.0.0.1:9123`.
130 pub base_url: String,
131 /// Nav entries to surface (defaults to one entry at `/{id}` if omitted).
132 #[serde(default)]
133 pub nav: Vec<NavEntry>,
134 /// Event types to deliver to the sidecar's webhook (`["*"]` = all). Defaults to all.
135 #[serde(default)]
136 pub subscribes: Option<Vec<String>>,
137 /// Role of the minted API key. Restricted to least-privilege (`viewer` | `integration`).
138 #[serde(default)]
139 pub role: Option<String>,
140}
141
142/// A stored sidecar registration row.
143#[derive(Clone, Debug, sqlx::FromRow)]
144pub struct ModuleRegistration {
145 pub id: String,
146 pub name: String,
147 pub version: String,
148 pub publisher: String,
149 pub description: String,
150 pub base_url: String,
151 /// JSON array of [`NavEntry`].
152 pub nav: sqlx::types::Json<Vec<NavEntry>>,
153 /// JSON array of event-type tokens.
154 pub subscribes: sqlx::types::Json<Vec<String>>,
155 pub role: String,
156 pub api_key_id: Option<String>,
157 pub webhook_id: Option<String>,
158 pub health: String,
159 pub health_checked_at: Option<chrono::DateTime<chrono::Utc>>,
160 pub created_at: chrono::DateTime<chrono::Utc>,
161 pub updated_at: chrono::DateTime<chrono::Utc>,
162}
163
164impl ModuleRegistration {
165 /// Project the stored row into the uniform manifest the dashboard consumes (kind = imported,
166 /// iframe-mounted, with live health).
167 pub fn to_manifest(&self) -> ModuleManifest {
168 ModuleManifest {
169 id: self.id.clone(),
170 name: self.name.clone(),
171 version: self.version.clone(),
172 publisher: self.publisher.clone(),
173 kind: ModuleKind::Imported,
174 description: self.description.clone(),
175 nav: self.nav.0.clone(),
176 mount: MountKind::Iframe,
177 health: Some(self.health.clone()),
178 }
179 }
180}
181
182/// Admin-only detail for a single registration (includes the sidecar URL + minted resource ids).
183#[derive(Clone, Debug, Serialize)]
184pub struct ModuleDetail {
185 pub id: String,
186 pub name: String,
187 pub version: String,
188 pub publisher: String,
189 pub description: String,
190 pub base_url: String,
191 pub nav: Vec<NavEntry>,
192 pub subscribes: Vec<String>,
193 pub role: String,
194 pub api_key_id: Option<String>,
195 pub webhook_id: Option<String>,
196 pub health: String,
197 pub health_checked_at: Option<chrono::DateTime<chrono::Utc>>,
198 pub created_at: chrono::DateTime<chrono::Utc>,
199}
200
201impl From<&ModuleRegistration> for ModuleDetail {
202 fn from(r: &ModuleRegistration) -> Self {
203 ModuleDetail {
204 id: r.id.clone(),
205 name: r.name.clone(),
206 version: r.version.clone(),
207 publisher: r.publisher.clone(),
208 description: r.description.clone(),
209 base_url: r.base_url.clone(),
210 nav: r.nav.0.clone(),
211 subscribes: r.subscribes.0.clone(),
212 role: r.role.clone(),
213 api_key_id: r.api_key_id.clone(),
214 webhook_id: r.webhook_id.clone(),
215 health: r.health.clone(),
216 health_checked_at: r.health_checked_at,
217 created_at: r.created_at,
218 }
219 }
220}
221
222/// The once-returned credentials a freshly registered sidecar needs to configure itself.
223#[derive(Clone, Debug, Serialize)]
224pub struct ModuleRegistered {
225 pub module: ModuleDetail,
226 /// The minted API key (plaintext, returned ONCE) the sidecar uses to call kernel APIs.
227 pub api_key: String,
228 /// The HMAC-SHA256 secret (returned ONCE) the kernel signs the sidecar's webhook deliveries with.
229 pub webhook_secret: String,
230}