feat: 新增脚手架/开发模式/权限白名单/system-monitor插件
- nebula create mod/key/list-templates 模组脚手架 - nebula dev 开发模式热重载 - manifest permissions.imports 权限白名单机制 - system-monitor 系统监控仪表盘插件 - 默认端口统一为 10086 - 修复 _init_nbpf 误读 Ed25519 私钥为 RSA 的 bug - 更新 README.md 文档
This commit is contained in:
302
oss/cli.py
302
oss/cli.py
@@ -147,7 +147,7 @@ def info(ctx):
|
||||
|
||||
@cli.command(name="cli")
|
||||
@click.option('--connect-host', default='127.0.0.1', help='后端地址(默认 127.0.0.1)')
|
||||
@click.option('--connect-port', default=8080, help='后端端口(默认 8080)')
|
||||
@click.option('--connect-port', default=10086, help='后端端口(默认 10086)')
|
||||
def cli_command(connect_host, connect_port):
|
||||
"""启动 TUI 前端(前后端分离,连接已有后端)"""
|
||||
click.echo("NebulaShell TUI 客户端(待实现)")
|
||||
@@ -373,6 +373,306 @@ def keygen(output_dir, name):
|
||||
click.echo(f" nebula nbpf pack ./my-plugin --ed25519-key {private_dir / f'{name}_ed25519.pem'} --rsa-key {private_dir / f'{name}_rsa.pem'} --rsa-pub {rsa_dir / f'{name}.pem'} --signer {name}")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# create 命令 — 模组脚手架
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
@cli.group()
|
||||
def create():
|
||||
"""创建模组/密钥等资源"""
|
||||
pass
|
||||
|
||||
|
||||
@create.command("mod")
|
||||
@click.argument("name", type=str, required=False, default=None)
|
||||
@click.option("--author", "-a", type=str, default=None, help="作者名")
|
||||
@click.option("--description", "-d", type=str, default=None, help="模组描述")
|
||||
@click.option("--type", "-t", "mod_type", type=click.Choice(["example", "adapter", "service", "security", "tool"]), default="example", help="模组类型")
|
||||
@click.option("--with-keys", is_flag=True, default=False, help="同时生成签名密钥")
|
||||
@click.option("--output", "-o", type=str, default=None, help="输出目录")
|
||||
@click.pass_context
|
||||
def create_mod(ctx, name, author, description, mod_type, with_keys, output):
|
||||
"""创建新模组脚手架"""
|
||||
import string as _string
|
||||
from pathlib import Path as _Path
|
||||
|
||||
# 交互式输入(如果参数缺失)
|
||||
if not name:
|
||||
name = click.prompt("📛 模组名称", type=str)
|
||||
if not author:
|
||||
author = click.prompt("👤 作者", type=str, default="anonymous")
|
||||
if not description:
|
||||
description = click.prompt("📝 描述", type=str, default="")
|
||||
|
||||
# 校验模组名
|
||||
valid_chars = set(_string.ascii_lowercase + _string.digits + "-_")
|
||||
safe_name = "".join(c for c in name.lower().replace(" ", "-") if c in valid_chars)
|
||||
if not safe_name:
|
||||
click.echo("❌ 模组名称无效,请使用字母、数字、连字符")
|
||||
raise click.Abort()
|
||||
|
||||
# 确定输出目录
|
||||
output_dir = _Path(output or safe_name)
|
||||
if output_dir.exists():
|
||||
click.echo(f"❌ 目录 '{output_dir}' 已存在")
|
||||
raise click.Abort()
|
||||
|
||||
# 渲染模板
|
||||
templates_dir = _Path(__file__).parent / "templates" / "mod"
|
||||
if not templates_dir.exists():
|
||||
click.echo("❌ 模板目录不存在,请检查安装")
|
||||
raise click.Abort()
|
||||
|
||||
# 替换变量
|
||||
replacements = {
|
||||
"{{ mod_name }}": safe_name,
|
||||
"{{ author }}": author,
|
||||
"{{ description }}": description,
|
||||
"{{ mod_type }}": mod_type,
|
||||
}
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for tmpl_file in templates_dir.iterdir():
|
||||
if tmpl_file.is_file():
|
||||
content_tmpl = tmpl_file.read_text(encoding="utf-8")
|
||||
for old, new in replacements.items():
|
||||
content_tmpl = content_tmpl.replace(old, new)
|
||||
out_path = output_dir / tmpl_file.name
|
||||
out_path.write_text(content_tmpl, encoding="utf-8")
|
||||
click.echo(f" ✅ 创建: {out_path.name}")
|
||||
|
||||
click.echo("")
|
||||
click.echo(f"🎉 模组 '{safe_name}' 创建成功!")
|
||||
click.echo(f"📂 位置: {output_dir.resolve()}")
|
||||
click.echo("")
|
||||
click.echo("下一步:")
|
||||
click.echo(f" cd {safe_name}")
|
||||
click.echo(" # 编辑 main.py 实现功能")
|
||||
click.echo(" # 然后打包:")
|
||||
click.echo(f' nebula nbpf pack ./{safe_name} -o mods/{safe_name}.nbpf --ed25519-key <key> --rsa-key <key> --rsa-pub <key> --signer "{author}"')
|
||||
|
||||
# 可选生成密钥
|
||||
if with_keys:
|
||||
click.echo("")
|
||||
click.echo("🔑 正在生成签名密钥...")
|
||||
try:
|
||||
from oss.core.nbpf.crypto import NBPCrypto
|
||||
keys_dir = output_dir / "keys"
|
||||
keys_dir.mkdir(exist_ok=True)
|
||||
|
||||
ed_priv, ed_pub = NBPCrypto.generate_ed25519_keypair()
|
||||
(keys_dir / "ed25519.pem").write_bytes(ed_priv)
|
||||
(keys_dir / "ed25519.pub.pem").write_bytes(ed_pub)
|
||||
|
||||
rsa_priv, rsa_pub = NBPCrypto.generate_rsa_keypair(key_size=2048)
|
||||
(keys_dir / "rsa.pem").write_bytes(rsa_priv)
|
||||
(keys_dir / "rsa.pub.pem").write_bytes(rsa_pub)
|
||||
|
||||
click.echo(f" ✅ Ed25519 密钥: {keys_dir}/ed25519.pem")
|
||||
click.echo(f" ✅ RSA 密钥: {keys_dir}/rsa.pem")
|
||||
click.echo("")
|
||||
click.echo("打包命令:")
|
||||
click.echo(f" nebula nbpf pack ./{safe_name} -o mods/{safe_name}.nbpf")
|
||||
click.echo(f" --ed25519-key {keys_dir}/ed25519.pem")
|
||||
click.echo(f" --rsa-key {keys_dir}/rsa.pem")
|
||||
click.echo(f" --rsa-pub {keys_dir}/rsa.pub.pem")
|
||||
except Exception as e:
|
||||
click.echo(f" ⚠ 密钥生成失败: {e}")
|
||||
|
||||
|
||||
@create.command("key")
|
||||
@click.option("--output", "-o", type=str, default="./keys", help="密钥输出目录")
|
||||
@click.option("--name", type=str, default="default", help="密钥名称")
|
||||
def create_key(output, name):
|
||||
"""生成 Ed25519 + RSA 签名密钥对"""
|
||||
from oss.core.nbpf.crypto import NBPCrypto
|
||||
from pathlib import Path as _Path
|
||||
|
||||
output_path = _Path(output)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
click.echo(f"🔑 生成密钥对到: {output_path.resolve()}")
|
||||
|
||||
ed_priv, ed_pub = NBPCrypto.generate_ed25519_keypair()
|
||||
(output_path / f"{name}_ed25519.pem").write_bytes(ed_priv)
|
||||
(output_path / f"{name}_ed25519.pub.pem").write_bytes(ed_pub)
|
||||
click.echo(f" ✅ Ed25519: {output_path / f'{name}_ed25519.pem'}")
|
||||
|
||||
rsa_priv, rsa_pub = NBPCrypto.generate_rsa_keypair(key_size=2048)
|
||||
(output_path / f"{name}_rsa.pem").write_bytes(rsa_priv)
|
||||
(output_path / f"{name}_rsa.pub.pem").write_bytes(rsa_pub)
|
||||
click.echo(f" ✅ RSA: {output_path / f'{name}_rsa.pem'}")
|
||||
|
||||
click.echo("")
|
||||
click.echo("密钥生成完成!")
|
||||
|
||||
|
||||
@create.command("list-templates")
|
||||
def list_templates():
|
||||
"""列出可用的模板"""
|
||||
from pathlib import Path as _Path
|
||||
templates_base = _Path(__file__).parent / "templates"
|
||||
if not templates_base.exists():
|
||||
click.echo("没有可用的模板")
|
||||
return
|
||||
for tdir in templates_base.iterdir():
|
||||
if tdir.is_dir():
|
||||
files = [f.name for f in tdir.iterdir() if f.is_file()]
|
||||
click.echo(f" 📦 {tdir.name}/")
|
||||
for f in files:
|
||||
click.echo(f" ├── {f}")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# dev 命令 — 开发模式热重载
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
@cli.command()
|
||||
@click.argument("mod_dir", type=str, required=False, default=None)
|
||||
@click.option("--port", "-p", type=int, default=None, help="HTTP API 端口")
|
||||
@click.option("--host", type=str, default=None, help="监听地址")
|
||||
@click.option("--skip-sign", is_flag=True, default=False, help="跳过签名验证(调试用)")
|
||||
@click.pass_context
|
||||
def dev(ctx, mod_dir, port, host, skip_sign):
|
||||
"""开发模式 — 监听模组文件变化并自动热重载"""
|
||||
import time as _time
|
||||
import hashlib as _hashlib
|
||||
from pathlib import Path as _Path
|
||||
from oss.core.watcher import FileWatcher
|
||||
from oss.logger.logger import Log as _Log
|
||||
|
||||
config = ctx.obj.get("config")
|
||||
if port:
|
||||
config.set("HTTP_API_PORT", port)
|
||||
else:
|
||||
config.set("HTTP_API_PORT", 10086)
|
||||
if host:
|
||||
config.set("HOST", host)
|
||||
|
||||
# 确定监听目录
|
||||
watch_dirs = []
|
||||
if mod_dir:
|
||||
mod_path = _Path(mod_dir).resolve()
|
||||
if not mod_path.exists():
|
||||
click.echo(f"❌ 目录不存在: {mod_dir}")
|
||||
raise click.Abort()
|
||||
watch_dirs.append(mod_path)
|
||||
click.echo(f"📁 监听目录: {mod_path}")
|
||||
else:
|
||||
# 默认监听 mods/ 和当前目录
|
||||
watch_dirs.append(_Path.cwd())
|
||||
click.echo(f"📁 监听目录: {_Path.cwd()}")
|
||||
click.echo("")
|
||||
|
||||
# 启动 NebulaShell 服务
|
||||
from oss.core.manager import PluginManager as _PluginManager
|
||||
|
||||
plugin_mgr = _PluginManager()
|
||||
plugin_mgr.load_all()
|
||||
# 同时加载 mods/ 目录下的 .nbpf 模组
|
||||
from pathlib import Path as _P
|
||||
mods_path = _P("mods")
|
||||
if mods_path.exists():
|
||||
for f in sorted(mods_path.iterdir()):
|
||||
if f.suffix == ".nbpf":
|
||||
plugin_mgr.load(f)
|
||||
plugin_mgr.start_all()
|
||||
|
||||
# 启动 HTTP 服务
|
||||
try:
|
||||
plugin_mgr.start_http_server()
|
||||
_Log.ok("Dev", f"HTTP API: http://{config.host}:{config.http_api_port}")
|
||||
except Exception as e:
|
||||
_Log.warn("Dev", f"HTTP 服务启动失败: {e}")
|
||||
|
||||
click.echo("")
|
||||
click.echo("🔧 NebulaShell 开发模式已启动")
|
||||
click.echo("=" * 50)
|
||||
click.echo(f" HTTP: http://{config.host}:{config.http_api_port}")
|
||||
click.echo(f" 监听: {', '.join(str(d) for d in watch_dirs)}")
|
||||
click.echo(f" 签名验证: {'跳过' if skip_sign else '开启'}")
|
||||
click.echo(f" 模组数: {len(plugin_mgr.plugins)}")
|
||||
click.echo("=" * 50)
|
||||
click.echo(" 按 Ctrl+C 停止")
|
||||
click.echo("")
|
||||
|
||||
# 文件变更缓存
|
||||
_file_hashes: dict[str, str] = {}
|
||||
|
||||
def _get_file_hash(path: _Path) -> str:
|
||||
"""计算文件 hash"""
|
||||
try:
|
||||
return _hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _get_dir_hash(directory: _Path) -> dict[str, str]:
|
||||
"""获取目录下所有文件的 hash"""
|
||||
result = {}
|
||||
for f in sorted(directory.rglob("*")):
|
||||
if f.is_file() and ".nbpf" not in f.suffix and "__pycache__" not in str(f):
|
||||
h = _get_file_hash(f)
|
||||
if h:
|
||||
result[str(f)] = h
|
||||
return result
|
||||
|
||||
# 初始化 hash
|
||||
for wd in watch_dirs:
|
||||
if wd.is_dir():
|
||||
_file_hashes.update(_get_dir_hash(wd))
|
||||
|
||||
# 主循环
|
||||
try:
|
||||
while True:
|
||||
_time.sleep(1)
|
||||
changed = False
|
||||
|
||||
for wd in watch_dirs:
|
||||
if not wd.exists():
|
||||
continue
|
||||
current = _get_dir_hash(wd)
|
||||
# 检查新增/修改
|
||||
for fpath, h in current.items():
|
||||
old_h = _file_hashes.get(fpath)
|
||||
if old_h is None:
|
||||
_Log.info("Dev", f"🆕 新增文件: {_Path(fpath).name}")
|
||||
changed = True
|
||||
elif old_h != h:
|
||||
_Log.info("Dev", f"📝 文件变更: {_Path(fpath).name}")
|
||||
changed = True
|
||||
_file_hashes[fpath] = h
|
||||
# 检查删除
|
||||
for fpath in list(_file_hashes.keys()):
|
||||
if fpath not in current:
|
||||
_Log.info("Dev", f"🗑 文件删除: {_Path(fpath).name}")
|
||||
_file_hashes.pop(fpath)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
_Log.info("Dev", "检测到变更,尝试热重载...")
|
||||
try:
|
||||
# 重新加载所有模组
|
||||
plugin_mgr.stop_all()
|
||||
# 清空并重新加载
|
||||
plugin_mgr.plugins.clear()
|
||||
plugin_mgr._plugin_dirs.clear()
|
||||
plugin_mgr.load_all()
|
||||
from pathlib import Path as _P2
|
||||
for f in sorted(_P2("mods").iterdir()):
|
||||
if f.suffix == ".nbpf":
|
||||
plugin_mgr.load(f)
|
||||
plugin_mgr.start_all()
|
||||
_Log.ok("Dev", f"热重载完成!当前模组数: {len(plugin_mgr.plugins)}")
|
||||
except Exception as e:
|
||||
_Log.error("Dev", f"热重载失败: {e}")
|
||||
except KeyboardInterrupt:
|
||||
click.echo("")
|
||||
_Log.info("Dev", "正在停止开发模式...")
|
||||
plugin_mgr.stop_all()
|
||||
_Log.info("Dev", "开发模式已停止")
|
||||
|
||||
|
||||
def main():
|
||||
cmd_name = os.path.basename(sys.argv[0])
|
||||
if cmd_name in ("oss", "oss.exe"):
|
||||
|
||||
@@ -16,8 +16,8 @@ class Config:
|
||||
|
||||
DEFAULTS = {
|
||||
# 服务器配置
|
||||
"HTTP_API_PORT": 8080,
|
||||
"HTTP_TCP_PORT": 8082,
|
||||
"HTTP_API_PORT": 10086,
|
||||
"HTTP_TCP_PORT": 10086,
|
||||
"HOST": "127.0.0.1",
|
||||
|
||||
# 数据目录
|
||||
|
||||
@@ -29,7 +29,7 @@ class HttpServer:
|
||||
def __init__(self, router, middleware, host=None, port=None):
|
||||
config = get_config()
|
||||
self.host = host or config.get("HOST", "127.0.0.1")
|
||||
self.port = port or config.get("HTTP_API_PORT", 8080)
|
||||
self.port = port or config.get("HTTP_API_PORT", 10086)
|
||||
self.router = router
|
||||
self.middleware = middleware
|
||||
self._server = None
|
||||
|
||||
@@ -154,11 +154,16 @@ class PluginManager:
|
||||
name = kf.stem
|
||||
trusted_rsa[name] = kf.read_bytes()
|
||||
|
||||
# 加载 RSA 私钥
|
||||
# 加载 RSA 私钥(只匹配名称包含 rsa 的文件,避免误读 Ed25519 私钥)
|
||||
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"))
|
||||
pk_files = [f for f in private_dir.glob("*.pem") if "rsa" in f.name.lower()]
|
||||
if not pk_files:
|
||||
# 回退:匹配任意私钥(警告日志)
|
||||
pk_files = list(private_dir.glob("*.pem"))
|
||||
if pk_files:
|
||||
Log.warn("Core", "未找到名称包含 'rsa' 的私钥文件,尝试加载第一个 .pem 文件(可能导致类型错误)")
|
||||
if pk_files:
|
||||
rsa_private = pk_files[0].read_bytes()
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class NIRCompiler:
|
||||
|
||||
# ── 编译 ──
|
||||
|
||||
def compile_source(self, source: str, filename: str = "<nbpf>") -> bytes:
|
||||
def compile_source(self, source: str, filename: str = "<nbpf>", allowed_imports: list[str] = None) -> bytes:
|
||||
"""将 Python 源码编译为序列化的 code object
|
||||
|
||||
Args:
|
||||
@@ -61,7 +61,7 @@ class NIRCompiler:
|
||||
"""
|
||||
try:
|
||||
# 静态安全检查
|
||||
self._static_check(source, filename)
|
||||
self._static_check(source, filename, allowed_imports or [])
|
||||
|
||||
# 编译为 code object
|
||||
code = compile(source, filename, 'exec')
|
||||
@@ -79,11 +79,12 @@ class NIRCompiler:
|
||||
except Exception as e:
|
||||
raise NIRCompileError(f"编译失败: {type(e).__name__}: {e}") from e
|
||||
|
||||
def compile_plugin(self, plugin_dir: Path) -> dict[str, bytes]:
|
||||
def compile_plugin(self, plugin_dir: Path, allowed_imports: list[str] = None) -> dict[str, bytes]:
|
||||
"""编译整个插件目录为 NIR
|
||||
|
||||
Args:
|
||||
plugin_dir: 插件目录路径
|
||||
allowed_imports: 允许导入的系统模块白名单(来自 manifest permissions.imports)
|
||||
|
||||
Returns:
|
||||
{module_name: nir_bytes} 字典
|
||||
@@ -105,7 +106,7 @@ class NIRCompiler:
|
||||
module_name = rel_path.replace(".py", "").replace("/", ".")
|
||||
if module_name.endswith(".__init__"):
|
||||
module_name = module_name[:-9] # 去掉 .__init__
|
||||
nir_data[module_name] = self.compile_source(source, str(plugin_dir / rel_path))
|
||||
nir_data[module_name] = self.compile_source(source, str(plugin_dir / rel_path), allowed_imports)
|
||||
|
||||
return nir_data
|
||||
|
||||
@@ -163,7 +164,7 @@ class NIRCompiler:
|
||||
|
||||
# ── 静态安全检查 ──
|
||||
|
||||
def _static_check(self, source: str, filename: str):
|
||||
def _static_check(self, source: str, filename: str, allowed_imports: list[str] = None):
|
||||
"""静态源码安全检查"""
|
||||
try:
|
||||
tree = ast.parse(source, filename=filename)
|
||||
@@ -174,12 +175,12 @@ class NIRCompiler:
|
||||
# 检查 import 语句
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
self._check_module(alias.name, node.lineno)
|
||||
self._check_module(alias.name, node.lineno, allowed_imports)
|
||||
|
||||
# 检查 from ... import 语句
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module:
|
||||
self._check_module(node.module, node.lineno)
|
||||
self._check_module(node.module, node.lineno, allowed_imports)
|
||||
|
||||
# 检查 __import__ 调用
|
||||
elif isinstance(node, ast.Call):
|
||||
@@ -196,12 +197,16 @@ class NIRCompiler:
|
||||
f"{filename}:{node.lineno} - 禁止使用 {node.func.id}()"
|
||||
)
|
||||
|
||||
def _check_module(self, module_name: str, lineno: int):
|
||||
"""检查模块是否被禁止"""
|
||||
def _check_module(self, module_name: str, lineno: int, allowed_imports: list[str] = None):
|
||||
"""检查模块是否被禁止(支持白名单豁免)"""
|
||||
base = module_name.split(".")[0]
|
||||
if base in self.FORBIDDEN_MODULES:
|
||||
# 检查是否在白名单中
|
||||
if allowed_imports and base in allowed_imports:
|
||||
return # 白名单放行
|
||||
raise NIRCompileError(
|
||||
f"第 {lineno} 行 - 禁止导入系统模块: '{module_name}'"
|
||||
f"(如需使用请在 manifest.json 的 permissions.imports 中声明)"
|
||||
)
|
||||
|
||||
def _reject_c_extensions(self, plugin_dir: Path):
|
||||
|
||||
@@ -114,9 +114,16 @@ class NBPFPacker:
|
||||
# 1. 读取 manifest
|
||||
manifest = self._read_manifest(plugin_dir)
|
||||
|
||||
# 2. 编译所有 .py 文件为 NIR
|
||||
# 2. 编译所有 .py 文件为 NIR(传入 manifest 权限白名单)
|
||||
Log.info("NBPF", f"编译插件: {plugin_dir.name}")
|
||||
nir_data = self.compiler.compile_plugin(plugin_dir)
|
||||
perms = manifest.get("permissions", {})
|
||||
if isinstance(perms, dict):
|
||||
allowed_imports = perms.get("imports", [])
|
||||
else:
|
||||
allowed_imports = [] # 旧的数组格式,不开放系统模块
|
||||
if allowed_imports:
|
||||
Log.info("NBPF", f"已授权导入: {allowed_imports}")
|
||||
nir_data = self.compiler.compile_plugin(plugin_dir, allowed_imports=allowed_imports)
|
||||
|
||||
# 3. 收集资源文件
|
||||
res_files = self._collect_resources(plugin_dir)
|
||||
|
||||
@@ -55,6 +55,7 @@ class NBPFLoader:
|
||||
self.trusted_ed25519_keys = trusted_ed25519_keys or {}
|
||||
self.trusted_rsa_keys = trusted_rsa_keys or {}
|
||||
self.rsa_private_key = rsa_private_key
|
||||
self._current_allowed_imports: list[str] = []
|
||||
|
||||
def load(
|
||||
self,
|
||||
@@ -112,7 +113,12 @@ class NBPFLoader:
|
||||
meta = manifest.get("metadata", {})
|
||||
name = plugin_name or meta.get("name", nbpf_path.stem)
|
||||
|
||||
# 9. 反序列化并执行
|
||||
# 9. 反序列化并执行(传入 imports 白名单)
|
||||
perms = manifest.get("permissions", {})
|
||||
if isinstance(perms, dict):
|
||||
self._current_allowed_imports = perms.get("imports", [])
|
||||
else:
|
||||
self._current_allowed_imports = []
|
||||
instance, module = self._deserialize_and_exec(nir_data, name)
|
||||
|
||||
# 10. 构建插件信息
|
||||
@@ -326,7 +332,7 @@ class NBPFLoader:
|
||||
"""反序列化 NIR 并执行,返回 (instance, module)"""
|
||||
|
||||
# 构建安全的全局命名空间
|
||||
safe_globals = self._build_safe_globals(plugin_name)
|
||||
safe_globals = self._build_safe_globals(plugin_name, self._current_allowed_imports)
|
||||
|
||||
# 按依赖顺序执行模块
|
||||
main_module = None
|
||||
@@ -365,9 +371,12 @@ class NBPFLoader:
|
||||
|
||||
return instance, main_module
|
||||
|
||||
def _build_safe_globals(self, plugin_name: str) -> dict:
|
||||
def _build_safe_globals(self, plugin_name: str, allowed_imports: list[str] = None) -> dict:
|
||||
"""构建安全的全局命名空间
|
||||
|
||||
如果插件在 manifest 中声明了 imports 权限,将 `__import__` 加回内置函数,
|
||||
并用白名单包装器限制只能导入声明的模块。
|
||||
|
||||
注意:Python 沙箱无法完全阻止通过 ()__class__.__bases__[0].__subclasses__()
|
||||
等反射方式逃逸。本沙箱仅用于防止意外访问危险模块,真正的安全隔离
|
||||
需要 OS 级容器化。
|
||||
@@ -390,6 +399,19 @@ class NBPFLoader:
|
||||
'KeyError': KeyError, 'IndexError': IndexError,
|
||||
'Exception': Exception, 'BaseException': BaseException,
|
||||
}
|
||||
|
||||
# 如果插件声明了 imports 权限,添加白名单 __import__
|
||||
if allowed_imports:
|
||||
_allowed_set = set(allowed_imports)
|
||||
def _safe_import(name, *args, **kwargs):
|
||||
base = name.split(".")[0]
|
||||
if base not in _allowed_set:
|
||||
raise ImportError(
|
||||
f"模块 '{name}' 不在权限白名单中。"
|
||||
f"请在 manifest.json 的 permissions.imports 中声明: {sorted(_allowed_set)}"
|
||||
)
|
||||
return __import__(name, *args, **kwargs)
|
||||
safe_builtins['__import__'] = _safe_import
|
||||
return {
|
||||
'__builtins__': safe_builtins,
|
||||
'__name__': f'nbpf.{plugin_name}',
|
||||
|
||||
24
oss/templates/mod/README.md
Normal file
24
oss/templates/mod/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# @{{ author }}/{{ mod_name }}
|
||||
|
||||
{{ description }}
|
||||
|
||||
## 安装
|
||||
|
||||
将 `{{ mod_name }}.nbpf` 放入 NebulaShell 的 `mods/` 目录即可。
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 打包
|
||||
nebula nbpf pack ./{{ mod_name }} -o {{ mod_name }}.nbpf \
|
||||
--ed25519-key ./keys/ed25519.pem \
|
||||
--rsa-key ./keys/rsa.pem \
|
||||
--signer "{{ author }}"
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
50
oss/templates/mod/main.py
Normal file
50
oss/templates/mod/main.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
@{{ author }}/{{ mod_name }}
|
||||
{{ description }}
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# 模组信息(可选,用于动态获取)
|
||||
NAME = "{{ mod_name }}"
|
||||
VERSION = "0.1.0"
|
||||
|
||||
|
||||
def init(deps):
|
||||
"""
|
||||
模组初始化。
|
||||
deps 包含:
|
||||
- deps["services"] — 其他模组注册的服务
|
||||
- deps["config"] — 当前模组的配置
|
||||
- deps["logger"] — 日志工具
|
||||
"""
|
||||
logger = deps.get("logger")
|
||||
if logger:
|
||||
logger.info(f"{NAME} v{VERSION} 初始化完成")
|
||||
|
||||
|
||||
def start():
|
||||
"""模组启动。init 成功后调用。"""
|
||||
pass
|
||||
|
||||
|
||||
def stop():
|
||||
"""模组停止。框架关闭时调用,释放资源。"""
|
||||
pass
|
||||
|
||||
|
||||
def reload(config: dict):
|
||||
"""热重载配置(可选)"""
|
||||
pass
|
||||
|
||||
|
||||
def health() -> dict:
|
||||
"""健康检查(可选)"""
|
||||
return {"status": "ok", "version": VERSION}
|
||||
|
||||
|
||||
def stats() -> dict:
|
||||
"""统计信息(可选)"""
|
||||
return {}
|
||||
22
oss/templates/mod/manifest.json
Normal file
22
oss/templates/mod/manifest.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@{{ author }}/{{ mod_name }}",
|
||||
"version": "0.1.0",
|
||||
"description": "{{ description }}",
|
||||
"author": "{{ author }}",
|
||||
"license": "MIT",
|
||||
"type": "{{ mod_type }}",
|
||||
"main": "main.py",
|
||||
"enabled": true,
|
||||
"priority": 999,
|
||||
"runtime": {
|
||||
"language": "python",
|
||||
"entry_point": "main.py",
|
||||
"requirements": []
|
||||
},
|
||||
"capabilities": [],
|
||||
"services": {
|
||||
"provides": [],
|
||||
"consumes": []
|
||||
},
|
||||
"config": {}
|
||||
}
|
||||
Reference in New Issue
Block a user