Compare commits

...
4 Commits
Author SHA1 Message Date
chakcy e9879ea2a3 fix: 加密 mbank 把密钥写进包内(运行端 get_key() 才取得到)
运行端 public/utils/launch.py 的 init_module_bank 用 PythonToSQLite.get_key() 从
**包内**取密钥,它不读旁挂 <包>.key —— 只写旁挂文件的话,--encrypt 产物启动即报
"模块 'plugins' 的字节码是加密的,但没有提供解密密钥",插件全部加载失败、契约
校验不过,网关直接起不来。

打包时改为一并调 packer.packer_key(key)(module-bank 0.4+ 存在包根部 packer_key,
不进模块清单);旁挂 .key 保留,分发/测试口径不变。
2026-10-14 15:29:25 +08:00
chakcy 7bdd80a7a5 modify: 0.3.6 2026-10-14 11:24:07 +08:00
chakcy bafca77436 feat: 打包时烘焙授权决策(--license 变为可选,消除砖头包风险)
prepare 阶段把"是否授权版"写进 chongming/_build_flags.py(随产物编译、部署侧
不可改):
- --license + 公钥:烘焙授权版 —— 强制校验,公钥随代码内置,现场即便把
  config.toml 里的公钥换成自己的并自签 license 也过不了验签;同时保留原有
  注入产物配置(仅作镜像/可见性)
- 不加 --license:烘焙非授权版 —— 不校验授权、插件照常加载(与开发态一致),
  即"打包不配 license"是合法选项,不再是拒绝所有插件的砖头包
- --license 缺公钥:默认仍让构建失败;ALLOW_NO_KEY=1 显式放行时降级为
  非授权版(旧行为是产出砖头包)
- 公钥在构建期校验格式,非法即终止构建;烘焙文件的常量名与网关读取端
  chongming.app.core.licensing 对齐
