"""NebulaShell Core Engine — 核心引擎 整合功能: - 插件加载(目录结构) - 生命周期管理 - 依赖解析 - 签名校验(RSA-SHA256) - PL 注入(沙箱执行) - 能力注册 - 文件监控与热重载 - HTTP 服务(子模块) - REPL 终端(子模块) - 全面防护:完整性检查、内存保护、行为审计、防篡改监控、降级恢复 - 数据存储接口(为 data-store 插件预留) """ import sys import json import re import os import time import types import hashlib import threading import traceback import importlib.util import functools from pathlib import Path from typing import Any, Optional, Callable from oss.plugin.types import register_plugin_type from oss.plugin.capabilities import scan_capabilities from oss.logger.logger import Log from oss.config import get_config from oss.core.lifecycle import LifecycleManager, Lifecycle from oss.core.security import IntegrityChecker, MemoryGuard, AuditLogger, TamperMonitor, FallbackManager, PluginPermissionError, PluginProxy from oss.core.deps import DependencyError, DependencyResolver from oss.core.datastore import DataStore from oss.core.pl_injector import PLValidationError, PLInjector from oss.core.watcher import HotReloadError, FileWatcher from oss.core.signature import SignatureError, SignatureVerifier, PluginSigner class PluginInfo: """插件信息""" def __init__(self): self.name: str = "" self.version: str = "" self.author: str = "" self.description: str = "" self.readme: str = "" self.config: dict[str, Any] = {} self.extensions: dict[str, Any] = {} self.lifecycle: Any = None self.capabilities: set[str] = set() self.dependencies: list[str] = [] self.pl_injected: bool = False self.file_hash: str = "" # 文件完整性 hash class CapabilityRegistry: """能力注册表""" def __init__(self, permission_check: bool = True): self.providers: dict = {} self.consumers: dict = {} self.permission_check = permission_check def register_provider(self, capability: str, plugin_name: str, instance: Any): self.providers[capability] = {"plugin": plugin_name, "instance": instance} if capability not in self.consumers: self.consumers[capability] = [] def register_consumer(self, capability: str, plugin_name: str): if capability not in self.consumers: self.consumers[capability] = [] if plugin_name not in self.consumers[capability]: self.consumers[capability].append(plugin_name) def get_provider(self, capability: str, requester: str = "", allowed_plugins: list = None) -> Optional[Any]: if capability not in self.providers: return None if self.permission_check and allowed_plugins is not None: pn = self.providers[capability]["plugin"] if pn != requester and pn not in allowed_plugins and "*" not in allowed_plugins: raise PluginPermissionError(f"插件 '{requester}' 无权使用能力 '{capability}'") return self.providers[capability]["instance"] def has_capability(self, capability: str) -> bool: return capability in self.providers def get_consumers(self, capability: str) -> list: return self.consumers.get(capability, []) class PluginManager: """插件管理器 — Core 的核心""" def __init__(self, permission_check: bool = True): self.plugins: dict = {} self.capability_registry = CapabilityRegistry(permission_check=permission_check) self.permission_check = permission_check self.enforce_signature = True self.pl_injector = PLInjector(self) self.lifecycle_manager = LifecycleManager() self.dependency_resolver = DependencyResolver() self.signature_verifier = SignatureVerifier() self.hot_reload_watcher = None # 全面防护 self.integrity_checker = IntegrityChecker() self.memory_guard = MemoryGuard(self) self.audit_logger = AuditLogger() self.tamper_monitor = TamperMonitor(self) self.fallback_manager = FallbackManager(self) # 数据存储 self.data_store = DataStore() # HTTP 服务 & REPL self.http_server = None self.repl_shell = None # NBPF 组件 self.nbpf_loader = None self._nbpf_initialized = False # 插件目录映射 self._plugin_dirs: dict[str, Path] = {} # ── NBPF 支持 ── def _init_nbpf(self): """初始化 NBPF 加载器""" if self._nbpf_initialized: return try: from oss.core.nbpf import NBPFLoader, NBPCrypto, NIRCompiler config = get_config() self._trusted_keys_dir = Path(config.get("NBPF_TRUSTED_KEYS_DIR", "./data/nbpf-keys/trusted")) rsa_keys_dir = Path(config.get("NBPF_RSA_KEYS_DIR", "./data/nbpf-keys/rsa")) # 加载信任的 Ed25519 公钥 trusted_ed25519 = {} if self._trusted_keys_dir.exists(): for kf in self._trusted_keys_dir.glob("*.pem"): name = kf.stem trusted_ed25519[name] = kf.read_bytes() # 加载信任的 RSA 公钥 trusted_rsa = {} if rsa_keys_dir.exists(): for kf in rsa_keys_dir.glob("*.pem"): name = kf.stem trusted_rsa[name] = kf.read_bytes() # 加载 RSA 私钥 rsa_private = None private_dir = Path(config.get("NBPF_KEYS_DIR", "./data/nbpf-keys")) / "private" if private_dir.exists(): pk_files = list(private_dir.glob("*.pem")) if pk_files: rsa_private = pk_files[0].read_bytes() self.nbpf_loader = NBPFLoader( crypto=NBPCrypto(), compiler=NIRCompiler(), trusted_ed25519_keys=trusted_ed25519, trusted_rsa_keys=trusted_rsa, rsa_private_key=rsa_private, ) self._nbpf_initialized = True Log.info("Core", "NBPF 加载器已初始化") except Exception as e: Log.warn("Core", f"NBPF 加载器初始化失败: {e}") def load_nbpf(self, nbpf_path: Path, plugin_name: str = None) -> Optional[Any]: """加载 .nbpf 插件文件 如果插件作者不在本地信任列表中,会通过 CLI 交互询问用户是否信任。 信任后自动将公钥加入信任列表,下次无需再次询问。 Args: nbpf_path: .nbpf 文件路径 plugin_name: 可选,插件名称 Returns: 插件实例,失败或用户拒绝信任返回 None """ if not self._nbpf_initialized: self._init_nbpf() if self.nbpf_loader is None: Log.error("Core", "NBPF 加载器未初始化,无法加载 .nbpf 文件") return None # 第一次尝试加载 result = self._do_load_nbpf(nbpf_path, plugin_name) if result is not None: return result # 如果第一次失败(未信任且用户首次拒绝),不再重试 return None def _do_load_nbpf(self, nbpf_path: Path, plugin_name: str = None) -> Optional[Any]: """执行 .nbpf 加载,含信任检查""" import base64 as _b64 import hashlib as _hl try: instance, info = self.nbpf_loader.load(nbpf_path, plugin_name) name = info["name"] is_trusted = info.get("trusted", False) # 如果作者未被信任,询问用户 if not is_trusted: author = info.get("author", "unknown") pub_key_b64 = info.get("signer_public_key", "") pub_key_bytes = _b64.b64decode(pub_key_b64) # 计算公钥指纹(SHA256 前 16 位 hex) fingerprint = _hl.sha256(pub_key_bytes).hexdigest()[:16] print("\n" + "=" * 54) print(f" [NBPF] 检测到未知作者的插件") print(f" {'─' * 50}") print(f" 插件名称: {name}") print(f" 插件作者: {author}") print(f" 插件版本: {info.get('version', '?')}") print(f" 作者公钥指纹: {fingerprint}") print(f" {'─' * 50}") answer = input(" 是否信任此作者? [y/N] > ").strip().lower() if answer in ("y", "yes"): # 用户信任 → 保存公钥到信任列表 self._trust_author(pub_key_bytes, name, author) # 重新加载 return self._do_load_nbpf(nbpf_path, plugin_name) else: Log.warn("Core", f"用户已拒绝信任作者 '{author}',跳过插件 {name}") return None # 构建 PluginInfo pinfo = PluginInfo() pinfo.name = name pinfo.version = info.get("version", "") pinfo.author = info.get("author", "") pinfo.description = info.get("description", "") pinfo.dependencies = info.get("manifest", {}).get("dependencies", []) # 注册到插件列表 self.plugins[name] = { "instance": instance, "module": None, "info": pinfo, "permissions": [], "nbpf_path": str(nbpf_path), } self._plugin_dirs[name] = nbpf_path.parent # 生命周期 pinfo.lifecycle = self.lifecycle_manager.create(name) # 审计日志 self.audit_logger.log(name, "loaded", f".nbpf 版本 {pinfo.version}") Log.ok("Core", f"NBPF 插件 '{name}' 加载成功") return instance except Exception as e: Log.error("Core", f"NBPF 插件加载失败: {e}") return None def _trust_author(self, pub_key_bytes: bytes, plugin_name: str, author: str): """将作者公钥加入本地信任列表""" import hashlib as _hl fingerprint = _hl.sha256(pub_key_bytes).hexdigest()[:16] key_name = f"author_{fingerprint}" # 创建信任目录 if not hasattr(self, '_trusted_keys_dir') or self._trusted_keys_dir is None: from oss.config import get_config cfg = get_config() self._trusted_keys_dir = Path(cfg.get("NBPF_TRUSTED_KEYS_DIR", "./data/nbpf-keys/trusted")) self._trusted_keys_dir.mkdir(parents=True, exist_ok=True) # 保存公钥文件 key_path = self._trusted_keys_dir / f"{key_name}.pem" key_path.write_bytes(pub_key_bytes) # 更新加载器的信任列表 self.nbpf_loader.trusted_ed25519_keys[key_name] = pub_key_bytes Log.ok("NBPF", f"已将作者 '{author}' 加入信任列表 ({key_path})") def _get_plugin_dir(self, plugin_name: str) -> Optional[Path]: return self._plugin_dirs.get(plugin_name) def _load_manifest(self, plugin_dir: Path) -> dict: mf = plugin_dir / "manifest.json" if not mf.exists(): return {} with open(mf, "r", encoding="utf-8") as f: return json.load(f) def _load_readme(self, plugin_dir: Path) -> str: rf = plugin_dir / "README.md" if not rf.exists(): return "" with open(rf, "r", encoding="utf-8") as f: return f.read() def _parse_config_file(self, file_path: Path, file_type: str) -> dict: """通用配置文件解析 - 使用 ast.literal_eval 安全解析""" import ast if not file_path.exists(): return {} try: with open(file_path, "r", encoding="utf-8") as f: content = f.read() except FileNotFoundError: Log.warn("Core", f"{file_type}文件不存在:{file_path}") return {} except PermissionError as e: Log.error("Core", f"{file_type}文件无权限读取:{file_path} - {e}") return {} except UnicodeDecodeError as e: Log.error("Core", f"{file_type}文件编码错误:{file_path} - {e}") return {} try: result = ast.literal_eval(content) if isinstance(result, dict): return {k: v for k, v in result.items() if not k.startswith("_")} except (ValueError, SyntaxError): pass config = {} for line in content.split('\n'): line = line.strip() if not line or line.startswith('#'): continue match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', line) if match: key, value_str = match.groups() if key.startswith('_'): continue try: value = ast.literal_eval(value_str) config[key] = value except (ValueError, SyntaxError): Log.warn("Core", f"{file_path} 跳过无效的值:{line}") continue return config def _load_config(self, plugin_dir: Path) -> dict: return self._parse_config_file(plugin_dir / "config.py", "配置") def _load_extensions(self, plugin_dir: Path) -> dict: return self._parse_config_file(plugin_dir / "extensions.py", "扩展") def load(self, plugin_dir: Path, use_sandbox: bool = True) -> Optional[Any]: """加载单个插件 支持: - 目录结构插件(main.py) - .nbpf 文件(直接传入 .nbpf 路径) """ # 如果是 .nbpf 文件,使用 NBPF 加载器 if plugin_dir.suffix == ".nbpf": return self.load_nbpf(plugin_dir) main_file = plugin_dir / "main.py" if not main_file.exists(): return None manifest = self._load_manifest(plugin_dir) readme = self._load_readme(plugin_dir) config = self._load_config(plugin_dir) extensions = self._load_extensions(plugin_dir) capabilities = scan_capabilities(plugin_dir) plugin_name = plugin_dir.name.rstrip("}") # 完整性检查:加载前计算 hash self.integrity_checker.register(plugin_name, plugin_dir) # PL 注入检查 pl_injection = manifest.get("config", {}).get("args", {}).get("pl_injection", False) if pl_injection: Log.tip("Core", f"插件 '{plugin_name}' 声明了 pl_injection,正在检查 PL/ 文件夹...") if not self.pl_injector.check_and_load_pl(plugin_dir, plugin_name): Log.error("Core", f"插件 '{plugin_name}' 因 PL 注入检查失败被拒绝加载") return None Log.ok("Core", f"插件 '{plugin_name}' PL 注入检查通过") permissions = manifest.get("permissions", []) spec = importlib.util.spec_from_file_location(f"plugin.{plugin_name}", str(main_file)) module = importlib.util.module_from_spec(spec) module.__package__ = f"plugin.{plugin_name}" module.__path__ = [str(plugin_dir)] sys.modules[spec.name] = module spec.loader.exec_module(module) if not hasattr(module, "New"): return None instance = module.New() if self.permission_check and permissions: instance = PluginProxy(plugin_name, instance, permissions, self.plugins) info = PluginInfo() meta = manifest.get("metadata", {}) info.name = meta.get("name", plugin_name) info.version = meta.get("version", "") info.author = meta.get("author", "") info.description = meta.get("description", "") info.readme = readme info.config = manifest.get("config", {}).get("args", config) info.extensions = extensions info.capabilities = capabilities info.dependencies = manifest.get("dependencies", []) info.pl_injected = pl_injection info.file_hash = self.integrity_checker.get_hash(plugin_name) or "" for cap in capabilities: self.capability_registry.register_provider(cap, plugin_name, instance) info.lifecycle = self.lifecycle_manager.create(plugin_name) self.plugins[plugin_name] = {"instance": instance, "module": module, "info": info, "permissions": permissions} self._plugin_dirs[plugin_name] = plugin_dir # 审计日志 self.audit_logger.log(plugin_name, "loaded", f"版本 {info.version}") # 通过 bridge 通知其他插件 if plugin_name != "plugin-bridge": bridge = self._get_bridge() if bridge: bridge.emit("plugin.loaded", name=plugin_name, version=info.version) return instance def _restart_plugin(self, plugin_name: str): """重启单个插件""" if plugin_name not in self.plugins: return plugin_dir = self._plugin_dirs.get(plugin_name) if not plugin_dir: return # 停止旧实例 try: if hasattr(self.plugins[plugin_name]["instance"], "stop"): self.plugins[plugin_name]["instance"].stop() except Exception: pass # 从 sys.modules 中移除 module_name = f"plugin.{plugin_name}" if module_name in sys.modules: del sys.modules[module_name] module_name = f"nbpf.{plugin_name}" if module_name in sys.modules: del sys.modules[module_name] # 重新加载 del self.plugins[plugin_name] self.load(plugin_dir) def load_all(self, mods_dir: str = "mods"): if 'plugin' not in sys.modules: pkg = types.ModuleType('plugin') pkg.__path__ = [] pkg.__package__ = 'plugin' sys.modules['plugin'] = pkg Log.tip("Core", "已创建 plugin 命名空间包") from oss.config import get_config config = get_config() store_dir = str(config.get("STORE_DIR", "./store")) if not self._check_any_plugins(store_dir): Log.warn("Core", "未检测到任何插件") self._bootstrap_installation() self._load_plugins_from_dir(Path(store_dir)) self._sort_by_dependencies() def _check_any_plugins(self, store_dir: str) -> bool: sp = Path(store_dir) if not sp.exists(): return False for vendor_dir in sp.iterdir(): if vendor_dir.is_dir(): for plugin_dir in vendor_dir.iterdir(): if plugin_dir.is_dir() and (plugin_dir / "main.py").exists(): return True return False def _load_plugins_from_dir(self, store_dir: Path): if not store_dir.exists(): Log.warn("Core", f"插件目录不存在: {store_dir}") return for vendor_dir in sorted(store_dir.iterdir()): if not vendor_dir.is_dir(): continue for plugin_dir in sorted(vendor_dir.iterdir()): if not plugin_dir.is_dir(): continue try: self.load(plugin_dir) except Exception as e: Log.error("Core", f"加载插件失败 {plugin_dir.name}: {e}") self._link_capabilities() def _load_mods_from_dir(self, mods_dir: Path): if not mods_dir.exists(): return nbpf_files = [] for f in mods_dir.iterdir(): if f.is_file() and f.suffix == ".nbpf": nbpf_files.append(f) nbpf_files.sort(key=lambda x: x.name) for f in nbpf_files: Log.info("Core", f"加载模组: {f.name}") self.load(f) self._link_capabilities() def _check_any_mods(self, mods_dir: str) -> bool: sp = Path(mods_dir) if sp.exists(): for f in sp.iterdir(): if f.is_file() and f.suffix == ".nbpf": return True return False def _bootstrap_installation(self): Log.info("Core", "跳过引导安装(无可用插件)") def _sort_by_dependencies(self): for n, i in self.plugins.items(): self.dependency_resolver.add_dependency(n, i["info"].dependencies) try: order = self.dependency_resolver.resolve() sp = {} for n in order: if n in self.plugins: sp[n] = self.plugins[n] for n in set(self.plugins.keys()) - set(sp.keys()): sp[n] = self.plugins[n] self.plugins = sp except Exception as e: Log.error("Core", f"依赖解析失败: {e}") def _link_capabilities(self): for pn, info in self.plugins.items(): for cap in info["info"].capabilities: if self.capability_registry.has_capability(cap): for cn in self.capability_registry.get_consumers(cap): if cn in self.plugins: ci = self.plugins[cn]["info"] ca = self.plugins[cn].get("permissions", []) try: p = self.capability_registry.get_provider(cap, requester=cn, allowed_plugins=ca) if p and hasattr(ci, "extensions"): ci.extensions[f"_{cap}_provider"] = p except PluginPermissionError as e: Log.error("Core", f"权限拒绝: {e}") def start_all(self): self._inject_dependencies() for n, i in self.plugins.items(): try: wrapped = self.fallback_manager.wrap_plugin_method(n, i["instance"].start) wrapped() except Exception as e: Log.error("Core", f"启动失败 {n}: {e}") def _get_bridge(self): """Get the plugin-bridge instance if loaded.""" if "plugin-bridge" in self.plugins: bridge = self.plugins["plugin-bridge"]["instance"] if hasattr(bridge, "emit"): return bridge return None def init_and_start_all(self): Log.info("Core", f"init_and_start_all 被调用,plugins={len(self.plugins)}") self._inject_dependencies() ordered = self._get_ordered_plugins() Log.tip("Core", f"插件启动顺序: {' -> '.join(ordered)}") for name in ordered: if "Core" in name: continue try: Log.info("Core", f"初始化: {name}") wrapped_init = self.fallback_manager.wrap_plugin_method(name, self.plugins[name]["instance"].init) wrapped_init() except Exception as e: Log.error("Core", f"初始化失败 {name}: {e}") for name in ordered: if "Core" in name: continue try: Log.info("Core", f"启动: {name}") wrapped_start = self.fallback_manager.wrap_plugin_method(name, self.plugins[name]["instance"].start) wrapped_start() bridge = self._get_bridge() if bridge and name != "plugin-bridge": bridge.emit("plugin.started", name=name) except Exception as e: Log.error("Core", f"启动失败 {name}: {e}") def _get_ordered_plugins(self) -> list[str]: try: ordered = [n for n in self.dependency_resolver.resolve() if n in self.plugins] if ordered: return ordered except Exception as e: Log.warn("Core", f"依赖解析失败,使用原始顺序: {e}") return list(self.plugins.keys()) def _inject_dependencies(self): Log.info("Core", f"开始注入依赖,共 {len(self.plugins)} 个插件") nm = {} for n in self.plugins: c = n.rstrip("}") nm[c] = n nm[c + "}"] = n for n, i in self.plugins.items(): inst = i["instance"] io = i.get("info") if not io or not io.dependencies: continue for dn in io.dependencies: ad = nm.get(dn) or nm.get(dn + "}") if ad and ad in self.plugins: sn = f"set_{dn.replace('-', '_')}" if hasattr(inst, sn): try: getattr(inst, sn)(self.plugins[ad]["instance"]) Log.ok("Core", f"注入成功: {n} <- {ad}") except Exception as e: Log.error("Core", f"注入依赖失败 {n}.{sn}: {e}") else: Log.warn("Core", f"{n} 没有 {sn} 方法") def stop_all(self): for n, i in reversed(list(self.plugins.items())): try: if hasattr(i["instance"], "stop"): i["instance"].stop() bridge = self._get_bridge() if bridge and n != "plugin-bridge": bridge.emit("plugin.stopped", name=n) except Exception as e: Log.error("Core", f"插件 {n} 停止失败:{type(e).__name__}: {e}") self.lifecycle_manager.stop_all() def get_info(self, name: str) -> Optional[PluginInfo]: if name in self.plugins: return self.plugins[name]["info"] return None def has_capability(self, capability: str) -> bool: return self.capability_registry.has_capability(capability) def get_capability_provider(self, capability: str) -> Optional[Any]: return self.capability_registry.get_provider(capability) # ── HTTP 服务 ── def start_http_server(self): """启动 HTTP 服务(子模块)""" try: from oss.core.http_api.server import HttpServer from oss.core.http_api.router import HttpRouter from oss.core.http_api.middleware import MiddlewareChain router = HttpRouter() middleware = MiddlewareChain() self.http_server = HttpServer(router=router, middleware=middleware) self.http_server.start() Log.ok("Core", "HTTP 服务已启动") except Exception as e: Log.error("Core", f"HTTP 服务启动失败: {e}") def stop_http_server(self): """停止 HTTP 服务""" if self.http_server: try: self.http_server.stop() Log.info("Core", "HTTP 服务已停止") except Exception as e: Log.error("Core", f"HTTP 服务停止失败: {e}") def get_http_router(self): """获取 HTTP 路由器""" if self.http_server: return self.http_server.router return None # ── REPL ── def start_repl(self): """启动 REPL 终端(子模块)""" try: from oss.core.repl.main import NebulaShell self.repl_shell = NebulaShell(self) Log.ok("Core", "REPL 终端已启动") self.repl_shell.cmdloop() except Exception as e: Log.error("Core", f"REPL 启动失败: {e}") # ── 防护管理 ── def start_tamper_monitor(self): """启动防篡改监控""" self.tamper_monitor.start() def stop_tamper_monitor(self): """停止防篡改监控""" self.tamper_monitor.stop() def get_audit_logs(self, plugin_name: str = None, limit: int = 50) -> list[dict]: """获取审计日志""" return self.audit_logger.get_logs(plugin_name, limit) def get_tamper_alerts(self) -> list[dict]: """获取防篡改告警""" return self.tamper_monitor.get_alerts() def get_degraded_plugins(self) -> list[str]: """获取降级插件列表""" return self.fallback_manager.get_degraded_plugins() def recover_plugin(self, plugin_name: str) -> bool: """手动恢复降级插件""" return self.fallback_manager.recover(plugin_name) def get_status(self) -> dict: """获取 Core 状态摘要""" nbpf_count = sum(1 for i in self.plugins.values() if i.get("nbpf_path")) return { "plugins": { "total": len(self.plugins), "nbpf": nbpf_count, "directory": len(self.plugins) - nbpf_count, "degraded": self.fallback_manager.get_degraded_plugins(), }, "nbpf_loader": self._nbpf_initialized, "http_server": self.http_server is not None, "tamper_monitor": self.tamper_monitor._running, "audit_logs": len(self.audit_logger._logs), "tamper_alerts": len(self.tamper_monitor._alerts), "data_store": str(self.data_store._base_dir), }