Skip to main content

haloforge_plugin_api/
lib.rs

1pub mod error;
2pub mod manifest;
3pub mod permissions;
4pub mod types;
5
6pub use error::PluginError;
7pub use manifest::{PluginManifest, CapabilityLevel, IntegrationConfig};
8pub use permissions::Permission;
9pub use types::*;
10
11/// The stable ABI version of this plugin API.
12/// Increment MAJOR on any breaking change to the HaloForgePlugin trait or context traits.
13pub const PLUGIN_ABI_VERSION: u32 = 1;
14
15// ─── The core plugin trait ────────────────────────────────────────────────────
16
17/// Every native plugin must implement this trait.
18///
19/// The host loads the dynamic library, calls `_haloforge_plugin_create()` (declared via
20/// the `declare_plugin!` macro) to obtain a `Box<dyn HaloForgePlugin>`, then calls
21/// `on_load()`. On disable/shutdown, `on_unload()` is called.
22pub trait HaloForgePlugin: Send + Sync {
23    /// Return plugin metadata. Called before `on_load`.
24    fn metadata(&self) -> PluginMetadata;
25
26    /// Called after the plugin is loaded and context is ready.
27    /// Register IPC commands, workflow step types, subscribe to events, create DB tables here.
28    fn on_load(
29        &mut self,
30        ctx: &dyn PluginContext,
31        ipc: &mut dyn IpcRegistrar,
32    ) -> Result<(), PluginError>;
33
34    /// Called when the plugin is being unloaded (disabled or app shutdown).
35    /// Stop background tasks, release file handles, unsubscribe events.
36    fn on_unload(&mut self) -> Result<(), PluginError>;
37
38    /// Called when the user saves new settings for this plugin in Plugin Manager.
39    fn on_settings_changed(&mut self, _settings: serde_json::Value) -> Result<(), PluginError> {
40        Ok(())
41    }
42
43    /// Called to execute a workflow step of a type registered by this plugin (Level 4).
44    /// Return a JSON result value on success, or PluginError on failure.
45    fn execute_workflow_step(
46        &mut self,
47        _step_type: &str,
48        _config: serde_json::Value,
49        _ctx: &dyn PluginContext,
50    ) -> Result<serde_json::Value, PluginError> {
51        Err(PluginError::Unsupported("execute_workflow_step".into()))
52    }
53}
54
55// ─── Plugin context (passed to the plugin at on_load) ────────────────────────
56
57/// The host-provided context injected into the plugin at load time.
58/// This is the plugin's *only* gateway to host services.
59pub trait PluginContext: Send + Sync {
60    /// Sandboxed database access (own tables + approved host tables).
61    fn db(&self) -> &dyn DatabaseAccess;
62
63    /// App event bus.
64    fn events(&self) -> &dyn EventBus;
65
66    /// HTTP client — `None` if `network:http*` not granted.
67    fn http(&self) -> Option<&dyn HttpClient>;
68
69    /// Filesystem access — `None` if `filesystem:*` not granted.
70    fn fs(&self) -> Option<&dyn PluginFs>;
71
72    /// Process runner — `None` if `process:spawn*` not granted.
73    fn process(&self) -> Option<&dyn ProcessRunner>;
74
75    /// Read the plugin's current settings (values from `settings_schema`).
76    fn settings(&self) -> serde_json::Value;
77
78    /// Persist updated plugin settings.
79    fn save_settings(&self, settings: serde_json::Value) -> Result<(), PluginError>;
80
81    /// Absolute path to the plugin's private data directory.
82    /// e.g. `~/.haloforge/plugins/{plugin-id}/data/`
83    fn data_dir(&self) -> std::path::PathBuf;
84
85    /// Emit a structured log line tagged with the plugin id.
86    fn log(&self, level: LogLevel, msg: &str);
87
88    /// Show a toast notification in the HaloForge UI.
89    fn notify(&self, notification: Notification);
90}
91
92// ─── Database access ──────────────────────────────────────────────────────────
93
94pub trait DatabaseAccess: Send + Sync {
95    /// Execute a SELECT against plugin-owned tables.
96    /// Table names must start with `plugin_{plugin_id}_`.
97    fn query(
98        &self,
99        sql: &str,
100        params: &[serde_json::Value],
101    ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>, PluginError>;
102
103    /// Execute INSERT / UPDATE / DELETE against plugin-owned tables.
104    fn execute(
105        &self,
106        sql: &str,
107        params: &[serde_json::Value],
108    ) -> Result<usize, PluginError>;
109
110    /// Create a table in the plugin's namespace.
111    /// The actual table name will be `plugin_{plugin_id}_{table_name}`.
112    fn create_table(&self, table_name: &str, schema_sql: &str) -> Result<(), PluginError>;
113
114    /// Read rows from an approved host table.
115    /// Requires `database:read:<table>` permission.
116    fn read_host_table(
117        &self,
118        table: HostTable,
119        limit: Option<u32>,
120    ) -> Result<Vec<serde_json::Value>, PluginError>;
121}
122
123/// Host tables that can be granted read access.
124#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
125#[serde(rename_all = "snake_case")]
126pub enum HostTable {
127    LaunchProfiles,
128    Workflows,
129    CodeSnippets,
130    Skills,
131    McpServers,
132    ChatSessions,
133    ModelConfigs,
134}
135
136impl HostTable {
137    pub fn as_str(&self) -> &'static str {
138        match self {
139            Self::LaunchProfiles => "launch_profiles",
140            Self::Workflows      => "workflows",
141            Self::CodeSnippets   => "code_snippets",
142            Self::Skills         => "skills",
143            Self::McpServers     => "mcp_servers",
144            Self::ChatSessions   => "chat_sessions",
145            Self::ModelConfigs   => "model_configs",
146        }
147    }
148
149    pub fn required_permission(&self) -> Permission {
150        Permission::DatabaseRead(self.as_str().to_string())
151    }
152}
153
154// ─── Event bus ────────────────────────────────────────────────────────────────
155
156pub trait EventBus: Send + Sync {
157    /// Emit a plugin-scoped event.
158    /// Full event name on the wire: `plugin:{plugin_id}:{event}`
159    fn emit(&self, event: &str, payload: serde_json::Value) -> Result<(), PluginError>;
160
161    /// Subscribe to a well-known app event.
162    /// Returns a token for unsubscribing.
163    fn subscribe(
164        &self,
165        event: AppEvent,
166        handler: Box<dyn Fn(serde_json::Value) + Send + Sync>,
167    ) -> SubscriptionToken;
168
169    fn unsubscribe(&self, token: SubscriptionToken);
170}
171
172/// Well-known app events plugins can subscribe to.
173#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
174#[serde(tag = "type", rename_all = "snake_case")]
175pub enum AppEvent {
176    AppStarted,
177    AppShuttingDown,
178    ThemeChanged,
179    WorkflowStarted    { workflow_id: String },
180    WorkflowCompleted  { workflow_id: String, success: bool },
181    WorkflowStepCompleted { workflow_id: String, step_index: usize },
182    ProfileLaunched    { profile_id: String },
183    ProfileStopped     { profile_id: String },
184    ChatMessageSent    { session_id: String },
185    ChatStreamCompleted{ session_id: String },
186    SettingsChanged,
187    Custom             { name: String },
188}
189
190// ─── HTTP client ──────────────────────────────────────────────────────────────
191
192pub trait HttpClient: Send + Sync {
193    fn get(
194        &self,
195        url: &str,
196        headers: Option<std::collections::HashMap<String, String>>,
197    ) -> Result<HttpResponse, PluginError>;
198
199    fn post(
200        &self,
201        url: &str,
202        body: serde_json::Value,
203        headers: Option<std::collections::HashMap<String, String>>,
204    ) -> Result<HttpResponse, PluginError>;
205}
206
207#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
208pub struct HttpResponse {
209    pub status: u16,
210    pub headers: std::collections::HashMap<String, String>,
211    pub body: serde_json::Value,
212}
213
214// ─── Filesystem ───────────────────────────────────────────────────────────────
215
216pub trait PluginFs: Send + Sync {
217    fn read_file(&self, path: &std::path::Path) -> Result<Vec<u8>, PluginError>;
218    fn write_file(&self, path: &std::path::Path, content: &[u8]) -> Result<(), PluginError>;
219    fn read_dir(&self, path: &std::path::Path) -> Result<Vec<FsEntry>, PluginError>;
220    fn exists(&self, path: &std::path::Path) -> bool;
221    fn create_dir_all(&self, path: &std::path::Path) -> Result<(), PluginError>;
222    fn remove_file(&self, path: &std::path::Path) -> Result<(), PluginError>;
223    fn remove_dir_all(&self, path: &std::path::Path) -> Result<(), PluginError>;
224}
225
226#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
227pub struct FsEntry {
228    pub path: std::path::PathBuf,
229    pub is_dir: bool,
230    pub size: Option<u64>,
231}
232
233// ─── Process runner ───────────────────────────────────────────────────────────
234
235pub trait ProcessRunner: Send + Sync {
236    /// Run a whitelisted executable and wait for it to finish.
237    fn run(
238        &self,
239        executable: &str,
240        args: &[&str],
241        cwd: Option<&std::path::Path>,
242    ) -> Result<ProcessOutput, PluginError>;
243}
244
245#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
246pub struct ProcessOutput {
247    pub exit_code: i32,
248    pub stdout: String,
249    pub stderr: String,
250}
251
252// ─── IPC registrar ────────────────────────────────────────────────────────────
253
254/// Handler type for plugin IPC commands.
255/// Takes (args JSON, plugin context), returns result JSON.
256pub type IpcHandler = Box<
257    dyn Fn(serde_json::Value, &dyn PluginContext) -> Result<serde_json::Value, PluginError>
258        + Send
259        + Sync,
260>;
261
262pub trait IpcRegistrar: Send + Sync {
263    /// Register a command callable from the frontend.
264    /// On the wire the command name is prefixed: `plugin_{plugin_id}_{name}`
265    fn register(&mut self, name: &str, handler: IpcHandler) -> Result<(), PluginError>;
266
267    /// Register a workflow step type (Level 4).
268    fn register_workflow_step_type(
269        &mut self,
270        definition: WorkflowStepTypeDefinition,
271    ) -> Result<(), PluginError>;
272}
273
274// ─── Entry-point macro ───────────────────────────────────────────────────────
275
276/// Every native plugin crate must call this macro exactly once.
277///
278/// # Example
279/// ```rust
280/// declare_plugin!(MyPlugin, MyPlugin::new);
281/// ```
282#[macro_export]
283macro_rules! declare_plugin {
284    ($plugin_type:ty, $constructor:path) => {
285        #[no_mangle]
286        pub extern "C" fn _haloforge_plugin_create() -> *mut dyn $crate::HaloForgePlugin {
287            let plugin: $plugin_type = $constructor();
288            Box::into_raw(Box::new(plugin))
289        }
290
291        #[no_mangle]
292        pub extern "C" fn _haloforge_plugin_destroy(ptr: *mut dyn $crate::HaloForgePlugin) {
293            if !ptr.is_null() {
294                unsafe { drop(Box::from_raw(ptr)); }
295            }
296        }
297
298        #[no_mangle]
299        pub extern "C" fn _haloforge_abi_version() -> u32 {
300            $crate::PLUGIN_ABI_VERSION
301        }
302    };
303}