2026-10-14 10:57:14 +08:00
chakcy fe50c70b03 docs: 去掉 README 中过期的 src/chongming/scripts 引用 2026-10-13 17:29:26 +08:00
3 changed files with 83 additions and 17 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# chongming-cli
chongming 网关的命令脚本集合,独立包维护(从 `src/chongming/scripts` 抽离)。
chongming 网关的命令脚本集合,独立包维护。
## 命令
+1 -1
View File
@@ -4,7 +4,7 @@ requires = ["hatchling"]
[project]
name = "chongming-cli"
version = "0.3.5"
version = "0.3.6"
description = "chongming 命令脚本集合:构建打包 / 进程插件打包 / 插件脚手架 / 模型生成 / 机器码授权签发"
readme = "README.md"
requires-python = ">=3.12"
+81 -15
View File
@@ -27,14 +27,16 @@
做 PyArmor 混淆。**默认关闭**(产物为明文,默认只适合内部
调试;对外分发必须加这个参数)
--license 打开授权:注入厂商授权公钥(缺公钥则构建失败)、跑授权私钥
泄漏守卫。**默认关闭**(不注入、不守卫、缺公钥也不报错,
产物无法校验任何授权;对外分发必须加这个参数)
泄漏守卫。**默认关闭**(license 是可选构建项:不加则产物为
"非授权版"——不校验授权、插件照常加载,与开发态一致;
加上则产物为"授权版"——强制校验,现场改配置/环境变量关不掉。
两者由构建期烘焙进产物,见 chongming/_build_flags.py)
示例:
uv run build --only mbank # 依赖未变时,仅重新打包 .mbank 模块文件
uv run build nuitka --only mbank,prepare
uv run build --encrypt --license # 对外分发:加密模块 + 注入授权公钥
uv run build # 内部调试:明文模块、不带授权
uv run build --encrypt --license # 对外分发:加密模块 + 授权版产物
uv run build # 内部/无授权版:明文模块、不校验授权
依赖收集(--only pack 生效):
打包收集清单 = 根 config.toml [pyinstaller_build_import](网关本体依赖)
@@ -99,7 +101,7 @@ class BuildOptions:
"""构建开关(命令行可选参数,默认全部关闭 = 明文调试产物)"""
encrypt: bool = False # --encrypt: 打开 mbank 加密与 PyArmor 混淆
license: bool = False # --license: 打开授权公钥注入与私钥泄漏守卫
license: bool = False # --license: 授权版产物(注入公钥 + 烘焙强制校验);默认非授权版
@dataclass
@@ -281,6 +283,11 @@ def _pack_mbank(
encrypt=False(默认,加 --encrypt 才打开)时传 key=None:module-bank 存
**明文**字节码,模块元信息里 encrypted=False,加载端不需要密钥也能导入。
加密时密钥写两处:旁挂 ``<包>.key``(分发/测试口径)**和包内根部的
``packer_key``**(运行端口径)—— ``public/utils/launch.py`` 的 init_module_bank
是用 ``PythonToSQLite.get_key()`` 取密钥的,它只读包内那份;只写旁挂文件的话
加密产物启动时会报"模块 'plugins' 的字节码是加密的,但没有提供解密密钥",
插件全部加载失败、契约校验不过,网关直接起不来。
"""
# 每次从空文件开始:旧包里的历史版本既让体积膨胀,也会留下重名/空的版本
Path(output_path).unlink(missing_ok=True)
@@ -300,6 +307,8 @@ def _pack_mbank(
packer.pack_directory(source_dir, package_name)
packer.verify_package_structure()
packer.delete_source_code(None)
if key:
packer.packer_key(key)
packer.sign_version("my_key", key_id="ci-2026")
# 给"当前这一版(已签名的)"起名,不派生工作版本 —— 加载端跟最新走就是它
packer.vfs.rename_snapshot(packer.vfs.snapshot_id, "v1.0.0")
@@ -387,33 +396,46 @@ def _stage_prepare_build_dir(config: Config, mode: str, options: BuildOptions) -
jwt_key = Encryption.generate_key()
content = content.replace("your-secret-key-change-in-production", jwt_key)
# 注入厂商授权公钥(机器码授权)—— 默认不做,加 --license 才注入
# 注入厂商授权公钥(机器码授权)—— 默认不做,加 --license 才注入;
# 两条路径都会把"本产物是否为授权版"烘焙进 chongming/_build_flags.py
# (编译进产物、部署侧不可改)—— 未加 --license 的产物是不校验授权、
# 插件照常加载的正常版本,而不是"拒绝所有插件"的砖头包。
if options.license:
pubkey = _resolve_license_public_key()
if pubkey:
from chongming_license import public_key_fingerprint
content = _set_license_pubkey(content, pubkey)
_write_build_flags(enforce=True, public_key=pubkey)
print(
f" ✓ 已注入厂商授权公钥(指纹 {public_key_fingerprint(pubkey)}):"
"产物为授权版,强制校验授权(现场改配置/环境变量关不掉)"
)
elif os.environ.get(ALLOW_NO_KEY_ENV) == "1":
# 公钥缺失时打包产物会拒绝**全部**插件(没人能证明被授权)= 砖头设备,
# 故默认让构建失败;仅内部演示允许显式放行。
# 无公钥 = 谁都证明不了被授权。此时**不再**烘焙"授权版"(那会拒绝
# 全部插件 = 砖头设备),而是烘焙非授权版:插件照常加载、不校验授权。
_write_build_flags(enforce=False)
print(
f" ⚠️ 未提供授权公钥且设置了 {ALLOW_NO_KEY_ENV}=1:"
"本产物无法校验任何授权(所有插件都不会加载),仅限内部演示"
"本产物为非授权版(不校验授权,插件照常加载),仅限内部演示"
)
else:
raise RuntimeError(
"缺少厂商授权公钥:打包产物会拒绝加载所有插件(设备变砖)。\n"
"缺少厂商授权公钥:无法产出可校验授权的产物。\n"
f" 请用环境变量 {LICENSE_PUBKEY_ENV}=<base64 公钥> 或 "
f"{LICENSE_PUBKEY_FILE_ENV}=<文件路径> 提供,\n"
f" 或把公钥写入 {DEFAULT_LICENSE_PUBKEY_FILE}"
"(由 `uv run chongming-license keygen` 输出)。\n"
f" 仅内部演示无授权构建可去掉 --license,"
f"或设 {ALLOW_NO_KEY_ENV}=1 强制放行。"
" 不需要授权校验的内部/演示构建请**去掉 --license**"
f"(产物不校验、插件照常加载),或设 {ALLOW_NO_KEY_ENV}=1 强制放行。"
)
else:
# 默认不注入:产物不带厂商授权信任根(模板里已写着的公钥也不动)
# 非授权版:明确烘焙"不校验授权"(产物内编译,部署侧改不了)——
# 打包不含 license 因此始终是合法选项,产物与开发态一致、插件照常加载。
_write_build_flags(enforce=False)
print(
" ⚠️ 未注入厂商授权公钥(默认):本产物无法校验任何授权,"
"对外分发请加 --license"
" ⚠️ 未注入厂商授权公钥(默认):本产物为非授权版(不校验授权,"
"插件照常加载),对外分发请加 --license"
)
cfg_path.write_text(content, encoding="utf-8")
@@ -457,6 +479,50 @@ def _resolve_license_public_key() -> str:
return ""
def _write_build_flags(enforce: bool, public_key: str = "") -> Path:
"""把"本产物是否为授权版"烘焙成 ``chongming/_build_flags.py``(随代码编译)。
格式契约与网关读取端 ``chongming.app.core.licensing`` 对齐(常量名一致):
该模块在打包 venv 的网关包内生成,编译进产物后**部署侧改不了** —— 所以
"打包不配 license" 可以是合法选项(烘焙 False = 不校验、插件照常加载),
而授权版产物的强制校验也不会被现场改 config.toml / 环境变量关掉。
授权版同时把公钥烘焙进代码:现场即便把 config.toml 里的公钥换成自己的
并自签一份 license,也过不了内置公钥的验签(配置公钥只在未烘焙时兜底)。
"""
import importlib.util
from chongming_license import load_public_key, public_key_fingerprint
fingerprint = ""
if public_key:
# 失败即构建期配置错误:趁早报错,避免打出一个"谁都验不过"的授权版产物
load_public_key(public_key)
fingerprint = public_key_fingerprint(public_key)
spec = importlib.util.find_spec("chongming")
if spec is None or not spec.submodule_search_locations:
raise RuntimeError(
"未找到 chongming 网关包,无法写入构建期授权标记(_build_flags.py)。\n"
" 请确认构建 venv 已安装 chongming-gateway(uv sync)"
)
target = Path(next(iter(spec.submodule_search_locations))) / "_build_flags.py"
target.write_text(
"\n".join([
'"""构建期烘焙的授权决策 —— 由打包脚本生成,请勿手改。',
"",
"编译进产物后部署侧不可修改:部署目录的 config.toml / 环境变量都影响不了",
"这份决策。读取端:chongming.app.core.licensing。",
'"""',
f"LICENSE_ENFORCE = {enforce!r}",
f'LICENSE_PUBLIC_KEY = "{public_key}"',
f'PUBLIC_KEY_FINGERPRINT = "{fingerprint}"',
]) + "\n",
encoding="utf-8",
)
return target
def _set_license_pubkey(text: str, pubkey: str) -> str:
"""把公钥写进 [license] 段的 public_key,保留其余注释与排版。