Skip to content

Commit 0d7d9ea

Browse files
authored
feat(engine): 权威性由内核自报能力推导,取代写死的常量 (#84)
Rust 内核由另一条线持续演进,而插件此前把「Rust 对所有质量路径固定返回 退出码 3、能力矩阵全 gap」写进 engine.py 的常量与 note。这种断言在内核 落地能力后会静默过期,让插件对外给出与事实相反的说法,且无任何提示。 改为读内核自报:`codeguard capabilities all --format json` 输出 capability_inventory,每个「语言 × 平台 × 检查类别」单元的 status 取 implemented / gap / not_applicable。新增 category_authority() 逐类别判断 签发方——implemented 才交给 Rust,其余(gap、not_applicable、清单不可读、 协议不匹配、找不到该单元)一律留在 Legacy,fail closed。 这样交接变成数据变化(内核自己把某类别标为 implemented),而不是插件改代码; 也支持逐类别交接,不必等内核全部就绪。 engine 子命令新增「内核自报能力」汇总:implemented / gap / not_applicable / unknown 计数,并说明当前全局签发方。 同步修正一处会过期的测试断言:RUST.note 不再要求包含 "gap" 字样,改为要求 指向 capability_inventory 这一事实来源。 验证:21 项引擎契约测试通过 / 架构门禁 OK / ruff 全过 / run_all 144 通过 / 可移植性 PASS / vendor check PASS。
1 parent 0ea342f commit 0d7d9ea

4 files changed

Lines changed: 278 additions & 20 deletions

File tree

‎scripts/check_architecture.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
}
4343

