use super::OrcsApp;
use crate::AppError;
use mlua::Lua;
use orcs_component::{ChildConfig, Component, RunnableChild, SpawnError};
use orcs_lua::{LuaChild, ScriptLoader};
use orcs_runtime::auth::{DefaultGrantStore, DefaultPolicy, GrantPolicy, PermissionChecker};
use orcs_runtime::session::SessionStore;
use orcs_runtime::LuaChildLoader;
use orcs_runtime::{
ChannelConfig, ConfigResolver, IOPort, LocalFileStore, OrcsEngine, Session, SessionAsset, World,
};
use orcs_types::{Principal, PrincipalId};
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
struct AppLuaChildLoader {
sandbox: Arc<dyn orcs_runtime::sandbox::SandboxPolicy>,
}
impl LuaChildLoader for AppLuaChildLoader {
fn load(&self, config: &ChildConfig) -> Result<Box<dyn RunnableChild>, SpawnError> {
let script = config
.script_inline
.as_ref()
.ok_or_else(|| SpawnError::Internal("no script_inline in config".into()))?;
let lua = Lua::new();
let lua_arc = Arc::new(Mutex::new(lua));
let child = LuaChild::from_script(lua_arc, script, Arc::clone(&self.sandbox))
.map_err(|e| SpawnError::Internal(format!("failed to load Lua child: {}", e)))?;
Ok(Box::new(child))
}
}
pub struct OrcsAppBuilder {
resolver: Box<dyn ConfigResolver>,
resume_session_id: Option<String>,
sandbox: Option<Arc<dyn orcs_runtime::sandbox::SandboxPolicy>>,
printer_slot: crate::SharedPrinterSlot,
}
impl OrcsAppBuilder {
#[must_use]
pub fn new(resolver: impl ConfigResolver + 'static) -> Self {
Self {
resolver: Box::new(resolver),
resume_session_id: None,
sandbox: None,
printer_slot: crate::SharedPrinterSlot::new(),
}
}
#[must_use]
pub fn with_printer_slot(mut self, slot: crate::SharedPrinterSlot) -> Self {
self.printer_slot = slot;
self
}
#[must_use]
pub fn with_sandbox(mut self, sandbox: Arc<dyn orcs_runtime::sandbox::SandboxPolicy>) -> Self {
self.sandbox = Some(sandbox);
self
}
#[must_use]
pub fn resume(mut self, session_id: impl Into<String>) -> Self {
self.resume_session_id = Some(session_id.into());
self
}
pub async fn build(self) -> Result<OrcsApp, AppError> {
let config = self
.resolver
.resolve()
.map_err(|e| AppError::Config(e.to_string()))?;
let sandbox = self.sandbox.ok_or_else(|| {
AppError::Config("sandbox policy not set (use .with_sandbox())".into())
})?;
let session_path = config.paths.session_dir_or_default();
let store = LocalFileStore::new(session_path)
.map_err(|e| AppError::Config(format!("Failed to create session store: {e}")))?;
let builtins_base = config.components.resolved_builtins_dir();
crate::builtins::ensure_expanded(&builtins_base)
.map_err(|e| AppError::Config(format!("Failed to expand builtins: {e}")))?;
let versioned_builtins = crate::builtins::versioned_dir(&builtins_base);
let component_loader = ScriptLoader::new(Arc::clone(&sandbox))
.with_paths(config.components.resolved_paths())
.with_path(&versioned_builtins);
let component_names = &config.components.load;
let mut world = World::new();
let io = world.create_channel(ChannelConfig::interactive());
let builtin_channels: Vec<_> = component_names
.iter()
.map(|_| world.create_channel(ChannelConfig::default()))
.collect();
let mut engine = OrcsEngine::new(world, io);
if !config.hooks.hooks.is_empty() {
let registry = orcs_runtime::shared_hook_registry();
let result = orcs_lua::hook_helpers::load_hooks_from_config(
&config.hooks,
®istry,
sandbox.root(),
);
for err in &result.errors {
tracing::warn!(hook = %err.hook_label, error = %err.error, "Hook load failed");
}
if result.loaded > 0 {
tracing::info!(
loaded = result.loaded,
skipped = result.skipped,
errors = result.errors.len(),
"Hooks initialized"
);
engine.set_hook_registry(registry);
}
}
if !config.mcp.is_empty() {
let manager = std::sync::Arc::new(orcs_mcp::McpClientManager::new(config.mcp.clone()));
let defs = manager.connect_all().await;
if !defs.is_empty() {
tracing::info!(
tool_count = defs.len(),
"MCP servers connected, {} tool(s) discovered",
defs.len()
);
}
engine.set_mcp_manager(manager);
}
let principal = Principal::User(PrincipalId::new());
let non_elevated_session: Arc<Session> = Arc::new(Session::new(principal.clone()));
let auth_checker: Arc<dyn PermissionChecker> = Arc::new(DefaultPolicy);
if config.scripts.auto_load {
let script_dirs = config.scripts.resolve_dirs(Some(sandbox.root()));
if !script_dirs.is_empty() {
let script_loader = ScriptLoader::new(Arc::clone(&sandbox)).with_paths(script_dirs);
let result = script_loader.load_all();
let loaded_count = result.loaded_count();
let warning_count = result.warning_count();
for warn in &result.warnings {
tracing::warn!(
path = %warn.path.display(),
error = %warn.error,
"Failed to load user script"
);
}
for (name, component) in result.loaded {
let world_ref = engine.world_read();
let channel_id = {
let mut w = world_ref.blocking_write();
w.create_channel(ChannelConfig::default())
};
let component_id = component.id().clone();
let user_grants: Arc<dyn GrantPolicy> = Arc::new(DefaultGrantStore::new());
engine.spawn_runner_full_auth(
channel_id,
Box::new(component),
None,
None,
None,
Arc::clone(&non_elevated_session),
Arc::clone(&auth_checker),
user_grants,
);
tracing::info!(
name = %name,
channel = %channel_id,
component = %component_id.fqn(),
"User script loaded"
);
}
if loaded_count > 0 {
tracing::info!(
"Loaded {} user script(s), {} warning(s)",
loaded_count,
warning_count
);
}
}
}
let (io_port, io_input, io_output) = IOPort::with_defaults(io);
let (_io_handle, io_event_tx) = engine.spawn_client_runner(io, io_port, principal.clone());
tracing::info!("ClientRunner spawned: channel={} (IO bridge)", io);
let elevated_session: Arc<Session> =
Arc::new(Session::new(principal.clone()).elevate(std::time::Duration::from_secs(3600)));
let shared_grants = Arc::new(DefaultGrantStore::new());
tracing::info!(
"Auth context created: principal={:?}, elevated={}",
elevated_session.principal(),
elevated_session.is_elevated()
);
let (session_asset, is_resumed, restored_count) =
if let Some(session_id) = &self.resume_session_id {
tracing::info!("Resuming session: {}", session_id);
let asset = store.load(session_id).await?;
let count = asset.component_snapshots.len();
let saved_grants = asset.granted_commands();
if !saved_grants.is_empty() {
match shared_grants.restore_grants(saved_grants) {
Ok(()) => {
tracing::info!("Restored {} grant(s) from session", saved_grants.len());
}
Err(e) => {
tracing::error!("Failed to restore grants: {e}");
}
}
}
(asset, true, count)
} else {
(SessionAsset::new(), false, 0)
};
let builtins_map: Arc<HashMap<String, String>> = Arc::new(
crate::builtins::FILES
.iter()
.map(|(path, content)| ((*path).to_string(), (*content).to_string()))
.collect(),
);
let mut component_routes = HashMap::new();
for (name, &channel_id) in component_names.iter().zip(builtin_channels.iter()) {
let component = component_loader
.load(name)
.map_err(|e| AppError::Config(format!("Failed to load component '{name}': {e}")))?;
let hints = component.runtime_hints();
let component_id = component.id().clone();
let output_tx = if hints.output_to_io {
Some(io_event_tx.clone())
} else {
None
};
let session = if hints.elevated {
Arc::clone(&elevated_session)
} else {
Arc::clone(&non_elevated_session)
};
let grants: Arc<dyn GrantPolicy> = if hints.elevated {
Arc::clone(&shared_grants) as Arc<dyn GrantPolicy>
} else {
Arc::new(DefaultGrantStore::new())
};
let lua_loader: Option<Arc<dyn LuaChildLoader>> = if hints.child_spawner {
Some(Arc::new(AppLuaChildLoader {
sandbox: Arc::clone(&sandbox),
}))
} else {
None
};
let component_loader: Option<Arc<dyn orcs_component::ComponentLoader>> =
if hints.child_spawner {
Some(Arc::new(
orcs_lua::LuaComponentLoader::new(Arc::clone(&sandbox))
.with_builtins(Arc::clone(&builtins_map)),
))
} else {
None
};
let initial_snapshot = session_asset.get_snapshot(&component_id.fqn()).cloned();
if initial_snapshot.is_some() {
tracing::info!(
component = %component_id.fqn(),
"Restoring snapshot for component"
);
}
let mut component_config = config.components.component_settings(name);
if let serde_json::Value::Object(ref mut map) = component_config {
map.insert("_global".into(), config.global_config_for_lua());
}
engine.spawn_runner_full_auth_with_snapshot(
channel_id,
Box::new(component),
output_tx,
lua_loader,
component_loader,
session,
Arc::clone(&auth_checker),
grants,
initial_snapshot,
component_config,
);
let short_name = component_id.name.clone();
component_routes.insert(short_name, channel_id);
tracing::info!(
channel = %channel_id,
component = %component_id.fqn(),
output_to_io = hints.output_to_io,
elevated = hints.elevated,
child_spawner = hints.child_spawner,
"Spawned component"
);
}
tracing::info!(
"Component routes registered: {:?}",
component_routes.keys().collect::<Vec<_>>()
);
let renderer = if config.ui.verbose {
orcs_runtime::io::ConsoleRenderer::verbose()
} else {
orcs_runtime::io::ConsoleRenderer::new()
};
tracing::debug!("OrcsApp created with config: debug={}", config.debug);
Ok(OrcsApp {
config,
resolver: self.resolver,
engine,
principal,
renderer,
io_input,
io_output,
pending_approval: None,
store,
session: session_asset,
is_resumed,
restored_count,
shared_grants,
component_routes,
printer_slot: self.printer_slot,
})
}
}