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
分支对每条 from-import 核验符号在目标模块有定义含再导出/子模块
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 放行记日志
责任范围过滤2026-09-17 同日修正依赖方向拉进来的外部文件只拦
引用涉及包的断裂它引用其他包如半迁移欠账 pbl_common的存量断裂
不是本任务责任不进门禁清单否则小任务背上全空间欠账永远修不完
pbls M1b 实测26 处里 6 处是 pbl_agent_runtimepbl_common 存量断裂
门禁范围check_closure_for_delivery
- 本任务写入 .py 所属的包 = 涉及包核验对象 = 涉及包内全部文件 + 全空间
import 涉及包的文件改公共包断掉依赖方事故形状会在 deliver 当轮被拦
@ -45,11 +54,16 @@ def _add_target(t, names):
def _collect_defs(tree):
"""模块级名字绑定集合 + 是否定义 __getattr__ + 星号导入 (module, level) 列表。"""
"""模块级名字绑定集合 + 是否定义 __getattr__/globals()注入 + 星号导入列表。"""
names = set()
dynamic = False
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):
nonlocal dynamic
for s in stmts:
@ -61,6 +75,19 @@ def _collect_defs(tree):
elif isinstance(s, ast.Assign):
for t in s.targets:
_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):
if s.target is not None:
_add_target(s.target, names)
@ -342,6 +369,13 @@ def check_closure_for_delivery(space_dir, written_files, max_problems=30):
problems = []
for f in sorted(targets):
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(), []
for p in problems: