BREAKING: Restructure from monolithic binary to modular plugin ecosystem Architecture changes: - Convert to Cargo workspace with crates/ directory - Create owlry-plugin-api crate with ABI-stable interface (abi_stable) - Move core binary to crates/owlry/ - Extract providers to native plugin crates (13 plugins) - Add owlry-lua crate for Lua plugin runtime Plugin system: - Plugins loaded from /usr/lib/owlry/plugins/*.so - Widget providers refresh automatically (universal, not hardcoded) - Per-plugin config via [plugins.<name>] sections in config.toml - Backwards compatible with [providers] config format New features: - just install-local: build and install core + all plugins - Plugin config: weather and pomodoro read from [plugins.*] - HostAPI for plugins: notifications, logging Documentation: - Update README with new package structure - Add docs/PLUGINS.md with all plugin documentation - Add docs/PLUGIN_DEVELOPMENT.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
161 lines
4.7 KiB
Rust
161 lines
4.7 KiB
Rust
//! Rune VM runtime creation and sandboxing
|
|
|
|
use rune::{Context, Diagnostics, Source, Sources, Unit, Vm};
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
|
|
use crate::manifest::PluginPermissions;
|
|
|
|
/// Configuration for the Rune sandbox
|
|
///
|
|
/// Some fields are reserved for future sandbox enforcement.
|
|
#[derive(Debug, Clone)]
|
|
#[allow(dead_code)]
|
|
#[derive(Default)]
|
|
pub struct SandboxConfig {
|
|
/// Allow network/HTTP operations
|
|
pub network: bool,
|
|
/// Allow filesystem operations
|
|
pub filesystem: bool,
|
|
/// Allowed filesystem paths (reserved for future sandbox enforcement)
|
|
pub allowed_paths: Vec<String>,
|
|
/// Allow running external commands (reserved for future sandbox enforcement)
|
|
pub run_commands: bool,
|
|
/// Allowed commands (reserved for future sandbox enforcement)
|
|
pub allowed_commands: Vec<String>,
|
|
}
|
|
|
|
|
|
impl SandboxConfig {
|
|
/// Create sandbox config from plugin permissions
|
|
pub fn from_permissions(permissions: &PluginPermissions) -> Self {
|
|
Self {
|
|
network: permissions.network,
|
|
filesystem: !permissions.filesystem.is_empty(),
|
|
allowed_paths: permissions.filesystem.clone(),
|
|
run_commands: !permissions.run_commands.is_empty(),
|
|
allowed_commands: permissions.run_commands.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a Rune context with owlry API modules
|
|
pub fn create_context(sandbox: &SandboxConfig) -> Result<Context, rune::ContextError> {
|
|
let mut context = Context::with_default_modules()?;
|
|
|
|
// Add standard modules based on permissions
|
|
if sandbox.network {
|
|
log::debug!("Network access enabled for Rune plugin");
|
|
}
|
|
|
|
if sandbox.filesystem {
|
|
log::debug!("Filesystem access enabled for Rune plugin");
|
|
}
|
|
|
|
// Add owlry API module
|
|
context.install(crate::api::module()?)?;
|
|
|
|
Ok(context)
|
|
}
|
|
|
|
/// Compile Rune source code into a Unit
|
|
pub fn compile_source(
|
|
context: &Context,
|
|
source_path: &Path,
|
|
) -> Result<Arc<Unit>, CompileError> {
|
|
let source_content = std::fs::read_to_string(source_path)
|
|
.map_err(|e| CompileError::Io(e.to_string()))?;
|
|
|
|
let source_name = source_path
|
|
.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.unwrap_or("init.rn");
|
|
|
|
let mut sources = Sources::new();
|
|
sources
|
|
.insert(Source::new(source_name, &source_content).map_err(|e| CompileError::Compile(e.to_string()))?)
|
|
.map_err(|e| CompileError::Compile(format!("Failed to insert source: {}", e)))?;
|
|
|
|
let mut diagnostics = Diagnostics::new();
|
|
|
|
let result = rune::prepare(&mut sources)
|
|
.with_context(context)
|
|
.with_diagnostics(&mut diagnostics)
|
|
.build();
|
|
|
|
match result {
|
|
Ok(unit) => Ok(Arc::new(unit)),
|
|
Err(e) => {
|
|
// Collect error messages
|
|
let mut error_msg = format!("Compilation failed: {}", e);
|
|
for diagnostic in diagnostics.diagnostics() {
|
|
error_msg.push_str(&format!("\n {:?}", diagnostic));
|
|
}
|
|
Err(CompileError::Compile(error_msg))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a new Rune VM from compiled unit
|
|
pub fn create_vm(
|
|
context: &Context,
|
|
unit: Arc<Unit>,
|
|
) -> Result<Vm, CompileError> {
|
|
let runtime = Arc::new(
|
|
context.runtime()
|
|
.map_err(|e| CompileError::Compile(format!("Failed to get runtime: {}", e)))?
|
|
);
|
|
Ok(Vm::new(runtime, unit))
|
|
}
|
|
|
|
/// Error type for compilation
|
|
#[derive(Debug)]
|
|
pub enum CompileError {
|
|
Io(String),
|
|
Compile(String),
|
|
}
|
|
|
|
impl std::fmt::Display for CompileError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
CompileError::Io(e) => write!(f, "IO error: {}", e),
|
|
CompileError::Compile(e) => write!(f, "Compile error: {}", e),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_sandbox_config_default() {
|
|
let config = SandboxConfig::default();
|
|
assert!(!config.network);
|
|
assert!(!config.filesystem);
|
|
assert!(!config.run_commands);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sandbox_from_permissions() {
|
|
let permissions = PluginPermissions {
|
|
network: true,
|
|
filesystem: vec!["~/.config".to_string()],
|
|
run_commands: vec!["notify-send".to_string()],
|
|
};
|
|
let config = SandboxConfig::from_permissions(&permissions);
|
|
assert!(config.network);
|
|
assert!(config.filesystem);
|
|
assert!(config.run_commands);
|
|
assert_eq!(config.allowed_paths, vec!["~/.config"]);
|
|
assert_eq!(config.allowed_commands, vec!["notify-send"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_context() {
|
|
let config = SandboxConfig::default();
|
|
let context = create_context(&config);
|
|
assert!(context.is_ok());
|
|
}
|
|
}
|