fix(import_closure): 破M1b无限退回死循环——globals()运行时注入逃逸阀+责任范围过滤(2026-09-17 pbls实锤)

根因1(逃逸阀缺口): agent用工具脚本生成db.py compat块(globals()[_name]=_val逐符号注入)修import闭包断裂,
运行时实测全通(pbl_blueprint.db/api/blueprint_crud/subobjects/templates import OK,PblError/require_tenant/
get_env全True),但ast静态量不出Subscript下标赋值→门禁恒报20处断裂→QC按'声称修复vs引擎实测断裂直接矛盾'
判造假证据→4轮退回达上限→fault→项目paused。与PEP562 __getattr__同类:静态不可判定即保守放行。
识别三形态: globals()[k]=v(含For/Try嵌套walk_top可达) / globals().update({...}) / _g=globals()别名。
函数体内globals()不算模块级注入(walk_top只走模块级stmts,不误放行)。

根因2(责任范围过宽): 依赖方向拉进来的外部文件(pbl_agent_runtime/api.py因import pbl_blueprint入范围)的
断裂源在pbl_common(半迁移存量欠账114处,别的任务的账)——违背门禁自己声明的'任务未触碰的包存量断裂不拦,
防死锁'原则,M1b实测26处里6处是pbl_common欠账,小任务永远修不完全空间账。
修: problems过滤 source.split('.')[0] in owned_pkgs(只拦涉及仓库自有包的契约断裂)。
原pbl_common事故形状防护不受影响: 重写pbl_common的任务owned_pkgs含pbl_common,断裂全保留。

修后M1b剩余真实断裂仅1处: m1b_common.py:114 from .errors import PblError而errors.py只有
PblBlueprintError——门禁拦它是对的(补一行别名即闭环),FAIL消息可行动。
本地走查6例通过(注入3形态识别/普通模块不误伤/函数内globals不误放行/真实compat块形状)。
This commit is contained in:
ymq 2026-09-17 22:13:51 +08:00
parent 2607176cb7
commit 12f8384d49

View File

@ -15,9 +15,18 @@ ImportError,公共内核对外契约瘫痪。设计文档明文「接口变更
2. 对每个 .py 收集模块级名字绑定(def/class/赋值/import 别名,含 If/Try 2. 对每个 .py 收集模块级名字绑定(def/class/赋值/import 别名,含 If/Try
分支),对每条 from-import 核验符号在目标模块有定义(含再导出/子模块)。 分支),对每条 from-import 核验符号在目标模块有定义(含再导出/子模块)。
3. 逃逸阀(防误报死锁):目标模块定义 module-level __getattr__(PEP 562 3. 逃逸阀(防误报死锁):目标模块定义 module-level __getattr__(PEP 562
动态属性)→ 跳过符号级核验;ast 解析失败跳过(语法归空壳门禁);外部库 动态属性)或 module-level `globals()[...] = ...` 运行时符号注入(2026-09-17
pbls M1b 死循环根治:agent 用工具脚本生成 compat 注入块修断裂,运行时
import 全通但 ast 量不出下标赋值 → 门禁恒报断裂 → QC 判「声称修复 vs
实测断裂」矛盾 → 无限退回;与 PEP 562 同类,静态不可判定即保守放行)
→ 跳过该模块符号级核验;ast 解析失败跳过(语法归空壳门禁);外部库
(顶层名不在包映射)不核验;本核验器自身异常 → 返回 None 放行(记日志)。 (顶层名不在包映射)不核验;本核验器自身异常 → 返回 None 放行(记日志)。
责任范围过滤(2026-09-17 同日修正):依赖方向拉进来的外部文件,只拦
「引用涉及包」的断裂;它引用其他包(如半迁移欠账 pbl_common)的存量断裂
不是本任务责任,不进门禁清单——否则小任务背上全空间欠账,永远修不完
(pbls M1b 实测:26 处里 6 处是 pbl_agent_runtime→pbl_common 存量断裂)。
门禁范围(check_closure_for_delivery): 门禁范围(check_closure_for_delivery):
- 本任务写入 .py 所属的包 = 涉及包;核验对象 = 涉及包内全部文件 + 全空间 - 本任务写入 .py 所属的包 = 涉及包;核验对象 = 涉及包内全部文件 + 全空间
import 涉及包的文件。改公共包断掉依赖方(事故形状)会在 deliver 当轮被拦, import 涉及包的文件。改公共包断掉依赖方(事故形状)会在 deliver 当轮被拦,
@ -45,11 +54,16 @@ def _add_target(t, names):
def _collect_defs(tree): def _collect_defs(tree):
"""模块级名字绑定集合 + 是否定义 __getattr__ + 星号导入 (module, level) 列表。""" """模块级名字绑定集合 + 是否定义 __getattr__/globals()注入 + 星号导入列表。"""
names = set() names = set()
dynamic = False dynamic = False
stars = [] stars = []
def _is_ns_call(node):
"""globals()/vars()/locals() 调用形态(运行时符号注入,静态不可判定)。"""
return (isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
and node.func.id in ("globals", "vars", "locals"))
def walk_top(stmts): def walk_top(stmts):
nonlocal dynamic nonlocal dynamic
for s in stmts: for s in stmts:
@ -61,6 +75,19 @@ def _collect_defs(tree):
elif isinstance(s, ast.Assign): elif isinstance(s, ast.Assign):
for t in s.targets: for t in s.targets:
_add_target(t, names) _add_target(t, names)
# globals()[name] = val / vars()[...] = ... 运行时注入:
# 符号面静态不可判定 → 整模块按动态放行(同 PEP 562 逃逸阀)
if (isinstance(t, ast.Subscript) and _is_ns_call(t.value)):
dynamic = True
if _is_ns_call(s.value):
# X = globals() 之后再 X[...] = ...:保守也视为注入形态
dynamic = True
elif isinstance(s, ast.Expr) and isinstance(s.value, ast.Call):
# globals().update({...}) 形态
fn = s.value.func
if (isinstance(fn, ast.Attribute) and fn.attr == "update"
and _is_ns_call(fn.value)):
dynamic = True
elif isinstance(s, ast.AnnAssign): elif isinstance(s, ast.AnnAssign):
if s.target is not None: if s.target is not None:
_add_target(s.target, names) _add_target(s.target, names)
@ -342,6 +369,13 @@ def check_closure_for_delivery(space_dir, written_files, max_problems=30):
problems = [] problems = []
for f in sorted(targets): for f in sorted(targets):
problems.extend(ck.check_file(f)) problems.extend(ck.check_file(f))
# 责任范围过滤(2026-09-17 pbls M1b 死循环修正):只保留「断裂源在
# 涉及仓库自有包」的问题——即本任务改动面自己的契约断裂(含依赖方
# import 涉及包断裂 = pbl_common 事故形状)。断裂源在其他包(如
# pbl_common 半迁移存量欠账)的不是本任务责任:小任务修不完全空间
# 欠账,硬拦只会无限退回(实测 26 处里 6 处是别的包的存量账)。
problems = [p for p in problems
if p.get("source", "").split(".")[0] in owned_pkgs]
# 去重 # 去重
seen, uniq = set(), [] seen, uniq = set(), []
for p in problems: for p in problems: