OpenMed 隐私证据新鲜度门控:基于注入时钟与聚合报告的本地确定性发布检查
【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed
OpenMed 的隐私发布证据只有在"仍然处于产生它的策略有效期内"时才具备资格。本文将讲解openmed.compliance模块中的证据新鲜度门控(evidence freshness gate)机制:如何用强类型策略定义各类证据的最大年龄、如何注入确定性时钟完成可重放的评估、supersession 语义如何淘汰旧证据,以及为何报告只输出聚合计数而绝不触碰敏感字段。读完本文,你将能在自己的发布流程中直接使用EvidenceFreshnessPolicy、EvidenceRecord、evaluate_evidence_freshness与assert_evidence_freshness构建一个不依赖网络、可重放、隐私安全的发布检查。
新鲜度门控的定位:本地确定性检查,而非合规认证
在 OpenMed 的合规体系中,隐私发布证据(如去标识化发布报告、校准结果)只有在产生它的策略版本仍然生效、且证据年龄未超过策略设定的上限时,才能作为发布依据。核心实现位于 openmed/compliance/evidence_freshness.py,并通过 openmed/compliance/init.py 导出全部公共 API(含EvidenceFreshnessPolicy、EvidenceRecord、EvidenceFreshnessReport、EvidenceFreshnessError、evaluate_evidence_freshness、assert_evidence_freshness等)。
该门控具有三个关键边界:
- 本地且确定性:评估器不会调用
datetime.now(),也不会发起任何网络请求,评估时间完全由调用方注入,因此同一份证据、同一份策略、同一个时钟值必然得到同一个结论; - 只做技术门控:它不宣称底层证据"准确、完整、法律充分或临床安全",仅是发布流水线中的一个技术控制点;
- 聚合输出:报告只包含策略版本、总数、通过/拒绝计数与确定性原因计数,不携带任何记录级引用或时间戳。
定义强类型的年龄策略
每种证据类型(evidence kind)都对应一个datetime.timedelta类型的最大年龄上限。类型被刻意保留为timedelta而非裸整数,这样调用方无法悄悄把单位从"天"改成"小时"或"秒"。策略版本号则进行精确字符串比较,不做任何网络版本发现。
from datetime import timedelta from openmed.compliance import EvidenceFreshnessPolicy policy = EvidenceFreshnessPolicy( policy_version="privacy-v2", age_limits={ "release": timedelta(days=30), "calibration": timedelta(days=7), }, )通配符与优先级
当多种证据类型共享同一个年龄上限时,可使用通配符键"*"作为兜底;某个具体类型一旦在age_limits中出现,其精确条目永远优先于通配符。这一逻辑由max_age_for()实现(见 evidence_freshness.py):先查精确类型,未命中再回落到"*":
def max_age_for(self, evidence_type: str) -> timedelta | None: return self.age_limits.get( evidence_type, self.age_limits.get(_WILDCARD_LIMIT), )构造期的强校验
从源码可以确认,策略构造做了三重约束:
age_limits与旧命名max_age_by_type只能二选一传入,否则抛TypeError(evidence_freshness.py);- 所有上限必须是
datetime.timedelta且非负(_require_duration,见 evidence_freshness.py),传入30这样的裸整数会直接抛TypeError,测试 test_evidence_freshness.py 对此有专门覆盖; policy_version必须是非空的不透明令牌(opaque token),由_SAFE_TOKEN_RE(^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$)校验(evidence_freshness.py)。
age_limits内部会被排序并包装为只读的MappingProxyType,保证策略在评估期间不可变,从而支持可重放性。此外还提供了from_mapping()类方法,允许从{"version": ..., "limits": ...}形式的映射构造策略(evidence_freshness.py),EvidenceAgePolicy则是面向发布规范术语的等价别名。
注入时钟进行可重放的评估
评估必须且只能注入一个时钟来源:as_of(显式评估时刻)、now(as_of的别名)或clock(可调用对象,或暴露now()方法的对象),三者只能提供一个,否则抛ValueError。这是刻意设计——省略时钟会被视为错误,而不是隐式读取宿主机的墙钟时间,从而让发布决策完全可重放。该约束由_resolve_evaluation_time()保证(evidence_freshness.py),测试也验证了"未提供或提供多个时钟来源都会报错"(test_evidence_freshness.py)。
from datetime import datetime, timezone from openmed.compliance import EvidenceRecord, evaluate_evidence_freshness as_of = datetime(2026, 8, 12, 12, 0, tzinfo=timezone.utc) evidence = [ EvidenceRecord( evidence_id="release-2026-08-11", evidence_type="release", generated_at=datetime(2026, 8, 11, 12, 0, tzinfo=timezone.utc), policy_version="privacy-v2", ) ] report = evaluate_evidence_freshness(evidence, policy, as_of=as_of) if not report.passed: raise RuntimeError(report.failure_message())时间戳解析规则
_parse_datetime()(evidence_freshness.py)只接受**带时区(aware)**的时间:
- 接受
datetime对象与 ISO 8601 字符串,字符串末尾的Z会被规范化为+00:00; - 无时区(naive)的时间戳一律视为无效;
- 所有有效时间都会被转换到 UTC 统一比较;
- 超出 UTC 可表示范围(如
0001-01-01T00:00:00+01:00)的时间会被判定为invalid_timestamp,测试 test_evidence_freshness.py 对该边界有覆盖。
测试 test_evidence_freshness.py 还演示了可重放性:用as_of评估与用一个返回相同时刻的固定时钟对象评估,得到的报告to_dict()完全一致,且时钟对象恰好只被调用一次。
失败关闭(fail closed)清单
门控对以下所有情况一律拒绝通过(每条记录最多贡献一个主原因,保证计数稳定):
| 场景 | 原因码 |
|---|---|
| 空输入(没有任何证据记录) | missing_evidence |
缺少evidence_id | missing_evidence_id |
evidence_id非法(非不透明令牌) | invalid_evidence_id |
重复的evidence_id | duplicate_evidence_id |
缺少evidence_type | missing_evidence_type |
evidence_type非法 | invalid_evidence_type |
evidence_type未在策略中登记(含无通配符兜底) | unknown_evidence_type |
| 缺少时间戳 | missing_timestamp |
| 时间戳非法(naive、畸形、越界) | invalid_timestamp |
| 时间戳晚于评估时刻(未来时间) | future_timestamp |
| 时间戳超出类型上限(已过期) | expired_evidence |
缺少policy_version | missing_policy_version |
policy_version非法 | invalid_policy_version |
policy_version与策略版本不完全一致 | policy_mismatch |
| 超会话引用非法 | invalid_supersession_link |
| 记录已被更早/更新的记录取代 | superseded_evidence |
全部原因码以常量形式定义在 evidence_freshness.py,并集中收在_REASON_CODES集合中用于报告校验。
边界规则:年龄恰好等于配置上限的证据仍然视为当前有效(as_of - generated_at > max_age才判定过期),测试 test_evidence_freshness.py 验证了 30 天整的证据通过、而 31 天的证据被判定为expired_evidence。
记录构造与映射别名
EvidenceRecord除了直接构造,还接受一系列常见序列化别名:时间戳可以是timestamp/observed_at,类型可以是type/kind,ID 可以是id/record_id,策略可以是policy(evidence_freshness.py)。同时刻/同类型/同引用的别名参数会触发TypeError,避免歧义。from_mapping()只读取这些安全的描述字段,忽略所有其他映射字段——测试 test_evidence_freshness.py 表明即使记录中混入raw_sensitive_fixture这样的合成敏感值,也不会出现在任何序列化输出中。PrivacyEvidence是该类的语义化别名。
Supersession:新旧证据的取代语义
superseded_by表示某条证据记录不再具备资格;而一条替换记录可以用supersedes指向前一个不透明引用:
- 若被引用的更早记录在本次评估的同一批输入中,它会被计为
superseded_evidence; - 若更早记录只存在于外部归档中(不在本次输入里),替换记录依然保持合格。
replacement = EvidenceRecord( evidence_id="release-2026-08-12", evidence_type="release", generated_at=as_of, policy_version="privacy-v2", supersedes="release-2026-08-11", )实现上,evaluate_evidence_freshness()会先扫描全部记录建立evidence_id -> 索引列表的映射,再收集"被supersedes指向且出现在输入中"以及"被superseded_by标记"的记录 ID 集合,最后在逐条判定主原因时命中superseded_ids即返回SUPERSEDED_EVIDENCE(evidence_freshness.py)。
测试 test_evidence_freshness.py 完整验证了三态行为:旧记录与替换记录同批输入时旧记录被拒、仅提交替换记录时整体通过、显式superseded_by也能单独触发拒绝。另外:
- 引用之间仅作为不透明令牌比较,不会做语义解析或网络查询;
- 超会话引用本身也要通过
_SAFE_TOKEN_RE校验,非法令牌(如含空格)会触发invalid_supersession_link(测试见 test_evidence_freshness.py); - 所有引用值永远不会出现在新鲜度报告中。
隐私安全的诊断输出
EvidenceFreshnessReport的to_dict()与to_json()只包含:
policy_version(策略版本)total_count(输入总数)current_count/accepted_count(通过/当前有效数)rejected_count(拒绝数)reason_counts(确定性原因计数,按原因码排序)passed(便捷布尔属性,等价于fresh)
它们不包含证据引用(evidence_id)、时间戳、载荷字段、源文本、标识符或任何超会话值。测试 test_evidence_freshness.py 明确断言"e-current"与"synthetic-payload"都不会出现在to_json()输出中。failure_message()也只会生成形如privacy evidence freshness gate failed: rejected=5, reasons=expired_evidence=1, ...的计数级消息(evidence_freshness.py)。
from openmed.compliance import assert_evidence_freshness, EvidenceFreshnessError try: assert_evidence_freshness(evidence, policy, as_of=as_of) except EvidenceFreshnessError as exc: print(exc.report.to_json()) # 只有聚合计数,绝无记录级字段assert_evidence_freshness()与check_evidence_freshness()(后者的单一实现别名)在门控未通过时抛出携带同一份报告对象的EvidenceFreshnessError,异常消息同样只含计数(evidence_freshness.py)。
结论:把新鲜度门控接入发布流水线
将上述机制组合起来,一个完整的发布检查片段如下:
from datetime import datetime, timezone from openmed.compliance import EvidenceFreshnessPolicy, EvidenceRecord, evaluate_evidence_freshness policy = EvidenceFreshnessPolicy( policy_version="privacy-v2", age_limits={"release": timedelta(days=30), "*": timedelta(days=7)}, ) as_of = datetime(2026, 8, 12, 12, 0, tzinfo=timezone.utc) evidence = [EvidenceRecord( evidence_id="release-2026-08-12", evidence_type="release", generated_at=as_of, policy_version="privacy-v2", )] report = evaluate_evidence_freshness(evidence, policy, as_of=as_of) # report.passed 为 True 且 to_json() 只含聚合计数,可安全写入 CI 日志 print(report.to_json())关键要点回顾:
- 类型安全:年龄上限必须是
timedelta,版本号精确比较,杜绝单位漂移与版本漂移; - 可重放:评估时间必须由
as_of/now/clock注入,评估器既不读墙钟也不联网,同一输入必然得到同一结论; - 失败关闭:空输入、缺失/非法字段、未知类型、naive/未来/过期时间戳、版本不匹配、非法超会话引用全部拒绝,年龄恰好等于上限则通过;
- 取代语义:
supersedes/superseded_by以不透明令牌完成新旧证据淘汰,外部归档中的旧记录不影响替换记录资格; - 隐私输出:报告与异常只含策略版本和聚合计数,永不外泄证据引用、时间戳、载荷或超会话值。
该门控已在 tests/unit/compliance/test_evidence_freshness.py 中具备完整的单元测试覆盖(可重放性、失败关闭矩阵、类型约束、边界年龄、超会话三态、映射别名与隐私输出)。需要进一步了解它与 OpenMed 其他合规控制(如访问审查过期门控、报告基数预算、防篡改审计链)如何配合使用时,可继续阅读 docs/compliance/access-review-expiry.md、[docs/compliance/report-cardinality.md] 与 [docs/compliance/audit-envelopes.md]。
【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考