Files
NebulaShell/oss/core/engine.py
Falck 3a096f59a9 重构:核心迁移至 oss/core + NBPF 多重签名加密 + NIR 编译器 + README 全面升级
- 核心功能从 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)
2026-05-05 07:29:43 +08:00

1687 lines
68 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)