Skip to main content

weft_core/
runtime.rs

1//! Core 运行时主入口:从 main.rs 抽取,供 binary 和 FFI 共用。
2
3use 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
133/// Resolves the highest-priority provider package name for a capability from
134/// the registry. Returns `None` if no provider is registered. Used by the
135/// background tick loops so they call packages by capability (e.g.
136/// `scheduler.cron`) rather than hardcoded names (A3).
137fn 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/// Check if a process with the given PID is still alive.
258#[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        // Process handle obtained → still alive (or just exited but handle valid).
268        // Check exit code to distinguish.
269        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 // STILL_ACTIVE
276        } 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    // Unix: signal 0 checks existence without killing.
287    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    // Load config. In managed desktop mode, the runtime root is the current
297    // working directory and config lives under ./config/config.toml.
298    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(&current_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        // A packaged desktop bundle sits cwd-side as `<bundle>/{weft-core.exe,
309        // packages/index.toml}` with no `config/` dir. Treat any cwd that holds
310        // a package index as the runtime root so the bundled packages load,
311        // instead of falling back to the compile-time source tree (which does
312        // not exist on an end-user machine → empty package list, blank app).
313        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    // Allow CLI overrides: --config-dir, --data-dir, --port
331    // These are used when weft-core is launched as a system service with explicit paths.
332    {
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                        // Always update config_path so save_config writes here
340                        // (the bundled desktop app needs a writable location).
341                        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                            // Watch parent process — exit if it dies (handles crash/kill).
373                            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    // FFI 模式覆盖:当 core 作为 DLL 嵌入 Flutter 进程时,ffi.rs 的
394    // start_core_with_options 通过环境变量注入 data_dir / config_dir,
395    // 因为无法修改 std::env::args。这里在 CLI args 解析之后再做一轮覆盖。
396    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    // 把 [web_search] 配置的 API key 注入进程环境,供 js-extension-runtime
412    // 等 service 子进程继承(它们读 process.env.EXA_API_KEY 等)。
413    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    // Enable wasmtime compiled-module cache for faster WASM plugin reloads.
423    // Extism/wasmtime cache: accelerates second-and-later startups by caching
424    // compiled wasm modules to disk. Key is file content hash (auto-invalidates
425    // on package update). Wasmtime 43 format: [cache] + directory (no `enabled`).
426    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        // Wasmtime 43 requires forward slashes and absolute path, no `enabled` field.
431        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    // Process manager (created early so plugins can use it)
471    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    // Virtual key store (created early so plugins can use it)
477    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    // Discover runtime package packages once with stable source precedence.
500    let mut package_manager = PackageManager::new();
501    let discovered_packages = discover_runtime_packages(&repo_root, &package_index);
502
503    // B3: capability requirement integrity check. Warn (don't block) about any
504    // `requires` capability that no registered source provides — e.g. a package
505    // requiring `ext.mcp` when mcp-client is absent, which would otherwise fail
506    // silently at runtime.
507    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    // Collect chat providers from installed packages
528    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    // Persistent KV store for plugins
706    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    // 多 agent 编排:把 config 的 [team.roleRouting] 写入 KV(key=`team:role_routing`),
724    // 供 team-runtime 按角色查 model/provider。config 是 source of truth,每次启动覆盖。
725    {
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    // Load WASM packages
738    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            // Dedicated runtime for WASM host-function blocking calls — keeps
758            // nested host block_on (capability dispatch + 60s HTTP egress) off
759            // the main HTTP runtime so /health and other handlers stay live.
760            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        // Call init() on each loaded plugin
776        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    // Build app state
834    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    // Auto-start services
884    process_manager.start_auto().await;
885
886    // Cron tick timer — drive the scheduler.cron provider every 10s; also runs
887    // periodic memory cleanup. Providers are resolved by capability (A3) rather
888    // than hardcoded package names.
889    {
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                // Resolve providers by capability (releases the registry lock
904                // before touching the wasm handle).
905                let (cron_provider, memory_provider) = {
906                    let reg = cron_registry.read().await;
907                    (
908                        resolve_capability_provider(&reg, "scheduler.cron"),
909                        resolve_capability_provider(&reg, "memory.store"),
910                    )
911                };
912
913                let guard = cron_handle.read().await;
914                if let Some(ref wh) = *guard {
915                    // Cron tick
916                    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                    // Memory cleanup every ~5 minutes (30 ticks * 10s)
924                    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                // Resolve the workflow.orchestration provider by capability (A3).
954                let orch_provider = {
955                    let reg = orch_registry.read().await;
956                    resolve_capability_provider(&reg, "workflow.orchestration")
957                };
958                let Some(orch_provider) = orch_provider else {
959                    continue;
960                };
961
962                // Clone the handle out, then release the RwLock read guard
963                // BEFORE the (potentially slow, LLM-bearing) dispatch call so a
964                // long dispatch never blocks writers (package load / generation swap).
965                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    // KV persistence helper
1083    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    // Periodic KV save — every 5 minutes
1102    {
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    // Build router and serve
1130    let app = build_router(state);
1131    // 注册全局 router 供 /rpc 内部 dispatch 和 FFI rpc_call 直接调用(不走网络)。
1132    // 注意:这一步完成后 FFI dispatch 即可用,与 HTTP listener 是否成功无关。
1133    crate::api::rpc::set_global_router(app.clone());
1134
1135    // FFI debug: write HTTP bind status to ffi-debug.log (append).
1136    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    // HTTP listener 绑定失败设为非致命:FFI 模式(客户端进程内嵌)下所有请求走
1139    // 进程内 dispatch,不依赖 HTTP。端口被残留进程占用时(os error 10048)只告警,
1140    // 让 core 继续以纯 FFI 模式运行,而不是整个 run_server 失败退出。
1141    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            // 不起 HTTP,但仍等待 shutdown 信号以便优雅退出(保存 KV、停服务)。
1167            graceful_shutdown.await;
1168        }
1169    }
1170
1171    Ok(())
1172}
1173