OpenMed 仅计数 Trace 隐私审计制品(Counts-Only Trace Privacy Audit Artifact)实战指南
【免费下载链接】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.guard.audit是 OpenMed 本地优先(local-first)隐私体系中的一块"证据摘要"组件:它把一次对显式选定 trace 存储的扫描,压缩成一份不含任何源值、路径、提示词或工具输出的确定性审计制品。本文围绕该制品的字段契约、确定性用法、底层实现与操作边界展开,结合源码与测试说明如何在不制造"第二份 trace 存储"的前提下记录可复核的隐私扫描证据。
设计动机:证据摘要,而不是 trace 副本
在 OpenMed 的隐私架构中,trace(会话轨迹)可能包含临床文本、提示词、工具输出等敏感内容。常规思路是"扫描后把结果留档",但留档本身会带来二次暴露风险:原始 trace 与扫描摘要共存时,摘要就成了新的泄露面。
openmed.guard.audit的定位因此非常明确——它产出的是一份evidence summary(证据摘要):
- 不是 trace 存储的副本;
- 不是合规认证(compliance certification);
- 只回答一个问题:"某个被明确选定的 trace 存储,确实被某个扫描器在某个策略下扫过,结果如何归类。"
这一设计在模块文档字符串中写得很直白(见 openmed/guard/audit.py):
"The artifact contract intentionally has no field for source text, prompts, tool results, replacement maps, paths, or arbitrary scanner metadata… This keeps JSON and Markdown exports useful as evidence without making them a second trace store."
固定的字段契约(Recorded Fields)
制品 schema 刻意保持极简,只有五个业务字段,外加一个固定 schema 版本标识:
| 字段 | 含义 | 约束 |
|---|---|---|
scanner_version | 扫描器标识 | 有界 token,匹配^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,127}$ |
policy_hash | 策略的规范 SHA-256 引用 | 必须形如sha256:+ 64 位小写十六进制 |
file_fingerprints | 内容指纹集合 | 排序后的sha256:<64 hex>,绝不包含路径或文件名 |
category_counts | 类别计数映射 | 排序后的"类别 → 非负整数"对 |
disposition | 操作结果 | 有界 token,如clean、redacted、quarantined、reviewed、blocked |
此外,to_dict()输出的 JSON 中还会带上两个判别字段:
schema_version:当前为1(源码中SCHEMA_VERSION = 1);artifact:固定值trace_privacy_audit(源码中ARTIFACT_NAME),用于从序列化结果反序列化时校验制品类型。
关键点:契约中刻意没有通用元数据或负载字段。源值(source values)、替换映射(replacement mappings)、提示词(prompts)、工具输出(tool outputs)、trace 主体、finding 详情,在制品契约中"无处安放"——这正是隐私边界所在:不是靠过滤,而是靠"根本没有存储位置"来保证不泄露。
从扫描器摘要(scanner summary)转换到制品时,摘要里出现的任何未知字段都会被直接忽略(from_scan_summary只提取白名单字段)。测试 tests/unit/guard/test_audit.py 中的test_scan_summary_allowlist_drops_source_values_mappings_prompts_and_tools验证了这一点:即使摘要里塞入source_values、replacement_mappings、prompt、tool_outputs,渲染出的 JSON 与 Markdown 中也找不到这些键和任何源值。
本地优先的确定性用法
该辅助模块不发起任何网络调用:
- 策略内容只在计算摘要(digest)时被消费,之后即丢弃;
fingerprint_file只读取显式提供的本地文件;- 两个辅助函数都不存储输入内容或输入路径。
文档给出的标准用法如下,这段代码可直接复制运行:
from openmed.guard.audit import TraceAuditArtifact, hash_policy artifact = TraceAuditArtifact.from_files( scanner_version="trace-scanner/1.0", policy_hash=hash_policy("synthetic policy configuration"), files=["trace-store.jsonl"], category_counts={"NAME": 2, "PHONE": 1}, disposition="redacted", ) artifact.write_json("evidence/trace-audit.json") artifact.write_markdown("evidence/trace-audit.md")源码中from_files的完整签名(见 openmed/guard/audit.py):
@classmethod def from_files( cls, scanner_version: str, policy_hash: str, files: Iterable[str | Path] | str | Path, category_counts: Mapping[str, int], disposition: str = "unknown", ) -> "TraceAuditArtifact":注意disposition的默认值是"unknown",即扫描器若不显式声明结果,制品会以unknown落盘,而不是凭空捏造一个结果。category_counts也可以用辅助函数count_categories从类别标签列表直接生成(自动校验标签安全性并排序):
from openmed.guard.audit import count_categories counts = count_categories(["NAME", "NAME", "PHONE"]) # {"NAME": 2, "PHONE": 1}确定性输出:字节级可复现
to_json()使用sort_keys=True对键排序,to_markdown()对指纹和类别排序。更重要的是,两种渲染都不包含时间戳、主机信息、路径、源值、替换映射、提示词或工具输出,因此对同一份摘要重复运行,得到的是字节级稳定(byte-stable)的输出。
to_json()的序列化约束(源码 openmed/guard/audit.py):
return json.dumps( self.to_dict(), allow_nan=False, # 拒绝 NaN/Infinity,防止非规范 JSON ensure_ascii=True, # 统一 ASCII 转义,跨平台稳定 indent=indent, # 默认 2 空格缩进 sort_keys=True, # 键排序 )to_markdown()输出的结构是确定的(源码 openmed/guard/audit.py):头部信息表(Scanner version / Policy hash / Disposition / Files fingerprinted / Findings counted)、## File fingerprints表格、## Category counts表格。Markdown 单元格内容会转义|和反引号,避免破坏表格结构。
对应的测试test_artifact_json_and_markdown_are_deterministic_and_counts_only验证:即使以不同顺序传入指纹与类别计数,两次渲染结果完全一致,且 JSON 内容精确等于白名单字段。
源码级实现细节:安全如何落地
openmed.guard.audit的"仅计数、不含敏感内容"不是靠约定,而是靠一层层强制校验实现的。
哈希引用必须规范
hash_policy(policy):接受字符串或 bytes,只计算 SHA-256 摘要并返回sha256:<64 hex>,内容不落盘;_safe_hash强制要求sha256:[0-9a-f]{64}格式,非法引用直接抛TraceAuditError。测试test_hash_references_must_be_canonical_sha256_values确认sha256:not-a-digest会被拒绝。
文件指纹:拒绝符号链接与"边读边改"
fingerprint_file的防护链路(源码 openmed/guard/audit.py):
candidate.stat(follow_symlinks=False)后检查S_ISREG——符号链接和非普通文件直接拒绝;- 以
O_NOFOLLOW、O_CLOEXEC(以及平台相关的O_BINARY)打开,从打开方式上再次杜绝跟随符号链接; - 打开后
os.fstat与先前的 stat 做samestat比对,防止 TOCTOU(检查到使用之间的替换); - 分块(1 MiB)读取计算 SHA-256;
- 读取结束后再次
fstat,比对 inode、st_size、st_mtime_ns、st_ctime_ns是否全部未变——文件在读取期间被修改则放弃; - 所有失败统一抛"无值"异常
unable to fingerprint trace file,不携带任何路径或文件内容,避免敏感路径进入日志或错误报告。
测试test_invalid_inputs_fail_without_echoing_values验证:对缺失文件调用fingerprint_file时,异常信息中不包含文件名;test_fingerprint_file_rejects_symlinks_without_reading_target(POSIX 平台)验证符号链接被拒绝且目标内容不出现在异常中;test_fingerprint_preserves_raw_newlines_and_end_of_file_bytes验证指纹严格基于原始字节(含\r\n与 EOF 字节),不受文本规范化影响。
校验后的计数映射不可变
TraceAuditArtifact是frozen=True的 dataclass,category_counts在__post_init__中经MappingProxyType包装为只读映射。测试test_category_counts_are_immutable_after_validation确认试图写入artifact.category_counts["RAW_TEXT"] = 1会抛TypeError。负计数同样被拒绝(category counts must be non-negative integers)。
原子写入与权限控制
JSON 与 Markdown 写入统一走_write_text_atomic(源码 openmed/guard/audit.py):
- 目标父目录自动创建;
- 使用
tempfile.mkstemp(prefix=".openmed-trace-audit-", dir=parent)创建同目录私有临时文件; - 写入后
flush+fsync,确保落盘; - POSIX 下
fchmod(0o600)(无fchmod时回退chmod),产出0600 权限的制品; - 写入前后两次检查目标不是符号链接,最后
os.replace原子替换; - 失败时保留既有制品并清理临时文件。
测试test_artifact_write_is_private_and_rejects_symlink_targets验证写入后权限为0600,且目标为符号链接时拒绝写入;test_atomic_write_failure_preserves_existing_artifact_and_cleans_temp验证替换失败时旧文件原样保留、临时文件被清理。
从扫描器摘要构建:白名单提取
除了直接对本地文件做指纹,TraceAuditArtifact.from_scan_summary(summary)支持把扫描器返回的摘要映射转成制品(源码 openmed/guard/audit.py)。它只读取五个白名单键,其余一切字段被静默丢弃。模块级便捷函数build_trace_audit同时接受预计算的指纹(可传字符串、bytes、Path或含fingerprint/file_fingerprint/hash键的映射)和/或本地文件路径,二者合并后统一排序去重校验。
反序列化侧同样严格:from_dict要求载荷携带artifact == "trace_privacy_audit"判别字段(测试test_from_dict_requires_the_serialized_schema_discriminators),未知字段一律忽略(测试test_json_round_trip_keeps_only_allowlisted_fields:向 JSON 中注入raw_trace、tool_output后重建,制品的输出依旧干净)。read_json/from_json支持从本地文件或字节串恢复制品,失败时同样只返回无值错误。
操作边界(Operational Boundaries)
这是文档强调的最后一层纪律,也是使用该组件时必须遵守的边界:
- 只向制品传递哈希与聚合计数。原始 trace 与任何可逆映射(replacement mapping)必须留在各自独立受控的本地存储中;
- 制品证明的是"扫描器报告了什么",它不证明扫描器检测到了每一个敏感值,也不证明某个 disposition 满足法律或监管要求;
- 因此它适合作为"扫描已执行"的取证证据,配合其他组件形成完整证据链(见下文)。
与周边组件的协同:完整的本地证据链
仅计数审计制品不是孤立的,它在 OpenMed 的本地隐私证据体系中与多个组件衔接:
- 证据包完整性:docs/security/evidence-integrity.md 中的
openmed.risk.check_evidence_bundle可对包含manifest.json的本地证据包做确定性校验,同样只计算 SHA-256、只产出计数与稳定失败类别; - 审计制品留存:docs/security/audit-retention.md 中的
openmed.risk.audit_retention为仅计数审计制品提供本地、确定性的留存规划(按 disposition 配置delete/retain规则),并通过指纹校验检测删除后的遗漏与篡改; - 会话末尾 scrubbing:docs/guides/session-end-hook.md 中的
python -m openmed.guard.session_hook --json <trace>可在会话结束时对已完成 trace 做确定性清洗,输出的机器可读摘要同样只含格式、清洗计数、字节数与前后 SHA-256 摘要,不含源值与路径——与本组件共享"仅计数、不携带敏感内容"的设计哲学。
三者共同构成一条完整的本地闭环:扫描(本文组件记录证据)→ 留存规划(audit-retention)→ 完整性校验(evidence bundle / deletion verification),全程无网络调用、无原始内容落盘。
结论
openmed.guard.audit用不到 600 行代码(openmed/guard/audit.py)实现了"证据摘要而非副本"的隐私原则:固定五字段契约 + 规范哈希 + 排序确定性输出 + 原子安全写入 + 无值错误。在 OpenMed 本地优先、患者数据不出网络的整体定位下,它为"trace 存储确实被扫描过"提供了可归档、可复核、可跨机器比对(字节级稳定)的取证证据,同时从契约层面杜绝了二次泄露的可能。使用它时请始终牢记:制品只证明扫描器报告了什么,不能替代扫描器本身的质量,更不能替代合规判断。
【免费下载链接】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),仅供参考