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