- 核心功能从 store/ 迁移至 oss/core/ 框架层 - 实现 NBPF 包格式:多重签名(Ed25519+RSA-PSS+HMAC)+ 多重加密(AES-256-GCM) - 实现 NIR 编译器:基于 compile()+marshal 的跨平台中间表示 - 新增 nebula nbpf CLI 命令组(pack/unpack/verify/sign/keygen) - 新增 19 个 NBPF 测试用例,覆盖全链路 - 彻底重写 README,大型项目标准框架风格,所有图表使用 SVG - 更新 LICENSE 版权声明 - 清理旧版 store 插件目录(已迁移至 oss/core)
1687 lines
68 KiB
Python
1687 lines
68 KiB
Python
"""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 collections import deque, defaultdict
|
||
|
||
from oss.plugin.types import Plugin, register_plugin_type
|
||
from oss.plugin.capabilities import scan_capabilities
|
||
from oss.logger.logger import Log
|
||
from oss.config import get_config
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 生命周期管理
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class LifecycleState:
|
||
PENDING = "pending"
|
||
RUNNING = "running"
|
||
STOPPED = "stopped"
|
||
DEGRADED = "degraded"
|
||
CRASHED = "crashed"
|
||
|
||
|
||
class LifecycleError(Exception):
|
||
pass
|
||
|
||
|
||
class Lifecycle:
|
||
VALID_TRANSITIONS = {
|
||
LifecycleState.PENDING: [LifecycleState.RUNNING],
|
||
LifecycleState.RUNNING: [LifecycleState.STOPPED, LifecycleState.DEGRADED, LifecycleState.CRASHED],
|
||
LifecycleState.STOPPED: [LifecycleState.RUNNING],
|
||
LifecycleState.DEGRADED: [LifecycleState.RUNNING, LifecycleState.STOPPED],
|
||
LifecycleState.CRASHED: [LifecycleState.PENDING, LifecycleState.STOPPED],
|
||
}
|
||
|
||
def __init__(self, name: str):
|
||
self.name = name
|
||
self.state = LifecycleState.PENDING
|
||
self._hooks: dict[str, list[Callable]] = {
|
||
"before_start": [], "after_start": [],
|
||
"before_stop": [], "after_stop": [],
|
||
"on_crash": [], "on_degrade": [],
|
||
}
|
||
self._extensions: dict[str, Any] = {}
|
||
|
||
def add_extension(self, name: str, extension: Any):
|
||
self._extensions[name] = extension
|
||
|
||
def get_extension(self, name: str) -> Any:
|
||
return self._extensions.get(name)
|
||
|
||
def start(self):
|
||
for hook in self._hooks["before_start"]:
|
||
hook(self)
|
||
self.transition(LifecycleState.RUNNING)
|
||
for hook in self._hooks["after_start"]:
|
||
hook(self)
|
||
|
||
def stop(self):
|
||
if self.state in (LifecycleState.RUNNING, LifecycleState.DEGRADED):
|
||
for hook in self._hooks["before_stop"]:
|
||
hook(self)
|
||
self.transition(LifecycleState.STOPPED)
|
||
for hook in self._hooks["after_stop"]:
|
||
hook(self)
|
||
|
||
def restart(self):
|
||
self.stop()
|
||
self.start()
|
||
|
||
def mark_crashed(self):
|
||
self.transition(LifecycleState.CRASHED)
|
||
for hook in self._hooks["on_crash"]:
|
||
hook(self)
|
||
|
||
def mark_degraded(self):
|
||
self.transition(LifecycleState.DEGRADED)
|
||
for hook in self._hooks["on_degrade"]:
|
||
hook(self)
|
||
|
||
def on(self, event: str, hook: Callable):
|
||
if event in self._hooks:
|
||
self._hooks[event].append(hook)
|
||
|
||
def transition(self, target_state: LifecycleState):
|
||
valid = self.VALID_TRANSITIONS.get(self.state, [])
|
||
if target_state in valid:
|
||
self.state = target_state
|
||
else:
|
||
raise LifecycleError(f"Cannot transition from {self.state} to {target_state}")
|
||
|
||
|
||
class LifecycleManager:
|
||
def __init__(self):
|
||
self.lifecycles: dict[str, Lifecycle] = {}
|
||
|
||
def create(self, name: str) -> Lifecycle:
|
||
lifecycle = Lifecycle(name)
|
||
self.lifecycles[name] = lifecycle
|
||
return lifecycle
|
||
|
||
def get(self, name: str) -> Optional[Lifecycle]:
|
||
return self.lifecycles.get(name)
|
||
|
||
def start_all(self):
|
||
for lc in self.lifecycles.values():
|
||
try:
|
||
lc.start()
|
||
except LifecycleError:
|
||
pass
|
||
|
||
def stop_all(self):
|
||
for lc in self.lifecycles.values():
|
||
try:
|
||
lc.stop()
|
||
except LifecycleError:
|
||
pass
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 插件信息
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
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 PermissionError(Exception):
|
||
"""权限错误"""
|
||
pass
|
||
|
||
|
||
class PluginProxy:
|
||
"""插件代理 - 防止越级访问"""
|
||
def __init__(self, plugin_name: str, plugin_instance: Any, allowed_plugins: list[str], all_plugins: dict):
|
||
self._plugin_name = plugin_name
|
||
self._plugin_instance = plugin_instance
|
||
self._allowed_plugins = set(allowed_plugins)
|
||
self._all_plugins = all_plugins
|
||
|
||
def get_plugin(self, name: str) -> Any:
|
||
if name not in self._allowed_plugins and "*" not in self._allowed_plugins:
|
||
raise PermissionError(f"插件 '{self._plugin_name}' 无权访问插件 '{name}'")
|
||
if name not in self._all_plugins:
|
||
return None
|
||
return self._all_plugins[name]["instance"]
|
||
|
||
def list_plugins(self) -> list[str]:
|
||
if "*" in self._allowed_plugins:
|
||
return list(self._all_plugins.keys())
|
||
return [n for n in self._allowed_plugins if n in self._all_plugins]
|
||
|
||
def get_capability(self, capability: str) -> Any:
|
||
return None
|
||
|
||
def __getattr__(self, name: str):
|
||
return getattr(self._plugin_instance, name)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 能力注册表
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
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 PermissionError(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 DependencyError(Exception):
|
||
pass
|
||
|
||
|
||
class DependencyResolver:
|
||
def __init__(self):
|
||
self.graph: dict[str, list[str]] = {}
|
||
|
||
def add_dependency(self, name: str, dependencies: list[str]):
|
||
self.graph[name] = dependencies
|
||
|
||
def resolve(self) -> list[str]:
|
||
self._detect_cycles()
|
||
|
||
in_degree: dict[str, int] = {name: 0 for name in self.graph}
|
||
who_depends_on: dict[str, list[str]] = {name: [] for name in self.graph}
|
||
|
||
for name, deps in self.graph.items():
|
||
for dep in deps:
|
||
if dep in in_degree:
|
||
in_degree[name] += 1
|
||
who_depends_on[dep].append(name)
|
||
|
||
queue = [name for name, degree in in_degree.items() if degree == 0]
|
||
result = []
|
||
|
||
while queue:
|
||
node = queue.pop(0)
|
||
result.append(node)
|
||
for dependent in who_depends_on.get(node, []):
|
||
in_degree[dependent] -= 1
|
||
if in_degree[dependent] == 0:
|
||
queue.append(dependent)
|
||
|
||
if len(result) != len(self.graph):
|
||
raise DependencyError("无法解析依赖,可能存在循环依赖")
|
||
|
||
return result
|
||
|
||
def _detect_cycles(self):
|
||
all_deps = set()
|
||
for deps in self.graph.values():
|
||
all_deps.update(deps)
|
||
all_plugins = set(self.graph.keys())
|
||
return list(all_deps - all_plugins)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 签名校验
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class SignatureError(Exception):
|
||
pass
|
||
|
||
|
||
class SignatureVerifier:
|
||
def __init__(self, key_dir: str = None):
|
||
config = get_config()
|
||
self.key_dir = Path(key_dir or str(config.get("SIGNATURE_KEYS_DIR", "./data/signature-verifier/keys")))
|
||
self.key_dir.mkdir(parents=True, exist_ok=True)
|
||
self.public_keys: dict[str, bytes] = {}
|
||
self._load_builtin_keys()
|
||
|
||
def _load_builtin_keys(self):
|
||
pub_dir = self.key_dir / "public"
|
||
if not pub_dir.exists():
|
||
return
|
||
for key_file in pub_dir.glob("*.pem"):
|
||
author_name = key_file.stem
|
||
self.public_keys[author_name] = key_file.read_bytes()
|
||
|
||
def _compute_plugin_hash(self, plugin_dir: Path) -> str:
|
||
hasher = hashlib.sha256()
|
||
files_to_hash = []
|
||
for file_path in sorted(plugin_dir.rglob("*")):
|
||
if file_path.is_file() and file_path.name != "SIGNATURE":
|
||
rel_path = file_path.relative_to(plugin_dir)
|
||
files_to_hash.append((str(rel_path), file_path))
|
||
for rel_path, file_path in files_to_hash:
|
||
hasher.update(rel_path.encode("utf-8"))
|
||
hasher.update(file_path.read_bytes())
|
||
return hasher.hexdigest()
|
||
|
||
def verify_plugin(self, plugin_dir: Path, author: str = "Falck") -> tuple[bool, str]:
|
||
import base64
|
||
from cryptography.hazmat.primitives import hashes, serialization
|
||
from cryptography.hazmat.primitives.asymmetric import padding
|
||
from cryptography.hazmat.backends import default_backend
|
||
from cryptography.exceptions import InvalidSignature
|
||
|
||
signature_file = plugin_dir / "SIGNATURE"
|
||
if not signature_file.exists():
|
||
return False, f"Plugin missing signature file: {plugin_dir}"
|
||
try:
|
||
sig_data = json.loads(signature_file.read_text())
|
||
except json.JSONDecodeError as e:
|
||
return False, f"Signature file format error: {e}"
|
||
required_fields = ["signature", "signer", "algorithm", "timestamp"]
|
||
for field in required_fields:
|
||
if field not in sig_data:
|
||
return False, f"Signature missing required field: {field}"
|
||
signer = sig_data["signer"]
|
||
signature = base64.b64decode(sig_data["signature"])
|
||
if signer not in self.public_keys:
|
||
return False, f"Unknown signer: {signer}"
|
||
try:
|
||
public_key = serialization.load_pem_public_key(
|
||
self.public_keys[signer], backend=default_backend()
|
||
)
|
||
except Exception as e:
|
||
return False, f"Public key load failed: {e}"
|
||
current_hash = self._compute_plugin_hash(plugin_dir)
|
||
try:
|
||
signed_data = f"{author}:{current_hash}".encode("utf-8")
|
||
public_key.verify(
|
||
signature, signed_data,
|
||
padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
|
||
hashes.SHA256()
|
||
)
|
||
return True, f"Signature verified (signer: {signer})"
|
||
except InvalidSignature:
|
||
return False, f"Signature mismatch! Plugin may have been tampered with (signer: {signer})"
|
||
except Exception as e:
|
||
return False, f"Signature verification error: {e}"
|
||
|
||
def is_official_plugin(self, plugin_dir: Path) -> bool:
|
||
"""检查是否为官方插件(使用内置公钥验证)"""
|
||
result, _ = self.verify_plugin(plugin_dir, author="NebulaShell")
|
||
return result
|
||
|
||
|
||
class PluginSigner:
|
||
def __init__(self, private_key_path: str = None):
|
||
self.private_key = None
|
||
if private_key_path:
|
||
self.load_private_key(private_key_path)
|
||
|
||
def load_private_key(self, key_path: str):
|
||
from cryptography.hazmat.primitives import serialization
|
||
from cryptography.hazmat.backends import default_backend
|
||
with open(key_path, "rb") as f:
|
||
pem_data = f.read()
|
||
self.private_key = serialization.load_pem_private_key(
|
||
pem_data, password=None, backend=default_backend()
|
||
)
|
||
|
||
def sign_plugin(self, plugin_dir: Path, signer_name: str, author: str = "Falck") -> str:
|
||
import base64
|
||
from cryptography.hazmat.primitives import hashes, serialization
|
||
from cryptography.hazmat.primitives.asymmetric import padding
|
||
from cryptography.hazmat.backends import default_backend
|
||
|
||
if not self.private_key:
|
||
raise ValueError("Private key not loaded")
|
||
hasher = hashlib.sha256()
|
||
files_to_hash = []
|
||
for file_path in sorted(plugin_dir.rglob("*")):
|
||
if file_path.is_file() and file_path.name not in ("SIGNATURE",):
|
||
rel_path = file_path.relative_to(plugin_dir)
|
||
files_to_hash.append((str(rel_path), file_path))
|
||
for rel_path, file_path in files_to_hash:
|
||
hasher.update(rel_path.encode("utf-8"))
|
||
hasher.update(file_path.read_bytes())
|
||
plugin_hash = hasher.hexdigest()
|
||
signed_data = f"{author}:{plugin_hash}".encode("utf-8")
|
||
signature = self.private_key.sign(
|
||
signed_data,
|
||
padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
|
||
hashes.SHA256()
|
||
)
|
||
sig_data = {
|
||
"signature": base64.b64encode(signature).decode(),
|
||
"signer": signer_name,
|
||
"algorithm": "RSA-SHA256",
|
||
"timestamp": time.time(),
|
||
"plugin_hash": plugin_hash,
|
||
"author": author
|
||
}
|
||
signature_file = plugin_dir / "SIGNATURE"
|
||
signature_file.write_text(json.dumps(sig_data, indent=2))
|
||
return str(signature_file)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 文件监控与热重载
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class HotReloadError(Exception):
|
||
pass
|
||
|
||
|
||
class FileWatcher:
|
||
def __init__(self, watch_dirs, extensions, callback):
|
||
self.watch_dirs = watch_dirs
|
||
self.extensions = extensions
|
||
self.callback = callback
|
||
self._running = False
|
||
self._thread = None
|
||
self._file_times = {}
|
||
self._init_file_times()
|
||
|
||
def _init_file_times(self):
|
||
for watch_dir in self.watch_dirs:
|
||
p = Path(watch_dir)
|
||
if p.exists():
|
||
for f in p.rglob("*"):
|
||
if f.is_file() and f.suffix in self.extensions:
|
||
self._file_times[str(f)] = f.stat().st_mtime
|
||
|
||
def start(self):
|
||
self._running = True
|
||
self._thread = threading.Thread(target=self._watch_loop, daemon=True)
|
||
self._thread.start()
|
||
Log.info("Core", "文件监控已启动")
|
||
|
||
def stop(self):
|
||
self._running = False
|
||
if self._thread:
|
||
self._thread.join(timeout=5)
|
||
|
||
def _watch_loop(self):
|
||
"""监控文件变化,触发热重载回调"""
|
||
while self._running:
|
||
try:
|
||
for watch_dir in self.watch_dirs:
|
||
p = Path(watch_dir)
|
||
if not p.exists():
|
||
continue
|
||
for f in p.rglob("*"):
|
||
if not f.is_file() or f.suffix not in self.extensions:
|
||
continue
|
||
current_mtime = f.stat().st_mtime
|
||
last_mtime = self._file_times.get(str(f))
|
||
if last_mtime is not None and current_mtime > last_mtime:
|
||
self._file_times[str(f)] = current_mtime
|
||
try:
|
||
self.callback(str(f))
|
||
except Exception as e:
|
||
Log.error("Core", f"热重载回调执行失败: {e}")
|
||
elif last_mtime is None:
|
||
self._file_times[str(f)] = current_mtime
|
||
except Exception as e:
|
||
Log.error("Core", f"文件监控异常: {e}")
|
||
time.sleep(2)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 全面防护机制
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class IntegrityChecker:
|
||
"""文件完整性检查"""
|
||
|
||
def __init__(self):
|
||
self._hashes: dict[str, str] = {}
|
||
|
||
def compute_hash(self, plugin_dir: Path) -> str:
|
||
"""计算插件目录的 SHA-256 hash"""
|
||
hasher = hashlib.sha256()
|
||
for file_path in sorted(plugin_dir.rglob("*")):
|
||
if file_path.is_file() and file_path.name not in ("SIGNATURE", "__pycache__"):
|
||
rel_path = str(file_path.relative_to(plugin_dir))
|
||
hasher.update(rel_path.encode("utf-8"))
|
||
hasher.update(file_path.read_bytes())
|
||
return hasher.hexdigest()
|
||
|
||
def register(self, plugin_name: str, plugin_dir: Path):
|
||
"""注册插件的初始 hash"""
|
||
self._hashes[plugin_name] = self.compute_hash(plugin_dir)
|
||
|
||
def verify(self, plugin_name: str, plugin_dir: Path) -> tuple[bool, str]:
|
||
"""验证插件文件是否被篡改"""
|
||
if plugin_name not in self._hashes:
|
||
return False, f"插件 '{plugin_name}' 未注册完整性检查"
|
||
current = self.compute_hash(plugin_dir)
|
||
if current == self._hashes[plugin_name]:
|
||
return True, "完整性验证通过"
|
||
return False, f"文件 hash 不匹配,插件可能被篡改"
|
||
|
||
def get_hash(self, plugin_name: str) -> Optional[str]:
|
||
return self._hashes.get(plugin_name)
|
||
|
||
|
||
class MemoryGuard:
|
||
"""运行时内存保护 - 防止插件修改 Core 内部状态"""
|
||
|
||
FROZEN_ATTRS = {
|
||
"plugins", "capability_registry", "lifecycle_manager",
|
||
"dependency_resolver", "signature_verifier", "pl_injector",
|
||
"integrity_checker", "audit_logger", "tamper_monitor",
|
||
"fallback_manager", "http_server", "repl_shell",
|
||
}
|
||
|
||
def __init__(self, manager: 'PluginManager'):
|
||
self._manager = manager
|
||
self._protected = True
|
||
|
||
def enable(self):
|
||
self._protected = True
|
||
|
||
def disable(self):
|
||
self._protected = False
|
||
|
||
def check_setattr(self, obj: Any, name: str, value: Any) -> bool:
|
||
"""检查是否允许设置属性,返回 False 表示拒绝"""
|
||
if not self._protected:
|
||
return True
|
||
if obj is self._manager and name in self.FROZEN_ATTRS:
|
||
Log.warn("Core", f"内存防护: 阻止了对 Core 内部属性 '{name}' 的修改")
|
||
return False
|
||
return True
|
||
|
||
|
||
class AuditLogger:
|
||
"""插件行为审计"""
|
||
|
||
def __init__(self, max_logs: int = 1000):
|
||
self._logs: deque = deque(maxlen=max_logs)
|
||
self._enabled = True
|
||
|
||
def enable(self):
|
||
self._enabled = True
|
||
|
||
def disable(self):
|
||
self._enabled = False
|
||
|
||
def log(self, plugin_name: str, action: str, detail: str = ""):
|
||
"""记录插件行为"""
|
||
if not self._enabled:
|
||
return
|
||
self._logs.append({
|
||
"time": time.time(),
|
||
"plugin": plugin_name,
|
||
"action": action,
|
||
"detail": detail,
|
||
})
|
||
|
||
def get_logs(self, plugin_name: str = None, limit: int = 50) -> list[dict]:
|
||
"""查询审计日志"""
|
||
if plugin_name:
|
||
filtered = [log for log in self._logs if log["plugin"] == plugin_name]
|
||
else:
|
||
filtered = list(self._logs)
|
||
return filtered[-limit:]
|
||
|
||
def get_stats(self) -> dict:
|
||
"""获取审计统计"""
|
||
stats: dict[str, int] = {}
|
||
for log in self._logs:
|
||
stats[log["plugin"]] = stats.get(log["plugin"], 0) + 1
|
||
return stats
|
||
|
||
|
||
class TamperMonitor:
|
||
"""防篡改监控 - 定期检查已加载插件的文件完整性"""
|
||
|
||
def __init__(self, manager: 'PluginManager', interval: int = 30):
|
||
self._manager = manager
|
||
self._interval = interval
|
||
self._running = False
|
||
self._thread = None
|
||
self._alerts: deque = deque(maxlen=100)
|
||
|
||
def start(self):
|
||
self._running = True
|
||
self._thread = threading.Thread(target=self._monitor_loop, daemon=True)
|
||
self._thread.start()
|
||
Log.info("Core", f"防篡改监控已启动 (间隔: {self._interval}s)")
|
||
|
||
def stop(self):
|
||
self._running = False
|
||
if self._thread:
|
||
self._thread.join(timeout=5)
|
||
|
||
def _monitor_loop(self):
|
||
while self._running:
|
||
try:
|
||
for plugin_name, info in self._manager.plugins.items():
|
||
plugin_dir = self._manager._get_plugin_dir(plugin_name)
|
||
if not plugin_dir:
|
||
continue
|
||
valid, msg = self._manager.integrity_checker.verify(plugin_name, plugin_dir)
|
||
if not valid:
|
||
alert = {
|
||
"time": time.time(),
|
||
"plugin": plugin_name,
|
||
"message": msg,
|
||
}
|
||
self._alerts.append(alert)
|
||
Log.error("Core", f"防篡改告警: 插件 '{plugin_name}' 可能被篡改!")
|
||
# 自动停止被篡改的插件
|
||
try:
|
||
info["instance"].stop()
|
||
lifecycle = self._manager.lifecycle_manager.get(plugin_name)
|
||
if lifecycle:
|
||
lifecycle.mark_crashed()
|
||
except Exception as e:
|
||
Log.error("Core", f"停止被篡改插件 '{plugin_name}' 失败: {e}")
|
||
except Exception as e:
|
||
Log.error("Core", f"防篡改监控异常: {e}")
|
||
time.sleep(self._interval)
|
||
|
||
def get_alerts(self) -> list[dict]:
|
||
return list(self._alerts)
|
||
|
||
|
||
class FallbackManager:
|
||
"""降级恢复机制 - 插件崩溃时自动重启"""
|
||
|
||
def __init__(self, manager: 'PluginManager', max_retries: int = 3):
|
||
self._manager = manager
|
||
self._max_retries = max_retries
|
||
self._retry_counts: dict[str, int] = {}
|
||
self._degraded: set[str] = set()
|
||
|
||
def wrap_plugin_method(self, plugin_name: str, method: Callable) -> Callable:
|
||
"""包装插件方法,捕获异常后自动重试"""
|
||
|
||
@functools.wraps(method)
|
||
def safe_method(*args, **kwargs):
|
||
try:
|
||
return method(*args, **kwargs)
|
||
except Exception as e:
|
||
Log.error("Core", f"插件 '{plugin_name}' 方法 '{method.__name__}' 异常: {e}")
|
||
self._handle_crash(plugin_name)
|
||
return None
|
||
|
||
return safe_method
|
||
|
||
def _handle_crash(self, plugin_name: str):
|
||
"""处理插件崩溃"""
|
||
retry_count = self._retry_counts.get(plugin_name, 0)
|
||
lifecycle = self._manager.lifecycle_manager.get(plugin_name)
|
||
|
||
if retry_count < self._max_retries:
|
||
self._retry_counts[plugin_name] = retry_count + 1
|
||
Log.warn("Core", f"插件 '{plugin_name}' 崩溃,正在重启 (第 {retry_count + 1}/{self._max_retries} 次)")
|
||
try:
|
||
if lifecycle:
|
||
lifecycle.mark_crashed()
|
||
self._manager._restart_plugin(plugin_name)
|
||
if lifecycle:
|
||
lifecycle.start()
|
||
Log.ok("Core", f"插件 '{plugin_name}' 重启成功")
|
||
except Exception as e:
|
||
Log.error("Core", f"插件 '{plugin_name}' 重启失败: {e}")
|
||
else:
|
||
Log.error("Core", f"插件 '{plugin_name}' 超过最大重试次数 ({self._max_retries}),标记为降级")
|
||
self._degraded.add(plugin_name)
|
||
if lifecycle:
|
||
lifecycle.mark_degraded()
|
||
|
||
def recover(self, plugin_name: str) -> bool:
|
||
"""手动恢复降级的插件"""
|
||
if plugin_name not in self._degraded:
|
||
return False
|
||
self._retry_counts[plugin_name] = 0
|
||
self._degraded.discard(plugin_name)
|
||
try:
|
||
self._manager._restart_plugin(plugin_name)
|
||
lifecycle = self._manager.lifecycle_manager.get(plugin_name)
|
||
if lifecycle:
|
||
lifecycle.start()
|
||
Log.ok("Core", f"插件 '{plugin_name}' 已手动恢复")
|
||
return True
|
||
except Exception as e:
|
||
Log.error("Core", f"恢复插件 '{plugin_name}' 失败: {e}")
|
||
return False
|
||
|
||
def is_degraded(self, plugin_name: str) -> bool:
|
||
return plugin_name in self._degraded
|
||
|
||
def get_degraded_plugins(self) -> list[str]:
|
||
return list(self._degraded)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 数据存储接口(为 data-store 插件预留)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class DataStore:
|
||
"""数据存储抽象接口
|
||
|
||
默认实现使用 JSON 文件存储到 ~/.nebula/data/
|
||
后续可由 data-store 插件替换为更完善的实现
|
||
"""
|
||
|
||
def __init__(self):
|
||
config = get_config()
|
||
data_dir_env = os.environ.get("NEBULA_DATA_DIR", "")
|
||
default_dir = Path(data_dir_env) if data_dir_env else Path.home() / ".nebula" / "data"
|
||
self._base_dir = Path(config.get("DATA_DIR", str(default_dir)))
|
||
self._base_dir.mkdir(parents=True, exist_ok=True)
|
||
self._lock = threading.Lock()
|
||
|
||
def _plugin_dir(self, plugin_name: str) -> Path:
|
||
"""获取插件专属数据目录"""
|
||
pd = self._base_dir / plugin_name
|
||
pd.mkdir(parents=True, exist_ok=True)
|
||
return pd
|
||
|
||
def save(self, plugin_name: str, key: str, data: Any) -> bool:
|
||
"""保存数据"""
|
||
with self._lock:
|
||
try:
|
||
file_path = self._plugin_dir(plugin_name) / f"{key}.json"
|
||
file_path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
||
return True
|
||
except Exception as e:
|
||
Log.error("Core", f"数据存储保存失败 [{plugin_name}/{key}]: {e}")
|
||
return False
|
||
|
||
def load(self, plugin_name: str, key: str, default: Any = None) -> Any:
|
||
"""加载数据"""
|
||
with self._lock:
|
||
try:
|
||
file_path = self._plugin_dir(plugin_name) / f"{key}.json"
|
||
if file_path.exists():
|
||
return json.loads(file_path.read_text(encoding="utf-8"))
|
||
return default
|
||
except Exception as e:
|
||
Log.error("Core", f"数据存储加载失败 [{plugin_name}/{key}]: {e}")
|
||
return default
|
||
|
||
def delete(self, plugin_name: str, key: str) -> bool:
|
||
"""删除数据"""
|
||
with self._lock:
|
||
try:
|
||
file_path = self._plugin_dir(plugin_name) / f"{key}.json"
|
||
if file_path.exists():
|
||
file_path.unlink()
|
||
return True
|
||
except Exception as e:
|
||
Log.error("Core", f"数据存储删除失败 [{plugin_name}/{key}]: {e}")
|
||
return False
|
||
|
||
def list_keys(self, plugin_name: str) -> list[str]:
|
||
"""列出插件所有数据键"""
|
||
pd = self._plugin_dir(plugin_name)
|
||
if not pd.exists():
|
||
return []
|
||
return [f.stem for f in pd.glob("*.json")]
|
||
|
||
def set_custom_path(self, plugin_name: str, custom_path: str) -> bool:
|
||
"""插件自定义存储路径(不能修改到项目目录内)"""
|
||
path = Path(custom_path).expanduser().resolve()
|
||
project_dir = Path.cwd().resolve()
|
||
if str(path).startswith(str(project_dir)):
|
||
Log.error("Core", f"插件 '{plugin_name}' 试图将数据存储到项目目录: {custom_path}")
|
||
return False
|
||
path.mkdir(parents=True, exist_ok=True)
|
||
# 创建符号链接或记录映射
|
||
mapping_file = self._base_dir / "_custom_paths.json"
|
||
mappings = {}
|
||
if mapping_file.exists():
|
||
try:
|
||
mappings = json.loads(mapping_file.read_text())
|
||
except (json.JSONDecodeError, OSError):
|
||
pass
|
||
mappings[plugin_name] = str(path)
|
||
mapping_file.write_text(json.dumps(mappings, indent=2))
|
||
return True
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# PL 注入
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class PLValidationError(Exception):
|
||
"""PL 校验错误"""
|
||
pass
|
||
|
||
|
||
class PLInjector:
|
||
"""PL 注入管理器 - 带完整安全限制"""
|
||
|
||
MAX_FUNCTIONS_PER_PLUGIN = 50
|
||
MAX_REGISTRATIONS_PER_NAME = 10
|
||
MAX_NAME_LENGTH = 128
|
||
MAX_DESCRIPTION_LENGTH = 256
|
||
|
||
_FUNCTION_NAME_RE = re.compile(r'^[a-zA-Z0-9_:/\-.]+$')
|
||
_EVENT_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_.]+$')
|
||
_ROUTE_PATH_RE = re.compile(r'^/[a-zA-Z0-9_\-/.]+$')
|
||
_FORBIDDEN_ROUTE_PATTERNS = [r'\.\.', r'//', r'/\.', r'~', r'\%']
|
||
|
||
def __init__(self, plugin_manager: 'PluginManager'):
|
||
self._plugin_manager = plugin_manager
|
||
self._injections: dict = {}
|
||
self._injection_registry: dict = {}
|
||
self._plugin_function_count: dict = {}
|
||
|
||
def check_and_load_pl(self, plugin_dir: Path, plugin_name: str) -> bool:
|
||
"""检查并加载 PL 文件夹,返回 True 表示成功"""
|
||
pl_dir = plugin_dir / "PL"
|
||
if not pl_dir.exists() or not pl_dir.is_dir():
|
||
Log.warn("Core", f"插件 '{plugin_name}' 声明了 pl_injection,但缺少 PL/ 文件夹,拒绝加载")
|
||
return False
|
||
|
||
pl_main = pl_dir / "main.py"
|
||
if not pl_main.exists():
|
||
Log.warn("Core", f"插件 '{plugin_name}' 的 PL/ 文件夹中缺少 main.py,拒绝加载")
|
||
return False
|
||
|
||
# 禁止危险文件类型
|
||
forbidden_ext = {'.sh', '.bat', '.exe', '.dll', '.so', '.dylib', '.bin'}
|
||
for f in pl_dir.rglob('*'):
|
||
if f.suffix.lower() in forbidden_ext:
|
||
Log.error("Core", f"插件 '{plugin_name}' 的 PL/ 文件夹包含危险文件: {f.name},拒绝加载")
|
||
return False
|
||
|
||
try:
|
||
# 受限沙箱
|
||
safe_builtins = {
|
||
'True': True, 'False': False, 'None': None,
|
||
'dict': dict, 'list': list, 'str': str, 'int': int,
|
||
'float': float, 'bool': bool, 'tuple': tuple, 'set': set,
|
||
'len': len, 'range': range, 'enumerate': enumerate,
|
||
'zip': zip, 'map': map, 'filter': filter,
|
||
'sorted': sorted, 'reversed': reversed,
|
||
'min': min, 'max': max, 'sum': sum, 'abs': abs,
|
||
'round': round, 'isinstance': isinstance, 'issubclass': issubclass,
|
||
'type': type, 'id': id, 'hash': hash, 'repr': repr,
|
||
'print': print, 'object': object, 'property': property,
|
||
'staticmethod': staticmethod, 'classmethod': classmethod,
|
||
'super': super, 'iter': iter, 'next': next,
|
||
'any': any, 'all': all, 'callable': callable,
|
||
'hasattr': hasattr, 'getattr': getattr, 'setattr': setattr,
|
||
'ValueError': ValueError, 'TypeError': TypeError,
|
||
'KeyError': KeyError, 'IndexError': IndexError,
|
||
'Exception': Exception, 'BaseException': BaseException,
|
||
}
|
||
safe_globals = {
|
||
'__builtins__': safe_builtins,
|
||
'__name__': f'plugin.{plugin_name}.PL',
|
||
'__package__': f'plugin.{plugin_name}.PL',
|
||
'__file__': str(pl_main),
|
||
}
|
||
|
||
with open(pl_main, 'r', encoding='utf-8') as f:
|
||
source = f.read()
|
||
|
||
# 静态源码安全检查
|
||
self._static_source_check(source, str(pl_main))
|
||
|
||
code = compile(source, str(pl_main), 'exec')
|
||
exec(code, safe_globals)
|
||
|
||
register_func = safe_globals.get('register')
|
||
if register_func and callable(register_func):
|
||
register_func(self)
|
||
Log.ok("Core", f"插件 '{plugin_name}' PL 注入成功")
|
||
else:
|
||
Log.warn("Core", f"插件 '{plugin_name}' 的 PL/main.py 缺少 register() 函数,但仍允许加载")
|
||
|
||
self._injections[plugin_name] = {"dir": str(pl_dir)}
|
||
return True
|
||
|
||
except PLValidationError as e:
|
||
Log.error("Core", f"插件 '{plugin_name}' PL 安全检查失败: {e}")
|
||
return False
|
||
except SyntaxError as e:
|
||
Log.error("Core", f"插件 '{plugin_name}' PL/main.py 语法错误: {e}")
|
||
return False
|
||
except FileNotFoundError as e:
|
||
Log.error("Core", f"插件 '{plugin_name}' PL 文件不存在:{e}")
|
||
return False
|
||
except PermissionError as e:
|
||
Log.error("Core", f"插件 '{plugin_name}' PL 文件权限错误:{e}")
|
||
return False
|
||
except Exception as e:
|
||
Log.error("Core", f"加载插件 '{plugin_name}' 的 PL 失败:{type(e).__name__}: {e}")
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
def _static_source_check(self, source: str, file_path: str):
|
||
"""静态源码安全检查 - 增强版,防止字符串拼接/编码绕过"""
|
||
import base64
|
||
|
||
# 首先检查是否有 base64 编码的恶意代码
|
||
try:
|
||
string_pattern = r'([A-Za-z0-9+/=]{20,})'
|
||
for match in re.finditer(string_pattern, source):
|
||
try:
|
||
decoded = base64.b64decode(match.group(1)).decode('utf-8', errors='ignore')
|
||
for dangerous in ['import ', 'exec(', 'eval(', 'compile(', 'os.', 'sys.', 'subprocess']:
|
||
if dangerous in decoded:
|
||
raise PLValidationError(f"{file_path} - 检测到 base64 编码的恶意代码")
|
||
except Exception:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
|
||
# 检查字符串拼接绕过
|
||
concat_patterns = [
|
||
r"""['"]ex['"]\s*\+\s*['"]ec['"]""",
|
||
r"""['"]impor['"]\s*\+\s*['"]t['"]""",
|
||
r"""['"]eva['"]\s*\+\s*['"]l['"]""",
|
||
r"""['"]compil['"]\s*\+\s*['"]e['"]""",
|
||
]
|
||
for pattern in concat_patterns:
|
||
if re.search(pattern, source):
|
||
raise PLValidationError(f"{file_path} - 检测到字符串拼接绕过尝试")
|
||
|
||
forbidden = [
|
||
(r'^\s*import\s+(os|sys|subprocess|shutil|socket|ctypes|cffi|multiprocessing|threading)', '禁止导入系统级模块'),
|
||
(r'^\s*from\s+(os|sys|subprocess|shutil|socket|ctypes|cffi|multiprocessing|threading)\s+import', '禁止导入系统级模块'),
|
||
(r'__import__\s*\(', '禁止使用 __import__'),
|
||
(r'(?<![a-zA-Z_])exec\s*\(', '禁止使用 exec'),
|
||
(r'(?<![a-zA-Z_])eval\s*\(', '禁止使用 eval'),
|
||
(r'(?<![a-zA-Z_])compile\s*\(', '禁止使用 compile'),
|
||
(r'(?<![a-zA-Z_])open\s*\(', '禁止直接操作文件'),
|
||
(r'__builtins__', '禁止访问 __builtins__'),
|
||
(r'getattr\s*\(\s*__builtins__', '禁止通过 getattr 访问 __builtins__'),
|
||
(r'setattr\s*\(', '禁止使用 setattr'),
|
||
(r'type\s*\(\s*\(\s*[\'"]', '禁止使用 type 动态创建类'),
|
||
]
|
||
for line_num, line in enumerate(source.split('\n'), 1):
|
||
stripped = line.strip()
|
||
if not stripped or stripped.startswith('#'):
|
||
continue
|
||
for pattern, msg in forbidden:
|
||
if re.search(pattern, stripped):
|
||
raise PLValidationError(f"{file_path}:{line_num} - {msg}: '{stripped}'")
|
||
|
||
def _validate_function_name(self, name: str) -> bool:
|
||
if not name or not isinstance(name, str):
|
||
return False
|
||
if len(name) > self.MAX_NAME_LENGTH:
|
||
return False
|
||
return bool(self._FUNCTION_NAME_RE.match(name))
|
||
|
||
def _validate_route_path(self, path: str) -> bool:
|
||
if not path or not isinstance(path, str):
|
||
return False
|
||
if len(path) > 256:
|
||
return False
|
||
if not self._ROUTE_PATH_RE.match(path):
|
||
return False
|
||
for p in self._FORBIDDEN_ROUTE_PATTERNS:
|
||
if re.search(p, path):
|
||
return False
|
||
return True
|
||
|
||
def _validate_event_name(self, event_name: str) -> bool:
|
||
if not event_name or not isinstance(event_name, str):
|
||
return False
|
||
if len(event_name) > self.MAX_NAME_LENGTH:
|
||
return False
|
||
return bool(self._EVENT_NAME_RE.match(event_name))
|
||
|
||
def _check_plugin_limit(self, plugin_name: str) -> bool:
|
||
count = self._plugin_function_count.get(plugin_name, 0)
|
||
if count >= self.MAX_FUNCTIONS_PER_PLUGIN:
|
||
Log.warn("Core", f"插件 '{plugin_name}' 注册功能数已达上限 ({self.MAX_FUNCTIONS_PER_PLUGIN})")
|
||
return False
|
||
return True
|
||
|
||
def _check_name_limit(self, name: str) -> bool:
|
||
registrations = self._injection_registry.get(name, [])
|
||
if len(registrations) >= self.MAX_REGISTRATIONS_PER_NAME:
|
||
Log.warn("Core", f"功能名称 '{name}' 注册次数已达上限 ({self.MAX_REGISTRATIONS_PER_NAME})")
|
||
return False
|
||
return True
|
||
|
||
def _wrap_function(self, func: Callable, plugin_name: str, name: str) -> Callable:
|
||
"""包装函数,异常安全"""
|
||
def _safe_wrapper(*args, **kwargs):
|
||
try:
|
||
return func(*args, **kwargs)
|
||
except Exception as e:
|
||
Log.error("Core", f"PL 注入功能 '{name}' (来自 {plugin_name}) 执行异常: {e}")
|
||
return None
|
||
return _safe_wrapper
|
||
|
||
def _get_caller_plugin_name(self) -> Optional[str]:
|
||
"""通过栈帧回溯获取调用者插件名"""
|
||
stack = traceback.extract_stack()
|
||
for frame in stack:
|
||
filename = frame.filename
|
||
if '/PL/' in filename and 'main.py' in filename:
|
||
parts = Path(filename).parts
|
||
for i, part in enumerate(parts):
|
||
if part == 'PL':
|
||
return parts[i - 1] if i > 0 else None
|
||
return None
|
||
|
||
def register_function(self, name: str, func: Callable, description: str = ""):
|
||
"""注册注入功能 - 带参数校验和权限限制"""
|
||
if not self._validate_function_name(name):
|
||
Log.error("Core", f"PL 注入功能名称非法: '{name}'")
|
||
return
|
||
if not callable(func):
|
||
Log.error("Core", f"PL 注入功能 '{name}' 不是可调用对象")
|
||
return
|
||
if description and len(description) > self.MAX_DESCRIPTION_LENGTH:
|
||
description = description[:self.MAX_DESCRIPTION_LENGTH]
|
||
|
||
plugin_name = self._get_caller_plugin_name() or "unknown"
|
||
|
||
if not self._check_plugin_limit(plugin_name):
|
||
return
|
||
if not self._check_name_limit(name):
|
||
return
|
||
|
||
wrapped_func = self._wrap_function(func, plugin_name, name)
|
||
|
||
if name not in self._injection_registry:
|
||
self._injection_registry[name] = []
|
||
self._injection_registry[name].append({
|
||
"func": wrapped_func, "plugin": plugin_name, "description": description,
|
||
})
|
||
self._plugin_function_count[plugin_name] = self._plugin_function_count.get(plugin_name, 0) + 1
|
||
Log.tip("Core", f"PL 注入功能已注册: '{name}' (来自 {plugin_name})")
|
||
|
||
def register_route(self, method: str, path: str, handler: Callable):
|
||
"""注册 HTTP 路由 - 带路径安全校验"""
|
||
valid_methods = {'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'}
|
||
method_upper = method.upper()
|
||
if method_upper not in valid_methods:
|
||
Log.error("Core", f"PL 注入路由方法非法: '{method}'")
|
||
return
|
||
if not self._validate_route_path(path):
|
||
Log.error("Core", f"PL 注入路由路径非法: '{path}'")
|
||
return
|
||
self.register_function(f"{method_upper}:{path}", handler, f"路由 {method_upper} {path}")
|
||
|
||
def register_event_handler(self, event_name: str, handler: Callable):
|
||
"""注册事件处理器 - 带名称校验"""
|
||
if not self._validate_event_name(event_name):
|
||
Log.error("Core", f"PL 注入事件名称非法: '{event_name}'")
|
||
return
|
||
self.register_function(f"event:{event_name}", handler, f"事件 {event_name}")
|
||
|
||
def get_injected_functions(self, name: str = None) -> list[Callable]:
|
||
if name:
|
||
return [e["func"] for e in self._injection_registry.get(name, [])]
|
||
return [f for es in self._injection_registry.values() for f in [e["func"] for e in es]]
|
||
|
||
def get_injection_info(self, plugin_name: str = None) -> dict:
|
||
if plugin_name:
|
||
return self._injections.get(plugin_name, {})
|
||
return dict(self._injections)
|
||
|
||
def has_injection(self, plugin_name: str) -> bool:
|
||
return plugin_name in self._injections
|
||
|
||
def get_registry_info(self) -> dict:
|
||
info = {}
|
||
for name, entries in self._injection_registry.items():
|
||
info[name] = {
|
||
"count": len(entries),
|
||
"plugins": [e["plugin"] for e in entries],
|
||
"descriptions": [e["description"] for e in entries],
|
||
}
|
||
return info
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# PluginManager — 核心管理器
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
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()
|
||
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 trusted_keys_dir.exists():
|
||
for kf in 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 插件文件
|
||
|
||
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
|
||
|
||
try:
|
||
instance, info = self.nbpf_loader.load(nbpf_path, plugin_name)
|
||
name = info["name"]
|
||
|
||
# 构建 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 _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}")
|
||
|
||
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, store_dir: str = "store"):
|
||
if 'plugin' not in sys.modules:
|
||
pkg = types.ModuleType('plugin')
|
||
pkg.__path__ = []
|
||
pkg.__package__ = 'plugin'
|
||
sys.modules['plugin'] = pkg
|
||
Log.tip("Core", "已创建 plugin 命名空间包")
|
||
|
||
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 _load_plugins_from_dir(self, store_dir: Path):
|
||
if not store_dir.exists():
|
||
return
|
||
core_plugins = set()
|
||
skip = {"Core", "archive"}
|
||
plugin_dirs = []
|
||
for ad in store_dir.iterdir():
|
||
if ad.is_dir():
|
||
for pd in ad.iterdir():
|
||
if pd.name in skip:
|
||
continue
|
||
# 支持目录插件(main.py)和 .nbpf 文件
|
||
if pd.is_dir() and (pd / "main.py").exists():
|
||
priority = 100
|
||
manifest_file = pd / "manifest.json"
|
||
if manifest_file.exists():
|
||
try:
|
||
meta = json.loads(manifest_file.read_text()).get("metadata", {})
|
||
raw = meta.get("load_priority", 100)
|
||
priority = 0 if raw == "first" else (int(raw) if isinstance(raw, (int, float)) else 100)
|
||
except (json.JSONDecodeError, OSError, (ValueError, TypeError)):
|
||
pass
|
||
plugin_dirs.append((priority, pd))
|
||
elif pd.suffix == ".nbpf":
|
||
# .nbpf 文件,优先级 50(在普通插件之前加载)
|
||
plugin_dirs.append((50, pd))
|
||
plugin_dirs.sort(key=lambda x: x[0])
|
||
for _, pd in plugin_dirs:
|
||
self.load(pd, use_sandbox=pd.name not in core_plugins)
|
||
self._link_capabilities()
|
||
|
||
def _check_any_plugins(self, store_dir: str) -> bool:
|
||
sp = Path(store_dir)
|
||
if sp.exists():
|
||
for ad in sp.iterdir():
|
||
if ad.is_dir():
|
||
for pd in ad.iterdir():
|
||
if pd.name in {"Core", "archive"}:
|
||
continue
|
||
if pd.is_dir() and (pd / "main.py").exists():
|
||
return True
|
||
if pd.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 PermissionError 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 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()
|
||
except Exception as e:
|
||
Log.error("Core", f"启动失败 {name}: {e}")
|
||
|
||
def _get_ordered_plugins(self) -> list[str]:
|
||
try:
|
||
return [n for n in self.dependency_resolver.resolve() if n in self.plugins]
|
||
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()
|
||
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),
|
||
}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 类型注册
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
register_plugin_type("PluginManager", PluginManager)
|
||
register_plugin_type("PluginInfo", PluginInfo)
|
||
register_plugin_type("CapabilityRegistry", CapabilityRegistry)
|
||
register_plugin_type("PLInjector", PLInjector)
|
||
register_plugin_type("Lifecycle", Lifecycle)
|
||
register_plugin_type("LifecycleManager", LifecycleManager)
|
||
register_plugin_type("DependencyResolver", DependencyResolver)
|
||
register_plugin_type("SignatureVerifier", SignatureVerifier)
|
||
register_plugin_type("IntegrityChecker", IntegrityChecker)
|
||
register_plugin_type("AuditLogger", AuditLogger)
|
||
register_plugin_type("TamperMonitor", TamperMonitor)
|
||
register_plugin_type("FallbackManager", FallbackManager)
|
||
register_plugin_type("DataStore", DataStore) |