From c501a4a5cb84c98d31b87010765ca5f4806978c7 Mon Sep 17 00:00:00 2001 From: zizi Date: Sat, 1 Aug 2026 01:24:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A1=86=E6=9E=B6(=E8=AF=84=E6=B5=8B=E9=9A=94?= =?UTF-8?q?=E7=A6=BB):=20=E5=AE=9E=E7=8E=B0=E5=85=AD=E5=8E=9F=E5=88=99?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E6=93=8E=E5=BC=BA=E5=88=B6=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DDL 103:run_type 插入后不可变触发器(评测性锚点钉死);生产只读角色 muse_read + RLS(评测候选对生产读默认不可见,app.eval_visible opt-in 才可见);已验证 root可见/muse_read不可见/opt-in可见 - invariant_checks.py:常设不变量(从靠预防迁到靠发现)——I1评测候选未接受/I2正文来源不追溯评测候选/I3评测质量结果可过滤/I4 oracle红线/I5 COMPLETED轮次封存(声明==实际落库),已跑通全PASS - 原则1/2(口号升级/代价明账)已落 SoT;原则3/4/5/6 实现层完成 --- .../runtime/scripts/invariant_checks.py | 96 +++++++++++++++++++ db/ddl/103-example评测隔离引擎强制.sql | 48 ++++++++++ 2 files changed, 144 insertions(+) create mode 100644 .claude/skills/runtime/scripts/invariant_checks.py create mode 100644 db/ddl/103-example评测隔离引擎强制.sql diff --git a/.claude/skills/runtime/scripts/invariant_checks.py b/.claude/skills/runtime/scripts/invariant_checks.py new file mode 100644 index 0000000..211cb4a --- /dev/null +++ b/.claude/skills/runtime/scripts/invariant_checks.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""常设不变量检查(harness 改造原则 5:从靠预防迁到靠发现)。 + +逻辑隔离不能全靠预防,必须靠持续发现——从物理隔离迁到逻辑隔离的那一刻, +必须同时从"靠预防"迁到"靠发现"。本脚本跑一组常设不变量,违反即报警(退出码非 0)。 +建议常设运行(CI / 看板健康面),违反即报警。 + +检查项: +I1 没有评测候选被接受(run_type=eval/diagnostic 且 state=accepted)——引擎已防,此为发现兜底。 +I2 没有正式正文的来源能追溯到评测候选(归因 source_object_id → 评测候选)。 +I3 评测候选的质量结果可被生产视图过滤(带评测标记,不混入生产质量视图)。 +I4 oracle 读侧红线:oracle/标准答案以 kind='oracle' 标记,生产模型输入不得包含(read-context 生产路径保证;此处校验标记完整)。 +I5 COMPLETED 轮次封存:一轮声明的 raw 集合 == 实际落库集合(lease 声明的 content_hashes 数 == 实际 raw_content 行数)。 + +跑法:.venv/bin/python .claude/skills/runtime/scripts/invariant_checks.py [--run-id X] +""" +import argparse +import json +import sys +from pathlib import Path + +DB_SCRIPTS = Path(__file__).resolve().parents[2] / "db" / "scripts" +sys.path.insert(0, str(DB_SCRIPTS)) +from db import connect # noqa: E402 + + +def _count(conn, sql, params=()): + return conn.execute(sql, params).fetchone()[0] + + +def check_invariants(run_id=None): + """返回 [(检查项, 是否通过, 说明)]。""" + results = [] + with connect(readonly=True) as conn: + # I1 没有评测候选被接受 + n = _count(conn, "SELECT count(*) FROM example_candidate " + "WHERE run_type IN ('eval','diagnostic') AND state='accepted'") + results.append(("I1 评测候选未被接受", n == 0, + f"{n} 个评测候选被接受" if n else "无评测候选被接受")) + + # I2 没有正式正文来源追溯到评测候选 + n = _count(conn, "SELECT count(*) FROM muse_content_block_source_attribution a " + "JOIN example_candidate c ON c.id::text = a.source_object_id " + "WHERE c.run_type IN ('eval','diagnostic')") + results.append(("I2 正式正文来源不追溯到评测候选", n == 0, + f"{n} 个正式正文来源追溯到评测候选" if n else "无正式正文来源追溯到评测候选")) + + # I3 评测候选的质量结果可被生产视图过滤(评测候选的质量结果都能关联到评测候选,即可过滤) + n = _count(conn, "SELECT count(*) FROM example_quality_result q " + "JOIN example_candidate c ON c.candidate_sha256 = q.candidate_sha256 " + "WHERE c.run_type IN ('eval','diagnostic') AND q.candidate_sha256 IS NULL") + results.append(("I3 评测质量结果可过滤", n == 0, + f"{n} 个评测质量结果无法关联评测候选(无法过滤)" if n else "评测质量结果均可关联过滤")) + + # I4 oracle 读侧红线:oracle raw 行都带 kind='oracle' 标记(生产路径据此排除) + n = _count(conn, "SELECT count(*) FROM example_raw_content " + "WHERE kind='oracle' AND content_sha256 IS NULL") + results.append(("I4 oracle 标记完整", n == 0, + f"{n} 个 oracle raw 缺哈希标记" if n else "oracle raw 标记完整")) + + # I5 COMPLETED 轮次封存:lease 声明的 content_hashes 数 == 实际 raw_content 行数 + if run_id: + leases = conn.execute( + "SELECT id, content_hashes FROM example_raw_lease WHERE run_id=%s", (run_id,)).fetchall() + else: + leases = conn.execute("SELECT id, content_hashes FROM example_raw_lease").fetchall() + seal_violations = 0 + for lease_id, content_hashes in leases: + declared = len(content_hashes) if isinstance(content_hashes, list) else 0 + actual = _count(conn, "SELECT count(*) FROM example_raw_content WHERE lease_id=%s", (lease_id,)) + if declared != actual: + seal_violations += 1 + results.append(("I5 轮次封存(声明==实际落库)", seal_violations == 0, + f"{seal_violations} 个租约声明与实际落库不符" if seal_violations else "所有租约封存一致")) + + return results + + +def main(): + ap = argparse.ArgumentParser(description="常设不变量检查(从靠预防迁到靠发现)") + ap.add_argument("--run-id", default=None, help="只检查某轮的轮次封存") + args = ap.parse_args() + results = check_invariants(run_id=args.run_id) + all_pass = True + for name, ok, msg in results: + print(f"[{'PASS' if ok else 'VIOLATION'}] {name}:{msg}") + if not ok: + all_pass = False + if not all_pass: + print("\n有不变量被违反——逻辑隔离出现越界,须排查。", file=sys.stderr) + sys.exit(1) + print("\n所有不变量通过。") + + +if __name__ == "__main__": + main() diff --git a/db/ddl/103-example评测隔离引擎强制.sql b/db/ddl/103-example评测隔离引擎强制.sql new file mode 100644 index 0000000..39ac31d --- /dev/null +++ b/db/ddl/103-example评测隔离引擎强制.sql @@ -0,0 +1,48 @@ +-- 评测隔离引擎强制(harness 改造原则 4:强制收拢到引擎单点 + run_type 钉死)。 +-- 设计拍板 2026-08-01(docs/2026-08-01-评测harness改造设计.md 原则 4/6)。 +-- 核心:评测与生产同库,靠逻辑隔离(访问分离);强制必须压到引擎单点,不靠每条查询自觉过滤。 +-- 1) run_type 插入后不可变——评测性这个锚点钉死,不能先改成 production 再接受。 +-- 2) (eval, accepted) 永不可达——既有 trg_example_candidate_no_eval_accept 已守;run_type 不可变后更硬。 +-- 3) 生产只读角色 muse_read + RLS——评测行对生产读默认不可见,显式 opt-in(app.eval_visible)才可见。 +-- 注意:root 是 superuser,RLS 对其无效;muse_read 是非超级用户角色,RLS 对它生效。 +-- 生产读路径应改用 muse_read 连接(切换为后续代码改动),admin/看板用 root 可见全部(含评测,带标记)。 + +-- ── 1) run_type 插入后不可变 ── +CREATE OR REPLACE FUNCTION reject_run_type_change() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +BEGIN + IF NEW.run_type IS DISTINCT FROM OLD.run_type THEN + RAISE EXCEPTION 'run_type 插入后不可变(评测隔离引擎强制,原 % 新 %)', OLD.run_type, NEW.run_type; + END IF; + RETURN NEW; +END; +$$; + +CREATE OR REPLACE TRIGGER trg_example_candidate_run_type_immutable +BEFORE UPDATE ON example_candidate +FOR EACH ROW EXECUTE FUNCTION reject_run_type_change(); + +-- ── 2) 生产只读角色 muse_read(非超级用户,RLS 对其生效)── +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'muse_read') THEN + CREATE ROLE muse_read NOLOGIN; + END IF; +END +$$; + +GRANT SELECT ON example_candidate TO muse_read; +GRANT SELECT ON example_run TO muse_read; +GRANT SELECT ON example_run_receipt TO muse_read; +GRANT SELECT ON example_quality_result TO muse_read; + +-- ── 3) RLS:评测候选对生产读默认不可见,显式 opt-in(app.eval_visible='true')才可见 ── +ALTER TABLE example_candidate ENABLE ROW LEVEL SECURITY; +ALTER TABLE example_candidate FORCE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS eval_isolation_candidate ON example_candidate; +CREATE POLICY eval_isolation_candidate ON example_candidate + FOR SELECT + USING (run_type IS DISTINCT FROM 'eval' OR current_setting('app.eval_visible', true) = 'true');