pub mod error;
pub mod manifest;
pub mod permissions;
pub mod types;
pub use error::PluginError;
pub use manifest::{
CapabilityLevel, DocumentHandlerConfig, HostCapability, IntegrationConfig, PluginManifest,
WindowPolicyConfig,
};
pub use permissions::Permission;
pub use types::*;
pub const PLUGIN_ABI_VERSION: u32 = 1;
pub const PUBLIC_HOST_API_VERSION: &str = "0.2.16";
pub trait HaloForgePlugin: Send + Sync {
fn metadata(&self) -> PluginMetadata;
fn on_load(
&mut self,
ctx: &dyn PluginContext,
ipc: &mut dyn IpcRegistrar,
) -> Result<(), PluginError>;
fn on_unload(&mut self) -> Result<(), PluginError>;
fn on_settings_changed(&mut self, _settings: serde_json::Value) -> Result<(), PluginError> {
Ok(())
}
fn execute_workflow_step(
&mut self,
_step_type: &str,
_config: serde_json::Value,
_ctx: &dyn PluginContext,
) -> Result<serde_json::Value, PluginError> {
Err(PluginError::Unsupported("execute_workflow_step".into()))
}
}
pub trait PluginContext: Send + Sync {
fn db(&self) -> &dyn DatabaseAccess;
fn events(&self) -> &dyn EventBus;
fn http(&self) -> Option<&dyn HttpClient>;
fn fs(&self) -> Option<&dyn PluginFs>;
fn process(&self) -> Option<&dyn ProcessRunner>;
fn settings(&self) -> serde_json::Value;
fn save_settings(&self, settings: serde_json::Value) -> Result<(), PluginError>;
fn data_dir(&self) -> std::path::PathBuf;
fn log(&self, level: LogLevel, msg: &str);
fn notify(&self, notification: Notification);
}
pub trait DatabaseAccess: Send + Sync {
fn query(
&self,
sql: &str,
params: &[serde_json::Value],
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>, PluginError>;
fn execute(
&self,
sql: &str,
params: &[serde_json::Value],
) -> Result<usize, PluginError>;
fn create_table(&self, table_name: &str, schema_sql: &str) -> Result<(), PluginError>;
fn read_host_table(
&self,
table: HostTable,
limit: Option<u32>,
) -> Result<Vec<serde_json::Value>, PluginError>;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HostTable {
LaunchProfiles,
Workflows,
CodeSnippets,
Skills,
McpServers,
ChatSessions,
ModelConfigs,
}
impl HostTable {
pub fn as_str(&self) -> &'static str {
match self {
Self::LaunchProfiles => "launch_profiles",
Self::Workflows => "workflows",
Self::CodeSnippets => "code_snippets",
Self::Skills => "skills",
Self::McpServers => "mcp_servers",
Self::ChatSessions => "chat_sessions",
Self::ModelConfigs => "model_configs",
}
}
pub fn required_permission(&self) -> Permission {
Permission::DatabaseRead(self.as_str().to_string())
}
}
pub trait EventBus: Send + Sync {
fn emit(&self, event: &str, payload: serde_json::Value) -> Result<(), PluginError>;
fn subscribe(
&self,
event: AppEvent,
handler: Box<dyn Fn(serde_json::Value) + Send + Sync>,
) -> SubscriptionToken;
fn unsubscribe(&self, token: SubscriptionToken);
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AppEvent {
AppStarted,
AppShuttingDown,
ThemeChanged,
WorkflowStarted { workflow_id: String },
WorkflowCompleted { workflow_id: String, success: bool },
WorkflowStepCompleted { workflow_id: String, step_index: usize },
ProfileLaunched { profile_id: String },
ProfileStopped { profile_id: String },
ChatMessageSent { session_id: String },
ChatStreamCompleted{ session_id: String },
SettingsChanged,
Custom { name: String },
}
pub trait HttpClient: Send + Sync {
fn get(
&self,
url: &str,
headers: Option<std::collections::HashMap<String, String>>,
) -> Result<HttpResponse, PluginError>;
fn post(
&self,
url: &str,
body: serde_json::Value,
headers: Option<std::collections::HashMap<String, String>>,
) -> Result<HttpResponse, PluginError>;
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct HttpResponse {
pub status: u16,
pub headers: std::collections::HashMap<String, String>,
pub body: serde_json::Value,
}
pub trait PluginFs: Send + Sync {
fn read_file(&self, path: &std::path::Path) -> Result<Vec<u8>, PluginError>;
fn write_file(&self, path: &std::path::Path, content: &[u8]) -> Result<(), PluginError>;
fn read_dir(&self, path: &std::path::Path) -> Result<Vec<FsEntry>, PluginError>;
fn exists(&self, path: &std::path::Path) -> bool;
fn create_dir_all(&self, path: &std::path::Path) -> Result<(), PluginError>;
fn remove_file(&self, path: &std::path::Path) -> Result<(), PluginError>;
fn remove_dir_all(&self, path: &std::path::Path) -> Result<(), PluginError>;
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FsEntry {
pub path: std::path::PathBuf,
pub is_dir: bool,
pub size: Option<u64>,
}
pub trait ProcessRunner: Send + Sync {
fn run(
&self,
executable: &str,
args: &[&str],
cwd: Option<&std::path::Path>,
) -> Result<ProcessOutput, PluginError>;
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProcessOutput {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
}
pub type IpcHandler = Box<
dyn Fn(serde_json::Value, &dyn PluginContext) -> Result<serde_json::Value, PluginError>
+ Send
+ Sync,
>;
pub trait IpcRegistrar: Send + Sync {
fn register(&mut self, name: &str, handler: IpcHandler) -> Result<(), PluginError>;
fn register_workflow_step_type(
&mut self,
definition: WorkflowStepTypeDefinition,
) -> Result<(), PluginError>;
}
#[macro_export]
macro_rules! declare_plugin {
($plugin_type:ty, $constructor:path) => {
#[no_mangle]
pub extern "C" fn _haloforge_plugin_create() -> *mut dyn $crate::HaloForgePlugin {
let plugin: $plugin_type = $constructor();
Box::into_raw(Box::new(plugin))
}
#[no_mangle]
pub extern "C" fn _haloforge_plugin_destroy(ptr: *mut dyn $crate::HaloForgePlugin) {
if !ptr.is_null() {
unsafe { drop(Box::from_raw(ptr)); }
}
}
#[no_mangle]
pub extern "C" fn _haloforge_abi_version() -> u32 {
$crate::PLUGIN_ABI_VERSION
}
};
}