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