4444
CORE_DEPENDENCIES = {
45-
# 引擎边界:纯数据与退出码翻译,不执行进程(探测由调用方注入)。
46-
"codeguard.engine": {"__future__", "dataclasses", "typing"},
45+
# 引擎边界:纯数据与退出码翻译,不执行进程(探测与能力报告由调用方注入)。
46+
"codeguard.engine": {"__future__", "dataclasses", "typing", "any"},
4747
"codeguard.cve_policy": {"__future__", "dataclasses"},
4848
"codeguard.cve_reports": {"__future__", "json", "math", "codeguard.cve_policy"},
4949
"codeguard.cve_scanners": {"__future__", "tempfile", "pathlib", "codeguard.cve_policy",

‎scripts/codeguard/engine.py‎

Lines changed: 114 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,42 @@
11
"""判定引擎边界:谁签发质量结论,以及各引擎退出码如何翻译成门禁动作。
22
33
插件自带的 Legacy Python 引擎随包发布,可以签发 PASS/FAIL。
4-
Rust ``codeguard`` 组件是后续统一内核,但当前对所有质量路径固定返回退出码 3
5-
(检查未完成),能力矩阵全部条目为 ``gap``。在它自行解除未完成状态之前,
6-
不得用它签发任何质量结论。
4+
Rust ``codeguard`` 组件是后续统一内核,其就绪程度**由它自己声明**——
5+
``codeguard capabilities --format json`` 报出的 ``capability_inventory`` 里,
6+
每个「语言 × 平台 × 检查类别」单元的 ``status`` 取 ``implemented`` / ``gap`` /
7+
``not_applicable``。
78
8-
本模块只做数据与规则,不执行进程:探测结果由调用方注入,便于在不启动外部
9-
工具的情况下验证门禁语义。
9+
因此本模块**不写死**内核是否可用:写死会在内核演进后静默过期,让插件对外
10+
给出与事实相反的说法。权威性按类别逐一判断:
11+
12+
- 该类别在内核自报中为 ``implemented`` → 该类别可由 Rust 签发;
13+
- 为 ``gap``、报告不可读、或内核缺席 → 一律仍由 Legacy 签发。
14+
15+
这样交接是数据变化(内核自己把某个类别标为 implemented),而不是插件改代码。
16+
17+
本模块只做数据与规则,不执行进程:探测与能力报告由调用方注入,便于在不启动
18+
外部工具的情况下验证门禁语义。
1019
"""
1120
from __future__ import annotations
1221

1322
from dataclasses import dataclass
14-
from typing import Literal
23+
from typing import Any, Literal
1524

1625
EngineId = Literal["legacy", "rust"]
1726
Verdict = Literal["PASS", "FAIL", "UNVERIFIED", "INCOMPLETE"]
27+
CapabilityStatus = Literal["implemented", "gap", "not_applicable"]
28+
29+
# 内核能力清单的协议标识;不匹配时视为"不可读",按 gap 处理。
30+
INVENTORY_REPORT_TYPE = "capability_inventory"
1831

1932

2033
@dataclass(frozen=True)
2134
class EngineSpec:
22-
"""一个候选判定引擎的登记信息;权威性是显式事实,不由存在性推导。"""
35+
"""一个候选判定引擎的登记信息。
36+
37+
``authoritative`` 是 Legacy 的静态事实(随插件发布即可签发);
38+
Rust 的权威性是**逐类别**的,见 :func:`category_authority`。
39+
"""
2340

2441
id: EngineId
2542
label: str
@@ -39,11 +56,12 @@ class EngineSpec:
3956
RUST = EngineSpec(
4057
id="rust",
4158
label="Rust codeguard 组件(独立内核)",
59+
# Rust 是否有权威性取决于它自报的能力,不在此写死。
4260
authoritative=False,
4361
version_command=("codeguard", "--version"),
4462
note=(
45-
"对所有质量路径固定返回退出码 3(未完成),能力矩阵全部 gap;"
46-
"只可作只读观察,命令面改用位置参数,无 fix/dockerfile/--lang/--ecosystem/--severity。"
63+
"就绪程度由内核自报的 capability_inventory 决定;"
64+
"命令面改用位置参数,无 fix/dockerfile/--lang/--ecosystem/--severity。"
4765
),
4866
)
4967

@@ -65,6 +83,18 @@ class GateOutcome:
6583
reason: str
6684

6785

86+
@dataclass(frozen=True)
87+
class CategoryAuthority:
88+
"""某个检查类别当前由谁签发,以及依据。"""
89+
90+
language: str
91+
platform: str
92+
category: str
93+
engine: EngineId
94+
status: CapabilityStatus | None
95+
reason: str
96+
97+
6898
# Legacy:0 通过 / 1 无法验证 / 2 存在违规 / 3 参数错误。
6999
LEGACY_EXIT: dict[int, tuple[Verdict, bool]] = {
70100
0: ("PASS", False),
@@ -92,9 +122,7 @@ class GateOutcome:
92122
def translate(engine_id: EngineId, exit_code: int) -> GateOutcome:
93123
"""把某引擎的退出码翻译成门禁动作;未知码一律按未验证处理。"""
94124
spec = ENGINES[engine_id]
95-
verdict, blocks = _EXIT_TABLES[engine_id].get(
96-
exit_code, ("UNVERIFIED", True)
97-
)
125+
verdict, blocks = _EXIT_TABLES[engine_id].get(exit_code, ("UNVERIFIED", True))
98126
reason = spec.note if not spec.authoritative else "Legacy 退出码语义"
99127
return GateOutcome(
100128
engine=engine_id,
@@ -105,11 +133,82 @@ def translate(engine_id: EngineId, exit_code: int) -> GateOutcome:
105133
)
106134

107135

136+
def _iter_cells(inventory: Any):
137+
"""遍历能力清单中的单元,兼容带过滤器的扁平 cells 与未过滤的 languages。"""
138+
if not isinstance(inventory, dict):
139+
return
140+
if isinstance(inventory.get("cells"), list):
141+
yield from (cell for cell in inventory["cells"] if isinstance(cell, dict))
142+
return
143+
for language in inventory.get("languages") or []:
144+
if not isinstance(language, dict):
145+
continue
146+
for platform, categories in (language.get("platforms") or {}).items():
147+
if not isinstance(categories, dict):
148+
continue
149+
for category, cell in categories.items():
150+
if isinstance(cell, dict):
151+
yield {
152+
"language": language.get("language"),
153+
"platform": platform,
154+
"category": category,
155+
**cell,
156+
}
157+
158+
159+
def category_authority(
160+
inventory: Any,
161+
*,
162+
language: str,
163+
platform: str,
164+
category: str,
165+
) -> CategoryAuthority:
166+
"""判断某个「语言 × 平台 × 类别」当前应由谁签发。
167+
168+
只有当内核自报该单元 ``implemented`` 时才交给 Rust;其余情况(gap、
169+
not_applicable、清单不可读、找不到该单元、内核缺席)一律留在 Legacy。
170+
读不到声明不等于通过——这是 fail closed。
171+
"""
172+
readable = (
173+
isinstance(inventory, dict)
174+
and inventory.get("report_type") == INVENTORY_REPORT_TYPE
175+
)
176+
if not readable:
177+
return CategoryAuthority(
178+
language=language, platform=platform, category=category,
179+
engine=LEGACY.id, status=None,
180+
reason="内核能力清单不可读或协议不匹配,权威性留在 Legacy",
181+
)
182+
183+
for cell in _iter_cells(inventory):
184+
if (cell.get("language") == language and cell.get("platform") == platform
185+
and cell.get("category") == category):
186+
status = cell.get("status")
187+
if status == "implemented":
188+
return CategoryAuthority(
189+
language=language, platform=platform, category=category,
190+
engine=RUST.id, status="implemented",
191+
reason="内核自报该检查类别已实现",
192+
)
193+
return CategoryAuthority(
194+
language=language, platform=platform, category=category,
195+
engine=LEGACY.id,
196+
status=status if status in ("gap", "not_applicable") else None,
197+
reason=f"内核自报该类别为 {status or '未知'},权威性留在 Legacy",
198+
)
199+
200+
return CategoryAuthority(
201+
language=language, platform=platform, category=category,
202+
engine=LEGACY.id, status=None,
203+
reason="内核能力清单中无该单元,按未证明处理",
204+
)
205+
206+
108207
def resolve_authoritative(*, rust_available: bool) -> EngineSpec:
109-
"""选择签发质量结论的引擎。
208+
"""在**没有任何类别**自报为 implemented 时选择签发方。
110209
111-
Rust 二进制存在并不改变权威性:只要它仍对质量路径返回未完成,签发方就必须
112-
是 Legacy。``rust_available`` 只影响调用方是否需要提示"检测到新内核"。
210+
逐类别交接请用 :func:`category_authority`;本函数只回答全局兜底问题:
211+
二进制存在本身不构成权威。
113212
"""
114213
if rust_available and not RUST.authoritative:
115214
return LEGACY

‎scripts/engine_report.py‎

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515

1616
sys.path.insert(0, str(Path(__file__).resolve().parent))
1717

18-
from codeguard.engine import ENGINES, resolve_authoritative
18+
from codeguard.engine import (
19+
ENGINES,
20+
INVENTORY_REPORT_TYPE,
21+
_iter_cells,
22+
resolve_authoritative,
23+
)
1924

2025
PROBE_TIMEOUT = 10
2126

@@ -55,8 +60,54 @@ def probe_rust() -> dict:
5560
}
5661

5762

63+
def probe_capability_inventory(path: str) -> dict:
64+
"""读取内核自报的能力清单。
65+
66+
权威性由此决定:插件不写死内核是否可用,而是读它自己声明的
67+
implemented / gap。读不到就按 gap 处理(fail closed)。
68+
"""
69+
try:
70+
proc = subprocess.run(
71+
[path, "capabilities", "all", "--format", "json"],
72+
capture_output=True, text=True, timeout=PROBE_TIMEOUT, check=False,
73+
)
74+
except (OSError, subprocess.SubprocessError) as exc:
75+
return {"readable": False, "reason": f"能力清单读取失败:{exc}"}
76+
if proc.returncode != 0:
77+
return {"readable": False, "reason": f"能力清单命令退出 {proc.returncode}"}
78+
try:
79+
payload = json.loads(proc.stdout)
80+
except ValueError:
81+
return {"readable": False, "reason": "能力清单不是 JSON"}
82+
if not isinstance(payload, dict) or payload.get("report_type") != INVENTORY_REPORT_TYPE:
83+
return {"readable": False, "reason": "缺少 capability_inventory 标记"}
84+
return summarize_inventory(payload)
85+
86+
87+
def summarize_inventory(payload: dict) -> dict:
88+
"""把能力清单汇总成 implemented / gap 计数。"""
89+
counts: dict[str, int] = {}
90+
for cell in _iter_cells(payload):
91+
status = cell.get("status")
92+
key = status if status in ("implemented", "gap", "not_applicable") else "unknown"
93+
counts[key] = counts.get(key, 0) + 1
94+
return {
95+
"readable": True,
96+
"release_version": payload.get("release_version"),
97+
"counts": counts,
98+
"implemented": counts.get("implemented", 0),
99+
"gap": counts.get("gap", 0),
100+
"not_applicable": counts.get("not_applicable", 0),
101+
"unknown": counts.get("unknown", 0),
102+
}
103+
104+
58105
def build_report() -> dict:
59106
rust = probe_rust()
107+
inventory = (
108+
probe_capability_inventory(rust["path"]) if rust["available"]
109+
else {"readable": False, "reason": "Rust 组件缺席"}
110+
)
60111
authoritative = resolve_authoritative(rust_available=rust["available"])
61112
return {
62113
"report_type": "engine",
@@ -72,6 +123,7 @@ def build_report() -> dict:
72123
for spec in (ENGINES["legacy"], ENGINES["rust"])
73124
],
74125
"rust_component": rust,
126+
"rust_capability_inventory": inventory,
75127
}
76128

77129

@@ -83,16 +135,30 @@ def render_human(report: dict) -> str:
83135
lines.append(f" - {entry['label']}:{mark}")
84136
lines.append(f" {entry['note']}")
85137
rust = report["rust_component"]
138+
inv = report["rust_capability_inventory"]
86139
lines.append("")
87140
if rust["available"]:
88141
lines.append(
89142
f"检测到 Rust 组件:{rust['path']}(cli_version={rust['cli_version']},"
90143
f"target={rust['target']},build_identity={rust['build_identity']},"
91144
f"rulepack_compatibility={rust['rulepack_compatibility']})"
92145
)
93-
lines.append(" 它当前不签发质量结论:检测到 ≠ 可用于门禁。")
94146
else:
95147
lines.append(f"未检测到 Rust 组件:{rust['reason']}")
148+
lines.append("")
149+
if inv.get("readable"):
150+
lines.append(
151+
f"内核自报能力(release={inv.get('release_version')}):"
152+
f"implemented {inv['implemented']} / gap {inv['gap']} / "
153+
f"not_applicable {inv['not_applicable']} / unknown {inv['unknown']}"
154+
)
155+
if inv["implemented"] == 0:
156+
lines.append(" 尚无任何类别自报已实现 → 全局签发方保持 Legacy。")
157+
else:
158+
lines.append(" 已有类别自报已实现 → 该类别可由 Rust 签发,"
159+
"逐类别结论见 category_authority。")
160+
else:
161+
lines.append(f"内核能力清单不可读({inv.get('reason')})→ 按未证明处理,签发方保持 Legacy。")
96162
return "\n".join(lines)
97163

98164

0 commit comments

Comments
 (0)