1use anyhow::{bail, Context};
4use rand::RngCore;
5use crate::api::build_router;
6use crate::api::openai_compat::AppState;
7use crate::app::{
8 build_capability_registry, instance_config_path, instance_lock_path, load_instance_config,
9 load_instance_config_from_path, load_instance_lock_from_path, load_product_package_declaration,
10 merge_core_capabilities, product_package_declaration_path,
11 resolve_product_package_declaration_with_policy_and_candidate_context, ResolvedApp,
12 ResolvedAppStatus, ResolvedInstanceMap, ResolvedInstanceSources, ServiceOriginCandidatePayload,
13};
14use crate::config::store::load_config_or_default;
15use crate::defaults::*;
16use crate::package::bridge::{PackageLoadInfo, WasmHandle, WasmHostState, WasmPackageHost};
17use crate::package::{
18 build_service_config, discover_runtime_packages, is_managed_runtime_root,
19 merged_package_aliases, native_library_candidates, resolve_wasm_startup_mode, NativeHandle,
20 NativePackageHost, NativePackageLoadInfo, PackageInfo, PackageManager, PackageRuntime,
21};
22use crate::pipeline::Pipeline;
23use crate::process::ProcessManager;
24use crate::vkeys::VirtualKeyStore;
25use std::collections::{HashMap, HashSet};
26use std::path::{Path, PathBuf};
27use std::sync::{Arc, Mutex as StdMutex};
28use std::time::Duration;
29use tokio::sync::{RwLock, Semaphore};
30use tracing_subscriber::EnvFilter;
31
32const ORCHESTRATOR_DISPATCH_CONCURRENCY: usize = 4;
33
34
35fn resolved_app_instance_dir(app: &ResolvedApp) -> Option<PathBuf> {
36 app.sources
37 .lock_path
38 .as_deref()
39 .and_then(|path| Path::new(path).parent().map(Path::to_path_buf))
40 .or_else(|| {
41 app.sources
42 .config_path
43 .as_deref()
44 .and_then(|path| Path::new(path).parent().map(Path::to_path_buf))
45 })
46}
47
48fn log_startup_generation_store_diagnostics(
49 app: &ResolvedApp,
50 store: &crate::app::AppGenerationStore,
51) {
52 let Some(instance_dir) = resolved_app_instance_dir(app) else {
53 return;
54 };
55
56 let diagnostics = crate::app::inspect_startup_generation_store(&instance_dir, store);
57 if diagnostics.is_clean() {
58 tracing::debug!(
59 app = %app.name,
60 instance_dir = %instance_dir.display(),
61 "Startup generation store diagnostics are clean"
62 );
63 return;
64 }
65
66 for diagnostic in diagnostics.diagnostics {
67 tracing::warn!(
68 app = %app.name,
69 instance_dir = %instance_dir.display(),
70 code = %diagnostic.code,
71 pointer = diagnostic.pointer.as_deref().unwrap_or("-"),
72 generation_id = diagnostic.generation_id.unwrap_or_default(),
73 "{}",
74 diagnostic.message
75 );
76 }
77}
78
79fn lock_down_runtime_token_permissions(_path: &Path) {
80 #[cfg(unix)]
81 {
82 use std::os::unix::fs::PermissionsExt;
83 let _ = std::fs::set_permissions(_path, std::fs::Permissions::from_mode(0o600));
84 }
85}
86
87fn ensure_runtime_token(data_dir: &Path) -> anyhow::Result<(String, PathBuf)> {
88 let token_path = data_dir.join("runtime-token");
89 if token_path.exists() {
90 let token = std::fs::read_to_string(&token_path)
91 .with_context(|| format!("failed to read runtime token from {}", token_path.display()))?
92 .trim()
93 .to_string();
94 if token.is_empty() {
95 bail!("runtime token file {} is empty", token_path.display());
96 }
97 lock_down_runtime_token_permissions(&token_path);
98 return Ok((token, token_path));
99 }
100
101 std::fs::create_dir_all(data_dir)
102 .with_context(|| format!("failed to create data dir {}", data_dir.display()))?;
103
104 let mut token_bytes = [0_u8; 32];
105 rand::thread_rng().fill_bytes(&mut token_bytes);
106 let token = token_bytes
107 .iter()
108 .map(|byte| format!("{byte:02x}"))
109 .collect::<String>();
110
111 std::fs::write(&token_path, format!("{token}\n"))
112 .with_context(|| format!("failed to write runtime token to {}", token_path.display()))?;
113 lock_down_runtime_token_permissions(&token_path);
114
115 Ok((token, token_path))
116}
117
118fn resolve_data_dir(repo_root: &Path, configured_data_dir: &str) -> PathBuf {
119 let candidate = PathBuf::from(configured_data_dir);
120 if candidate.is_absolute() {
121 candidate
122 } else {
123 repo_root.join(candidate)
124 }
125}
126
127fn prebuilt_core_capability_registry() -> crate::app::CapabilityRegistry {
128 let mut registry = crate::app::CapabilityRegistry::default();
129 merge_core_capabilities(&mut registry);
130 registry
131}
132
133fn resolve_capability_provider(
138 registry: &crate::app::CapabilityRegistry,
139 capability: &str,
140) -> Option<String> {
141 registry
142 .get(capability)
143 .and_then(|entry| entry.providers.iter().max_by_key(|p| p.priority))
144 .map(|p| p.provider.clone())
145}
146
147pub fn discover_product_roots(repo_root: &Path) -> Vec<PathBuf> {
148 let mut roots = Vec::new();
149 let mut seen = HashSet::new();
150 let active_instance_names = discover_instance_names(repo_root);
151
152 let packages_dir = repo_root.join("packages");
153 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
154 for entry in entries.flatten() {
155 let path = entry.path();
156 if !path.is_dir() || !path.join("package.toml").exists() {
157 continue;
158 }
159 let name = path
160 .file_name()
161 .and_then(|value| value.to_str())
162 .unwrap_or("");
163 if name.is_empty() || !seen.insert(name.to_string()) {
164 continue;
165 }
166 if !active_instance_names.is_empty() && !active_instance_names.contains(name) {
167 continue;
168 }
169 roots.push(path);
170 }
171 }
172
173 roots
174}
175
176fn discover_instance_names(repo_root: &Path) -> HashSet<String> {
177 let mut names = HashSet::new();
178 let instances_dir = repo_root.join(".weft");
179 if let Ok(entries) = std::fs::read_dir(&instances_dir) {
180 for entry in entries.flatten() {
181 let path = entry.path();
182 if !path.is_dir() || !path.join("config.toml").exists() {
183 continue;
184 }
185 if let Some(name) = path.file_name().and_then(|value| value.to_str()) {
186 names.insert(name.to_string());
187 }
188 }
189 }
190 names
191}
192
193fn lock_bindings_to_app_bindings(
194 lock: &crate::app::InstanceLock,
195) -> Vec<crate::app::AppBindingResolution> {
196 lock.bindings
197 .iter()
198 .filter(|binding| !binding.capability.trim().is_empty())
199 .map(|binding| crate::app::AppBindingResolution {
200 capability: binding.capability.clone(),
201 provider: binding.provider.clone(),
202 mutable: binding.mutable,
203 source: if binding.binding_source.trim().is_empty() {
204 "lock".into()
205 } else {
206 binding.binding_source.clone()
207 },
208 })
209 .collect()
210}
211
212pub fn build_generation_store_map(
213 resolved_apps: &ResolvedInstanceMap,
214) -> crate::app::GenerationStoreMap {
215 let mut generation_store_map = crate::app::GenerationStoreMap::default();
216 for app in resolved_apps.values() {
217 if let Some(ref lock_path) = app.sources.lock_path {
218 if let Ok(lock) = load_instance_lock_from_path(std::path::Path::new(lock_path)) {
219 let lock_bindings = lock_bindings_to_app_bindings(&lock);
220 let active_generation = crate::app::AppGeneration {
221 id: lock.generation as u64,
222 app_name: app.name.clone(),
223 version: app.version.clone(),
224 bindings: if lock_bindings.is_empty() {
225 app.bindings.clone()
226 } else {
227 lock_bindings
228 },
229 capabilities: app.capabilities.clone(),
230 enabled_features: lock.assembly.enabled_features.clone(),
231 scene: lock.scene.clone(),
232 profile: lock.profile.clone(),
233 binding_set_id: lock.binding_set_id.clone(),
234 closure_id: lock.closure_id.clone(),
235 lock_digest: String::new(),
236 lock_path: lock_path.clone(),
237 parent_generation: None,
238 created_by: String::new(),
239 status: crate::app::GenerationStatus::Active,
240 validation_results: vec![],
241 created_at: 0,
242 };
243 let store = crate::app::AppGenerationStore {
244 active: Some(active_generation),
245 candidate: None,
246 rollback: None,
247 next_id: lock.generation as u64 + 1,
248 };
249 log_startup_generation_store_diagnostics(app, &store);
250 generation_store_map.insert(app.name.clone(), store);
251 }
252 }
253 }
254 generation_store_map
255}
256
257#[cfg(windows)]
259fn is_process_alive(pid: u32) -> bool {
260 use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
261 use windows_sys::Win32::Foundation::CloseHandle;
262 unsafe {
263 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
264 if handle.is_null() {
265 return false;
266 }
267 let mut exit_code: u32 = 0;
270 let still_active = if windows_sys::Win32::System::Threading::GetExitCodeProcess(
271 handle,
272 &mut exit_code as *mut u32,
273 ) != 0
274 {
275 exit_code == 259 } else {
277 false
278 };
279 CloseHandle(handle);
280 still_active
281 }
282}
283
284#[cfg(not(windows))]
285fn is_process_alive(pid: u32) -> bool {
286 unsafe { libc::kill(pid as i32, 0) == 0 }
288}
289
290
291pub async fn run_server() -> anyhow::Result<()> {
292 tracing_subscriber::fmt()
293 .with_env_filter(EnvFilter::from_default_env().add_directive("weft_core=info".parse()?))
294 .try_init().ok();
295
296 let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
299 let repo_source_root = crate_root
300 .parent()
301 .map(Path::to_path_buf)
302 .unwrap_or_else(|| crate_root.clone());
303 let current_dir = std::env::current_dir().unwrap_or_else(|_| repo_source_root.clone());
304 let managed_runtime = is_managed_runtime_root(¤t_dir);
305 let repo_root = if managed_runtime {
306 current_dir.clone()
307 } else if current_dir.join("packages").join("index.toml").is_file() {
308 current_dir.clone()
314 } else {
315 repo_source_root.clone()
316 };
317 let mut config_path = if managed_runtime {
318 repo_root.join("config").join("config.toml")
319 } else {
320 [
321 repo_root.join("config").join("config.toml"),
322 crate_root.join("config").join("config.toml"),
323 ]
324 .into_iter()
325 .find(|path| path.exists())
326 .unwrap_or_else(|| repo_root.join("config").join("config.toml"))
327 };
328 let mut config = load_config_or_default(&config_path);
329
330 {
333 let args: Vec<String> = std::env::args().skip(1).collect();
334 let mut i = 0;
335 while i < args.len() {
336 match args[i].as_str() {
337 "--config-dir" => {
338 if let Some(val) = args.get(i + 1) {
339 let override_path = PathBuf::from(val).join("config.toml");
342 if override_path.exists() {
343 config = load_config_or_default(&override_path);
344 }
345 config_path = override_path;
346 i += 2;
347 } else {
348 i += 1;
349 }
350 }
351 "--data-dir" => {
352 if let Some(val) = args.get(i + 1) {
353 config.core.data_dir = val.clone();
354 i += 2;
355 } else {
356 i += 1;
357 }
358 }
359 "--port" => {
360 if let Some(val) = args.get(i + 1) {
361 if let Ok(port) = val.parse::<u16>() {
362 config.core.port = port;
363 }
364 i += 2;
365 } else {
366 i += 1;
367 }
368 }
369 "--parent-pid" => {
370 if let Some(val) = args.get(i + 1) {
371 if let Ok(ppid) = val.parse::<u32>() {
372 std::thread::spawn(move || {
374 loop {
375 std::thread::sleep(std::time::Duration::from_secs(2));
376 if !is_process_alive(ppid) {
377 tracing::info!("Parent process (pid {}) exited, shutting down", ppid);
378 std::process::exit(0);
379 }
380 }
381 });
382 }
383 i += 2;
384 } else {
385 i += 1;
386 }
387 }
388 _ => { i += 1; }
389 }
390 }
391 }
392
393 if let Ok(ffi_config_dir) = std::env::var("WEFT_FFI_CONFIG_DIR") {
397 if !ffi_config_dir.is_empty() {
398 let override_path = PathBuf::from(&ffi_config_dir).join("config.toml");
399 if override_path.exists() {
400 config = load_config_or_default(&override_path);
401 }
402 config_path = override_path;
403 }
404 }
405 if let Ok(ffi_data_dir) = std::env::var("WEFT_FFI_DATA_DIR") {
406 if !ffi_data_dir.is_empty() {
407 config.core.data_dir = ffi_data_dir;
408 }
409 }
410
411 config.web_search.apply_to_env();
414
415 let default_provider = config
416 .routing
417 .default_provider
418 .clone()
419 .unwrap_or_else(|| "openrouter".into());
420 let data_dir = resolve_data_dir(&repo_root, &config.core.data_dir);
421
422 if std::env::var("EXTISM_CACHE_CONFIG").is_err() {
427 let cache_dir = data_dir.join("wasm-cache");
428 let _ = std::fs::create_dir_all(&cache_dir);
429 let cache_config_path = data_dir.join("wasmtime-cache.toml");
430 let cache_config_content = format!(
432 "[cache]\ndirectory = \"{}\"\n",
433 cache_dir.to_string_lossy().replace('\\', "/")
434 );
435 if std::fs::write(&cache_config_path, &cache_config_content).is_ok() {
436 std::env::set_var("EXTISM_CACHE_CONFIG", cache_config_path.to_string_lossy().as_ref());
437 tracing::info!(
438 cache_config = %cache_config_path.display(),
439 "WASM module cache enabled"
440 );
441 }
442 }
443
444 let (runtime_token, runtime_token_path) = ensure_runtime_token(&data_dir)?;
445 tracing::info!(
446 data_dir = %data_dir.display(),
447 runtime_token_path = %runtime_token_path.display(),
448 "Runtime loopback token ready"
449 );
450
451 let addr = format!("{}:{}", config.core.host, config.core.port);
452 tracing::info!("weft-core starting on {}", addr);
453 let shared_config = Arc::new(RwLock::new(config.clone()));
454 let shared_pipeline = Arc::new(Pipeline {
455 router: Arc::new(DefaultRouter {
456 default_provider: default_provider.clone(),
457 }),
458 key_selector: Arc::new(FailoverSelector),
459 transforms: Arc::new(crate::defaults::transforms::TransformRegistry::with_defaults()),
460 error_handler: Arc::new(DefaultErrorHandler {
461 max_retries: config.fallback.retry_count,
462 }),
463 http_client: reqwest::Client::builder()
464 .connect_timeout(Duration::from_secs(10))
465 .timeout(Duration::from_secs(90))
466 .http1_only()
467 .build()?,
468 });
469
470 let process_manager = Arc::new(ProcessManager::new());
472 for svc_config in &config.services {
473 process_manager.register(svc_config.clone()).await;
474 }
475
476 let vkey_store = Arc::new(VirtualKeyStore::new());
478 vkey_store.load_from_config(&config.virtual_keys);
479
480 let prebuilt_core_registry = prebuilt_core_capability_registry();
481 let package_service_client = crate::app::ReqwestPackageServiceClient::new();
482 let package_index = crate::app::resolve_package_index_with_client(
483 &repo_root,
484 &config.core.data_dir,
485 config.registry.package_source_url.as_deref(),
486 &package_service_client,
487 )
488 .await;
489 let service_origin_payload = ServiceOriginCandidatePayload::from_package_index(&package_index);
490 let resolver_input =
491 crate::app::ResolveInputCoordinator::from_package_index(&package_index)
492 .with_service_origin_candidates(
493 &crate::app::SynthesizedServiceOriginCandidateAdapter,
494 &service_origin_payload,
495 )
496 .build();
497 let runtime_package_aliases = merged_package_aliases(&package_index, &config.package_aliases);
498
499 let mut package_manager = PackageManager::new();
501 let discovered_packages = discover_runtime_packages(&repo_root, &package_index);
502
503 for unmet in package_index.unmet_requirements() {
508 tracing::warn!(
509 "Unmet requirement: package '{}' requires capability '{}' but no registered source provides it",
510 unmet.package,
511 unmet.capability
512 );
513 }
514 for package in &discovered_packages {
515 let manifest = &package.manifest;
516 package_manager.register(PackageInfo {
517 name: manifest.package_info.name.clone(),
518 version: Some(manifest.package_info.version.clone()),
519 overrides: vec![],
520 enabled: true,
521 has_ui: false,
522 description: Some(manifest.package_info.description.clone()),
523 });
524 }
525 tracing::info!("Discovered {} plugins", discovered_packages.len());
526
527 let chat_providers: Vec<crate::api::openai_compat::ChatProviderInfo> = discovered_packages
529 .iter()
530 .filter(|package| {
531 package
532 .manifest
533 .resolved_provides()
534 .contains(&"chat_channel".to_string())
535 })
536 .map(|package| crate::api::openai_compat::ChatProviderInfo {
537 name: package.manifest.package_info.name.clone(),
538 endpoint: package
539 .manifest
540 .resolved_chat_endpoint()
541 .unwrap_or_else(|| "/chat".to_string()),
542 description: package.manifest.package_info.description.clone(),
543 })
544 .collect();
545 let mut chat_providers = chat_providers;
546 chat_providers.sort_by(|left, right| left.name.cmp(&right.name));
547 chat_providers
548 .dedup_by(|left, right| left.name == right.name && left.endpoint == right.endpoint);
549 tracing::info!("Registered {} chat providers", chat_providers.len());
550
551 for package in &discovered_packages {
552 match package.runtime {
553 PackageRuntime::Service => match build_service_config(package) {
554 Ok(service_config) => {
555 tracing::info!(
556 "Registering service runtime package '{}' with process manager",
557 package.manifest.package_info.name
558 );
559 process_manager.register(service_config).await;
560 }
561 Err(error) => {
562 tracing::warn!(
563 "Failed to register service runtime package '{}': {}",
564 package.manifest.package_info.name,
565 error
566 );
567 }
568 },
569 PackageRuntime::Remote | PackageRuntime::Unknown(_) => {
570 tracing::warn!(
571 "Package '{}' declares unsupported runtime '{}' - manifest is loaded but runtime will not start",
572 package.manifest.package_info.name,
573 package.runtime.as_str()
574 );
575 }
576 PackageRuntime::Wasm => {}
577 PackageRuntime::Native => {
578 tracing::info!(
579 "Package '{}' declares native runtime - registered for future native loading",
580 package.manifest.package_info.name
581 );
582 }
583 }
584 }
585 let mut resolved_apps: ResolvedInstanceMap = HashMap::new();
586
587 for product_root in discover_product_roots(&repo_root) {
588 match load_product_package_declaration(&product_root) {
589 Ok(manifest) => {
590 let app_config = load_instance_config(&product_root).unwrap_or_default();
591 let (enabled_features, _, config_overrides) =
592 crate::api::generations::preview_runtime_inputs(
593 &manifest,
594 &app_config,
595 &manifest
596 .flattened_bindings()
597 .iter()
598 .map(
599 |(capability, binding)| crate::app::AppBindingResolution {
600 capability: capability.clone(),
601 provider: binding.provider.clone(),
602 mutable: binding.mutable,
603 source: "declaration-default".into(),
604 },
605 )
606 .collect::<Vec<_>>(),
607 );
608 match resolve_product_package_declaration_with_policy_and_candidate_context(
609 &manifest,
610 &discovered_packages,
611 None,
612 None,
613 Some(&prebuilt_core_registry),
614 Some(&package_index),
615 resolver_input.resolve_candidate_context(),
616 ) {
617 Ok(mut resolved) => {
618 let allowed_capabilities =
619 crate::api::generations::preview_runtime_capabilities(
620 &manifest,
621 &enabled_features,
622 );
623 let runtime_bindings =
624 crate::api::generations::preview_runtime_bindings_from_manifest(
625 &manifest,
626 &allowed_capabilities,
627 &package_index,
628 &config_overrides,
629 );
630 resolved.capabilities = allowed_capabilities;
631 resolved.enabled_features = enabled_features;
632 resolved.bindings = runtime_bindings;
633 let declaration_path = product_package_declaration_path(&product_root);
634 let instance_config = instance_config_path(&product_root);
635 let instance_lock = instance_lock_path(&product_root);
636 resolved.sources.manifest_path = declaration_path.display().to_string();
637 resolved.sources.config_path = Some(instance_config.display().to_string());
638 resolved.sources.lock_path = Some(instance_lock.display().to_string());
639 resolved.config_path = Some(instance_config.display().to_string());
640 resolved_apps.insert(resolved.name.clone(), resolved);
641 }
642 Err(error) => {
643 let declaration_path = product_package_declaration_path(&product_root);
644 let instance_config = instance_config_path(&product_root);
645 let instance_lock = instance_lock_path(&product_root);
646 let unresolved = ResolvedApp {
647 name: manifest.app.name.clone(),
648 version: manifest.app.version.clone(),
649 display_name: manifest.app.display_name.clone(),
650 description: manifest.app.description.clone(),
651 capabilities:
652 crate::api::generations::preview_runtime_capabilities(
653 &manifest,
654 &enabled_features,
655 ),
656 enabled_features,
657 bindings: vec![],
658 validation_checks: manifest.validation.checks.clone(),
659 config_path: Some(instance_config.display().to_string()),
660 status: ResolvedAppStatus::Unresolved,
661 errors: vec![error.to_string()],
662 sources: ResolvedInstanceSources {
663 manifest_path: declaration_path.display().to_string(),
664 config_path: Some(instance_config.display().to_string()),
665 lock_path: Some(instance_lock.display().to_string()),
666 },
667 };
668 resolved_apps.insert(unresolved.name.clone(), unresolved);
669 }
670 }
671 }
672 Err(error) => {
673 tracing::warn!(
674 "Failed to load product package declaration at {}: {}",
675 product_root.display(),
676 error
677 );
678 }
679 }
680 }
681
682 let mut capability_registry = build_capability_registry(&discovered_packages, &resolved_apps);
683 merge_core_capabilities(&mut capability_registry);
684
685 let active_profile = {
686 let mut profile = crate::app::AppProfile::Safe;
687 for app in resolved_apps.values() {
688 if app.status == ResolvedAppStatus::Resolved {
689 if let Some(ref config_path_str) = app.config_path {
690 if let Ok(app_config) =
691 load_instance_config_from_path(std::path::Path::new(config_path_str))
692 {
693 profile = crate::app::AppProfile::from_str_loose(
694 &app_config.app_runtime.profile,
695 );
696 break;
697 }
698 }
699 }
700 }
701 profile
702 };
703 tracing::info!("Active profile: {}", active_profile.as_str());
704 let core_policy = crate::app::CorePolicy::default_policy();
705 let kv_path = std::path::PathBuf::from("./data/plugin_kv.json");
707 let kv_data: HashMap<String, String> = if kv_path.exists() {
708 match std::fs::read_to_string(&kv_path) {
709 Ok(json) => serde_json::from_str(&json).unwrap_or_default(),
710 Err(e) => {
711 tracing::warn!("Failed to load KV store from {}: {}", kv_path.display(), e);
712 HashMap::new()
713 }
714 }
715 } else {
716 HashMap::new()
717 };
718 let kv_count = kv_data.len();
719 let kv_store: Arc<StdMutex<HashMap<String, String>>> = Arc::new(StdMutex::new(kv_data));
720 if kv_count > 0 {
721 tracing::info!("Loaded {} KV entries from {}", kv_count, kv_path.display());
722 }
723 {
726 let role_routing_json = serde_json::to_string(&config.team.role_routing)
727 .unwrap_or_else(|_| "".to_string());
728 let n = config.team.role_routing.len();
729 kv_store
730 .lock()
731 .unwrap()
732 .insert("team:role_routing".to_string(), role_routing_json);
733 tracing::info!("team role_routing loaded into KV: {} role(s)", n);
734 }
735 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
736
737 let wasm_packages: Vec<_> = discovered_packages
739 .iter()
740 .filter(|package| package.runtime == PackageRuntime::Wasm)
741 .collect();
742 let wasm_handle = if !wasm_packages.is_empty() {
743 let load_infos: Vec<PackageLoadInfo> = wasm_packages
744 .iter()
745 .map(|package| PackageLoadInfo {
746 name: package.manifest.package_info.name.clone(),
747 dir: package.dir.clone(),
748 wasm_path: package.entry_path.clone().expect("wasm package entry"),
749 startup_mode: resolve_wasm_startup_mode(&package.manifest),
750 permissions: package.manifest.permissions.clone(),
751 })
752 .collect();
753
754 let host_state = WasmHostState {
755 config: shared_config.clone(),
756 pipeline: shared_pipeline.clone(),
757 runtime_handle: crate::package::bridge::host_blocking_handle(),
761 process_manager: process_manager.clone(),
762 vkey_store: vkey_store.clone(),
763 kv_store: kv_store.clone(),
764 caller_package_name: String::new(),
765 package_dir: String::new(),
766 permissions: Default::default(),
767 package_map: Arc::new(StdMutex::new(HashMap::new())),
768 package_aliases: Arc::new(StdMutex::new(runtime_package_aliases.clone())),
769 call_depth: Arc::new(StdMutex::new(0)),
770 app_state: Arc::new(StdMutex::new(None)),
771 };
772
773 let host = WasmPackageHost::new(&load_infos, host_state);
774
775 for info in &load_infos {
777 if host.has_package(&info.name) {
778 match host.call(&info.name, "init", "") {
779 Ok(_) => tracing::info!("Package '{}' initialized", info.name),
780 Err(e) => tracing::warn!(
781 "Package '{}' init() failed (may not export it): {}",
782 info.name,
783 e
784 ),
785 }
786 }
787 }
788
789 Some(WasmHandle::new(host))
790 } else {
791 None
792 };
793
794 let native_packages: Vec<_> = discovered_packages
795 .iter()
796 .filter(|package| package.runtime == PackageRuntime::Native)
797 .collect();
798 let native_handle = if !native_packages.is_empty() {
799 let mut host = NativePackageHost::new();
800 for package in native_packages {
801 if let Some(entry_path) = package.entry_path.as_ref() {
802 let candidates = native_library_candidates(entry_path);
803 if let Some(library_path) =
804 candidates.into_iter().find(|candidate| candidate.exists())
805 {
806 let load_info = NativePackageLoadInfo {
807 name: package.manifest.package_info.name.clone(),
808 dir: package.dir.clone(),
809 library_path,
810 };
811 if let Err(error) = host.load_package(&load_info) {
812 tracing::warn!(
813 "Failed to load native package '{}': {}",
814 package.manifest.package_info.name,
815 error
816 );
817 }
818 } else {
819 tracing::warn!(
820 "Native package '{}' discovered but no loadable library was found",
821 package.manifest.package_info.name
822 );
823 }
824 }
825 }
826 Some(NativeHandle::new(host))
827 } else {
828 None
829 };
830
831 let generation_store_map = build_generation_store_map(&resolved_apps);
832
833 let state = AppState {
835 config: shared_config,
836 config_path: config_path.clone(),
837 pipeline: shared_pipeline,
838 process_manager: process_manager.clone(),
839 vkey_store,
840 package_manager: Arc::new(RwLock::new(package_manager)),
841 wasm_handle: Arc::new(RwLock::new(wasm_handle)),
842 native_handle: Arc::new(RwLock::new(native_handle)),
843 resolved_apps: Arc::new(RwLock::new(resolved_apps)),
844 capability_registry: Arc::new(RwLock::new(capability_registry)),
845 active_profile: Arc::new(RwLock::new(active_profile)),
846 core_policy: Arc::new(core_policy),
847 generation_store: Arc::new(RwLock::new(generation_store_map)),
848 package_index: Arc::new(package_index),
849 repo_root: repo_root.clone(),
850 data_dir: data_dir.clone(),
851 runtime_token: Some(runtime_token.clone()),
852 runtime_token_path: Some(runtime_token_path.clone()),
853 chat_providers: Arc::new(RwLock::new(chat_providers)),
854 shutdown_tx: Arc::new(StdMutex::new(Some(shutdown_tx))),
855 stream_buffer: Arc::new(StdMutex::new(std::collections::HashMap::new())),
856 };
857
858 if let Some(handle) = state.wasm_handle.read().await.as_ref() {
859 if let Err(error) = handle.set_app_state(state.clone()) {
860 tracing::warn!("Failed to wire app state into WASM host: {}", error);
861 }
862 }
863
864 if let Some(ref handle) = *state.wasm_handle.read().await {
865 let loaded = handle.package_names();
866 for (alias, target) in &runtime_package_aliases {
867 if loaded.iter().any(|name| name == target) {
868 tracing::info!(
869 "Runtime package alias '{}' resolved to loaded implementation '{}'",
870 alias,
871 target
872 );
873 } else {
874 tracing::warn!(
875 "Runtime package alias '{}' points to '{}' but that implementation is not loaded",
876 alias,
877 target
878 );
879 }
880 }
881 }
882
883 process_manager.start_auto().await;
885
886 {
890 let cron_handle = state.wasm_handle.clone();
891 let cron_registry = state.capability_registry.clone();
892 tokio::spawn(async move {
893 let mut interval = tokio::time::interval(std::time::Duration::from_secs(10));
894 let mut cleanup_counter: u32 = 0;
895 loop {
896 interval.tick().await;
897 let now_ms = std::time::SystemTime::now()
898 .duration_since(std::time::UNIX_EPOCH)
899 .unwrap_or_default()
900 .as_millis() as u64;
901 let tick_input = format!(r#"{{"now_ms":{}}}"#, now_ms);
902
903 let (cron_provider, memory_provider) = {
906 let reg = cron_registry.read().await;
907 (
908 resolve_capability_provider(®, "scheduler.cron"),
909 resolve_capability_provider(®, "memory.store"),
910 )
911 };
912
913 let guard = cron_handle.read().await;
914 if let Some(ref wh) = *guard {
915 if let Some(ref provider) = cron_provider {
917 if wh.has_package(provider) {
918 if let Err(e) = wh.call(provider, "tick", &tick_input) {
919 tracing::debug!("cron tick error: {}", e);
920 }
921 }
922 }
923 cleanup_counter += 1;
925 if cleanup_counter >= 30 {
926 cleanup_counter = 0;
927 if let Some(ref provider) = memory_provider {
928 if wh.has_package(provider) {
929 let cleanup_input =
930 format!(r#"{{"agent":"*","now_ms":{}}}"#, now_ms);
931 let _ = wh.call(provider, "cleanup_expired", &cleanup_input);
932 }
933 }
934 }
935 }
936 }
937 });
938 }
939
940 {
941 let orch_handle = state.wasm_handle.clone();
942 let orch_registry = state.capability_registry.clone();
943 tokio::spawn(async move {
944 let mut interval = tokio::time::interval(std::time::Duration::from_secs(3));
945 loop {
946 interval.tick().await;
947 let now_ms = std::time::SystemTime::now()
948 .duration_since(std::time::UNIX_EPOCH)
949 .unwrap_or_default()
950 .as_millis() as u64;
951 let tick_input = format!(r#"{{"now_ms":{}}}"#, now_ms);
952
953 let orch_provider = {
955 let reg = orch_registry.read().await;
956 resolve_capability_provider(®, "workflow.orchestration")
957 };
958 let Some(orch_provider) = orch_provider else {
959 continue;
960 };
961
962 let wh = orch_handle.read().await.clone();
966 if let Some(wh) = wh {
967 if wh.has_package(&orch_provider) {
968 {
969 let wh_tick = wh.clone();
970 let prov_tick = orch_provider.clone();
971 let inp_tick = tick_input.clone();
972 let res_tick = tokio::task::spawn_blocking(move || {
973 wh_tick.call(&prov_tick, "tick", &inp_tick)
974 })
975 .await;
976 match res_tick {
977 Ok(Err(e)) => tracing::debug!("orchestrator tick error: {}", e),
978 Err(e) => tracing::debug!("orchestrator tick join error: {}", e),
979 Ok(Ok(_)) => {}
980 }
981 }
982
983 let wh_list = wh.clone();
984 let prov_list = orch_provider.clone();
985 let inp_list = tick_input.clone();
986 let pending = match tokio::task::spawn_blocking(move || {
987 wh_list.call(&prov_list, "list_pending_handoffs", &inp_list)
988 })
989 .await
990 {
991 Ok(Ok(result)) => result,
992 Ok(Err(e)) => {
993 tracing::debug!("orchestrator list_pending_handoffs error: {}", e);
994 continue;
995 }
996 Err(e) => {
997 tracing::debug!(
998 "orchestrator list_pending_handoffs join error: {}",
999 e
1000 );
1001 continue;
1002 }
1003 };
1004
1005 let handoffs = match serde_json::from_str::<serde_json::Value>(&pending)
1006 .ok()
1007 .and_then(|value| {
1008 value
1009 .get("data")
1010 .and_then(|data| data.get("handoffs"))
1011 .and_then(|handoffs| handoffs.as_array().cloned())
1012 }) {
1013 Some(handoffs) => handoffs,
1014 None => continue,
1015 };
1016
1017 let semaphore = Arc::new(Semaphore::new(ORCHESTRATOR_DISPATCH_CONCURRENCY));
1018 let mut tasks = Vec::with_capacity(handoffs.len());
1019
1020 for handoff in handoffs {
1021 let board_id = handoff
1022 .get("board_id")
1023 .and_then(|value| value.as_str())
1024 .map(str::to_owned);
1025 let handoff_id = handoff
1026 .get("handoff_id")
1027 .and_then(|value| value.as_str())
1028 .map(str::to_owned);
1029 let (Some(board_id), Some(handoff_id)) = (board_id, handoff_id) else {
1030 continue;
1031 };
1032
1033 let permit = match semaphore.clone().acquire_owned().await {
1034 Ok(permit) => permit,
1035 Err(e) => {
1036 tracing::debug!("orchestrator semaphore closed: {}", e);
1037 break;
1038 }
1039 };
1040 let wh_dispatch = wh.clone();
1041 let prov_dispatch = orch_provider.clone();
1042 let input = format!(
1043 r#"{{"board_id":"{}","handoff_id":"{}"}}"#,
1044 serde_json::to_string(&board_id).unwrap_or_default().trim_matches('"'),
1045 serde_json::to_string(&handoff_id).unwrap_or_default().trim_matches('"')
1046 );
1047 tasks.push(tokio::spawn(async move {
1048 let _permit = permit;
1049 let res = tokio::task::spawn_blocking(move || {
1050 wh_dispatch.call_isolated(&prov_dispatch, "dispatch_one", &input)
1051 })
1052 .await;
1053 match res {
1054 Ok(Err(e)) => tracing::debug!(
1055 board_id = %board_id,
1056 handoff_id = %handoff_id,
1057 "orchestrator dispatch_one error: {}",
1058 e
1059 ),
1060 Err(e) => tracing::debug!(
1061 board_id = %board_id,
1062 handoff_id = %handoff_id,
1063 "orchestrator dispatch_one join error: {}",
1064 e
1065 ),
1066 Ok(Ok(_)) => {}
1067 }
1068 }));
1069 }
1070
1071 for task in tasks {
1072 if let Err(e) = task.await {
1073 tracing::debug!("orchestrator dispatch task join error: {}", e);
1074 }
1075 }
1076 }
1077 }
1078 }
1079 });
1080 }
1081
1082 fn save_kv(kv: &Arc<StdMutex<HashMap<String, String>>>, path: &std::path::Path) {
1084 let data = kv.lock().unwrap();
1085 if data.is_empty() {
1086 return;
1087 }
1088 if let Some(parent) = path.parent() {
1089 let _ = std::fs::create_dir_all(parent);
1090 }
1091 match serde_json::to_string(&*data) {
1092 Ok(json) => {
1093 if let Err(e) = std::fs::write(path, json) {
1094 tracing::warn!("Failed to save KV store: {}", e);
1095 }
1096 }
1097 Err(e) => tracing::warn!("Failed to serialize KV store: {}", e),
1098 }
1099 }
1100
1101 {
1103 let kv_save = kv_store.clone();
1104 let kv_save_path = kv_path.clone();
1105 tokio::spawn(async move {
1106 let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
1107 loop {
1108 interval.tick().await;
1109 save_kv(&kv_save, &kv_save_path);
1110 }
1111 });
1112 }
1113
1114 let pm_shutdown = process_manager.clone();
1115 let kv_shutdown = kv_store.clone();
1116 let kv_shutdown_path = kv_path.clone();
1117 let graceful_shutdown = async move {
1118 tokio::select! {
1119 _ = tokio::signal::ctrl_c() => {}
1120 _ = async {
1121 let _ = shutdown_rx.await;
1122 } => {}
1123 }
1124 tracing::info!("Shutting down, saving KV store and stopping services...");
1125 save_kv(&kv_shutdown, &kv_shutdown_path);
1126 pm_shutdown.stop_all().await;
1127 };
1128
1129 let app = build_router(state);
1131 crate::api::rpc::set_global_router(app.clone());
1134
1135 let ffi_log_path = std::env::var("WEFT_FFI_DATA_DIR").ok().map(|d| std::path::PathBuf::from(d).join("ffi-debug.log"));
1137
1138 match tokio::net::TcpListener::bind(&addr).await {
1142 Ok(listener) => {
1143 tracing::info!("weft-core listening on {}", addr);
1144 if let Some(ref log_path) = ffi_log_path {
1145 use std::io::Write;
1146 if let Ok(mut f) = std::fs::OpenOptions::new().append(true).open(log_path) {
1147 let _ = writeln!(f, "[ffi] HTTP bind OK on {}", addr);
1148 }
1149 }
1150 axum::serve(listener, app)
1151 .with_graceful_shutdown(graceful_shutdown)
1152 .await?;
1153 }
1154 Err(e) => {
1155 tracing::warn!(
1156 "HTTP listener bind on {} failed ({}); continuing in FFI-only mode (in-process dispatch still works). Awaiting shutdown signal.",
1157 addr,
1158 e
1159 );
1160 if let Some(ref log_path) = ffi_log_path {
1161 use std::io::Write;
1162 if let Ok(mut f) = std::fs::OpenOptions::new().append(true).open(log_path) {
1163 let _ = writeln!(f, "[ffi] HTTP bind FAILED on {}: {}", addr, e);
1164 }
1165 }
1166 graceful_shutdown.await;
1168 }
1169 }
1170
1171 Ok(())
1172}
1173