Friend 项目 Parity Pack v0 解析:基于本地脱敏 Cassette 重放的 STT/LLM 契约测试体系
2026/9/15 13:27:43 网站建设 项目流程

Friend 项目 Parity Pack v0 解析:基于本地脱敏 Cassette 重放的 STT/LLM 契约测试体系

【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend

本篇文章以仓库中 backend/testing/parity_pack_v0/README.md 为主干,结合 backend/testing/parity_pack_v0/ 下的实现源码、backend/routers/listen/parity_capture.py 实时接线以及 backend/tests/unit/test_parity_pack_v0.py 等测试用例,系统讲解 Parity Pack v0 的完整机制:它如何在仅限开发(dev)且默认拒绝的强门禁下采集匿名的"线缆观测"(cassette)、如何通过 SHA-256 脱敏指纹保证可重放性,以及如何用严格调用拓扑的播放器在无外网、无真实供应商的条件下完成 STT/LLM 回环重放。读完本文,你将掌握这套"采集→脱敏→重放→金标漂移"流水线的每个配置项、目录契约与操作流程,并能在本地复现npm run test:parity-pack-v0的整套检查。


1. 定位:本地重放包契约,而非生产采集路径

Parity Pack v0 在 README 开头就给出了明确的边界定义:This is a local replay-pack contract, not a production capture path。它是一套本地重放包契约,用来验证"同一批请求在录制时的行为与当前代码行为保持一致"(parity,即对等一致性),而不是一条可以随时开关的生产录制通道。

由此派生出的两条硬性约束贯穿全文:

  • 载荷受限:Pack 载荷(pack payloads)是受限的本地/开发产物,必须禁止被提交到 Git("must never be committed");
  • 默认拒绝:所有采集入口默认 deny,只有显式满足三项环境变量条件才会放行。

实现上,整个模块由 backend/testing/parity_pack_v0/ 下的若干小文件组成,各司其职:

文件职责
whitelist.pyCaptureWhitelist:从环境变量解析并执行默认拒绝门禁
schema.pyCassetteIdentityRequestFingerprint:匿名身份与脱敏指纹
redaction.py保守脱敏:凭据键、邮箱、电话、URL 参数
capture.pyCaptureTap/CaptureInvocation:采集与落盘
players.pySTTCassettePlayer/LLMCassettePlayer回环播放器
manifest.pymanifest.json的构建与写盘
gold.py金标双跑、漂移报告
runner.py无外网运行原语与 Fake 命中记账
matrix.py六个合成 overlay 的名称矩阵
rewrite.py未来重写二进制的描述符槽位

2. 开发采集门禁:三项环境变量与默认拒绝

采集的唯一入口是CaptureWhitelist。README 明确:CaptureTap使用CaptureWhitelist.from_environ(),在序列化任何 cassette 字节之前调用allows(principal_id)默认 deny。允许采集必须同时满足三个设置:

OMI_ENV_STAGE=dev OMI_PARITY_PACK_CAPTURE=1 OMI_PARITY_PACK_ALLOWED_PRINCIPALS='synthetic-user-1,synthetic-device-2'

对照 whitelist.py 的源码,门禁判定逻辑非常直接:

def allows(self, principal_id: str | None) -> bool: """Default deny: only explicit dev enablement plus an exact allow-list hit.""" return bool(self.enabled and self.environment == "dev" and principal_id and principal_id in self.principal_ids)

即允许条件为:OMI_PARITY_PACK_CAPTURE取值为1/true/yes(大小写不敏感)OMI_ENV_STAGE == "dev"principal 非空严格命中OMI_PARITY_PACK_ALLOWED_PRINCIPALS逗号分隔列表。任何一个条件不满足,start()都会拒绝并只保留有界的 lane/reason 元数据。

backend/tests/unit/test_parity_pack_v0.py 用测试锁死了这一行为:prod阶段即使 capture 置 1 也拒绝;空环境默认拒绝;特别地,OMI_ENV不是规范运行时阶段变量——只有OMI_ENV_STAGE能开启 dev 采集(test_capture_whitelist_ignores_non_canonical_env_var)。

采集命名遵守匿名规则:在CassetteIdentity中使用匿名会话/事件标识符;请求指纹是规范化脱敏请求结构的 SHA-256 摘要;auth、cookies、keys、签名 URL 参数以及尽力而为的邮箱/电话字符串都会在生成摘要前被移除或打码。严禁把原始请求数据写进 manifest、报告或 Git。

3. 匿名身份与脱敏指纹:契约的地基

3.1 CassetteIdentity:六元组身份

schema.py 中的CassetteIdentity是"单次供应商调用"的身份元组,六个字段:

字段说明
anon_session匿名会话标识(必须已是匿名、稳定的标识符)
provider_lane供应商通道,如sttllmmemory
route_or_model路由或模型名
call_ordinal调用序号(≥ 0)
retry_attempt重试次数(≥ 0)
parent_event_anon父事件的匿名标识

构造器在__post_init__中强制:四个文本字段必须为非空字符串,call_ordinal/retry_attempt必须非负。key()方法对身份的规范 JSON 做 SHA-256,生成 64 位、文件系统安全的稳定键名——不包含任何请求内容。测试 test_identity_tuple_is_stable_and_complete 验证了as_dict()的字段完整性与key()的长度。

3.2 RequestFingerprint:canonical + redacted 的一次性摘要

RequestFingerprint.from_request()的流程是:先用redact_value(value, drop_sensitive=True)递归移除敏感字段(而不是打码),再对规范化 JSON(sort_keys=True, separators=(",",":"))做 SHA-256,算法标识为sha256-canonical-redacted-v1。摘要本身只用于比对,绝不写入 manifest 或报告。

redaction.py 的脱敏是"保守型"的,包含三层规则:

  • 敏感键:正则SENSITIVE_KEY匹配authorizationauthcookietokensecretapi_keypasswordsignaturesignedprivate_keycredentialbearerjwt等(带词边界)。_is_sensitive_key()先把 camelCase 归一化为 snake_case,所以accessTokenclientSecretaccess_tokenaccess-token一样能被捕获;
  • URL:剥离 query/fragment,仅保留 scheme/netloc/path,并追加[REDACTED_URL_PARAMS]标记;
  • 邮箱/电话[REDACTED_EMAIL][REDACTED_PHONE]打码。

redact_valuedrop_sensitive参数决定敏感值的处理方式:为True(指纹场景)时直接省略,为False(报告场景)时替换为稳定的[REDACTED]标记。测试 test_fingerprint_is_canonical_and_never_contains_sensitive_values 证明:键序不同的两个请求会得到相同指纹,而指纹/脱敏结果中不会残留topsecret或电话号码。

3.3 特例:audio_b64 不做过度脱敏

capture.py 的CaptureInvocation.observe()有一个容易被忽略的关键细节:base64 音频是不透明二进制而非自由文本,对其跑文本模式脱敏可能把一段 base64 数字误判成电话号码、从而破坏可重放音频事件。因此实现中对payload.get("audio_b64")原样保留,仅对其余字段走redact_value

4. CaptureTap:三类方向的时序观测

CaptureTap.start()在通过白名单后创建一次调用(invocation)CaptureInvocation.observe()的观测边界记录三类 wire 事件:

  • client:客户端方向的观测(如解码后的客户端音频);
  • outbound:出站到供应商的观测(如发送给 STT 的 socket 数据);
  • inbound:供应商回调方向的观测(如 STT 返回的 transcript)。

每个CassetteEventdirectiondt_mspayload组成;dt_ms相对毫秒(相对 invocation 起点的时间差,由可注入的time.monotonic时钟计算,取整到毫秒),且必须非负(构造器强制校验)。persist()schema_version: 1、identity、fingerprint 与事件数组写成紧凑 JSON(sort_keys=True, separators=(",",":"))到cassettes/<identity-key>.json,并可选追加surface/source顶层判别字段(见第 6 节)。

白名单未命中时,CaptureTap.start()不写任何 cassette 字节,只把{"provider_lane": ..., "reason": "whitelist_miss"}追加进有界元数据列表denied_metadata,并裁剪到最近 100 条(del self.denied_metadata[:-100])。

5. 实时路径接线:/v4/listen 与 ListenParityCapture

README 指出,/v4/listen运行时只在 Firebase WebSocket 认证完成、STT 供应商选定之后才会创建routers.listen.parity_capture.ListenParityCapture。它:

  1. 用 Firebase UIDCaptureWhitelist的精确比对;
  2. 在 cassette 创建前派生匿名会话/事件标识符(_anonymous_id对 UID+session 做 SHA-256 并截取前 32 位十六进制,见 parity_capture.py);
  3. 记录解码后的客户端音频、成功的 STT socket 发送、以及供应商 transcript 回调;
  4. 在正常 listen 会话 teardown 期间持久化。

parity_capture.py 的from_environ()把门禁决策细化为可观测的分类(_allowlist_decision_reason):allowedcapture_disabledstage_not_devprincipal_missingallowlist_miss,并同步写入遥测。整条链路设计为采集绝不拖垮 listen 会话:初始化、观察、持久化任何一步异常都只记 warning 日志并降级为无操作(fail-closed,但服务本身继续)。

5.1 第四个变量:OMI_PARITY_PACK_ROOT

README 强调第四个、必需且仅操作者可设的变量,其值必须是仓库之外的绝对路径

OMI_PARITY_PACK_ROOT=/absolute/restricted/local/parity-pack

没有默认 root。缺失、相对路径或位于仓库内的 root 一律禁用。源码_capture_root()落实了这一规则:先要求非空、expanduser后必须is_absolute(),再用root.resolve().relative_to(repository_root)探测——若解析后的 root 落在仓库目录树内则返回None(禁用)。因此以下情况全部判定为禁用:

  • 未设置OMI_PARITY_PACK_ROOT
  • root 是相对路径;
  • root 位于仓库内;
  • OMI_ENV_STAGE不是dev
  • 缺少OMI_PARITY_PACK_CAPTURE=1
  • 白名单未命中。

任何 Helm 或生产默认配置都不会开启这条路径。由于本地 cassette 可能包含受限的音频/transcript 事件载荷,必须把 root 放在 Git 之外,且永远不要把它附到 PR 上。

5.2 有界性约束:采集不是无限录音

parity_capture.py 与 live_capture.py 共享一组采集上限常量:

常量含义
MAX_CAPTURE_EVENTS1 000单次 invocation 最多事件数
MAX_CAPTURE_AUDIO_BYTES8 MiB音频总字节上限
MAX_CAPTURE_TEXT_CHARS16 384单段文本截断长度
MAX_CAPTURE_SEQUENCE_ITEMS1 000序列元素截断数量

_can_observe()分配 base64 表示之前就检查原始输入(len(audio)),超限即置_limit_reached并丢弃后续事件,防止把 listen 会话的内存打爆。

6. 更多采集面:SurfaceParityCapture 与 surface 判别表

SurfaceParityCapture复用同一套门禁/导出器,覆盖额外的记忆形成(memory-forming)表面。它在 cassette 文档上追加可选的顶层判别字段surface/source,但保持 v1 的 identity、fingerprint、event 契约不变,因此存量播放器无需改动。README 的映射表如下:

surfacesource采集接缝
pttdesktop_ptt_httpdesktop_ptt_streamDesktop PCM PTT 与实时 PTT STT(有界音频 + transcript 事件)
screendesktop_screen_activity_sync纯文本屏幕活动/上下文同步;无视频或 embedding 向量
conversation_finalizationconversation_<source>transcript 输入、记忆抽取结果与已接受记忆
memory_writev3_memory_createv3_memory_batch_createintegration_<app>twitter_<persona>手动/API、集成与社交记忆写入者
memory_importv3_memory_import_batch有界导入产物与摄取结果(非原始媒体)

surface_parity_capture.py 提供了这些表面的通用实现:from_environ()内部同样走_capture_rootCaptureTap.start(),任何初始化异常都降级为禁用实例。对记忆载荷还定义了窄化函数_memory_payload(),只保留idcontent(截断到 16K)、categoryvisibilitysource_type五个字段——绝不把完整证据链塞进 cassette。capture_memory_write()则是一次写入一个"memory-write" cassettes 的便捷入口(request仅含memory_countsource,不暴露 UID)。对应的单元测试在 test_surface_parity_capture.py。

7. 开发部署:emptyDir 挂载与私有 GCS 导出

README 描述了开发 listen 部署的落地方式:挂载/var/omi-parity-pack作为emptyDir,仅供显式白名单的 dogfood 主体验证者使用。Pod 的fsGroup: 10001与非 root 后端镜像组一致,使 listener 能创建并持久化cassettes/目录,随后尽力而为地把 cassette JSON 导出到私有开发桶:

gs://based-hardware-dev-omi-parity-pack-v0/parity-pack/v0/cassettes/<identity-key>.json

导出是fail-open的:即使 GCS 宕机,listen 会话也照常继续(persist()内对导出异常只记 warning)。README 给出的离线重放下载命令:

gcloud storage cp -r \ "gs://based-hardware-dev-omi-parity-pack-v0/parity-pack/v0" \ ./omi-parity-pack-dogfood/ # Point OMI_PARITY_PACK_ROOT at the local tree (or compose a pack with # manifest.json as required by this README), then: npm run test:parity-pack-v0

两条红线:永远不要把 cassettes 提升到生产存储,也不要提交进仓库;emptyDir 是临时存储,pod 重启后、成功导出之前的数据会丢失。

7.1 可观测性:零初始化计数与日志标记

开发 listen 采集暴露零初始化的 Prometheus 计数器omi_parity_pack_capture_events_total{stage,outcome,reason_class}及配套的parity_pack_capture_event日志标记。其封闭标签(closed labels)用于区分:接受的 listen、白名单决策、采集初始化、cassette 持久化、GCS 导出尝试/成功/失败。这些事件从不包含主体验证者或会话标识、载荷、凭据或 cassette 对象路径;非 dev 运行时既不递增该计数器也不写该日志。相关实现在 parity_telemetry.py。

8. 重放播放器:严格调用拓扑的回环适配器

采集之后是重放。STTCassettePlayerLLMCassettePlayer是 wire-oracle fakes 的回调式回环适配器(loopback adapters)。两者共享有序的InvocationTopology

  • play()先验证完整身份规范化脱敏请求指纹,再依次产出录制的PlayedEvent(direction / dt_ms / payload)并交给emit回调;
  • assert_complete()未使用的 cassette判定失败(unused cassettes: N);
  • 错位、乱序或多余调用立即失败CassetteTopologyError)。

players.py 的具体行为:

情形错误信息含义
已消费完但还有新调用extra invocation: <key>多出的调用
身份与当前位置 cassette 不一致out-of-order cassette: expected=... actual=...乱序
请求指纹不一致cassette request fingerprint mismatch请求不匹配
player 通道不匹配<lane> player cannot play <lane>通道错配
消费后仍有剩余unused cassettes: N未用完的 cassette

STTCassettePlayer.lane = "stt"LLMCassettePlayer.lane = "llm",互不串位。_read()还会校验schema_version == 1,不支持的版本直接抛ValueError。cassettes 始终是受限的本地/dev 输入,禁止提交。

9. 包目录布局与 manifest.json 契约

README 规定的受限本地包目录结构:

<restricted-local-pack>/ manifest.json # hashes + case descriptors only inputs/<case>.json # referenced by inputs_ref cassettes/<identity-key>.json # referenced by cassette_refs

manifest.json记录:schema 版本、pack_id、产物哈希,以及每个 case的:输入/cassette 引用、预期结果(expected outcomes)、不变量 ID、匿名 cassette 身份、脱敏请求指纹(仅摘要)。由 manifest.py 的build_manifest()生成(schema_version: 1pack_idcasesartifact_hashes四段),sha256_file()按 1 MiB 分块计算产物哈希。测试 test_manifest_has_references_hashes_outcomes_and_invariants 验证了 case 中invariant_idsartifact_hashes的完整性。

基础检查命令是:

npm run test:parity-pack-v0

该脚本在仓库根 package.json 中映射到 backend/testing/parity_pack_v0/run.sh,后者在仓库根执行PYTHONPATH=backend的 pytest,覆盖 test_parity_pack_v0.py 与 test_parity_pack_v0_stage3.py 两组测试。

10. 金标、漂移与重写槽

10.1 double_run_gold:先双跑,再冻结

gold.py 的double_run_gold()在更新金标(gold)前把每个 case跑两遍

  • 两次结果摘要不同 → 直接抛AssertionErrornon-deterministic replay for <case_id>),拒绝非确定性结果
  • 只有write_gold=True才允许改写expected_outcomes(把第一次结果写回 manifest);普通重放只产出一份仅摘要、仅 warn的漂移报告drift_report()status: warn/okdrift_count、逐条Drift(case_id, expected_digest, actual_digest)enforcement: warn-only)。

设计意图很明确:漂移永不掩盖结果、永不阻塞开发者排查,它只是一个提示信号。金标变更必须显式走write_gold=True,防止普通重放以副作用方式悄悄改掉本地包的预期结果。

10.2 rewrite_launch_descriptor:重写二进制的显式集成槽

rewrite_launch_descriptor()是为未来重写二进制预留的显式集成槽位。描述符(RewriteLaunchDescriptor)包含commandinput_manifestoutput_manifestavailable,其中available恒为False。README 特别强调:该描述符在本仓库中刻意不可用——操作者必须自行安装并调用经批准的二进制(默认名omi-replay-rewrite),重放流程绝不会下载或执行任意二进制。这是对供应链安全的刻意约束:只有经人工批准、显式提供的二进制才能改写 cassette 数据。

11. 合成 v0 矩阵:六个 overlay

README 给出合成 v0 矩阵的六个 overlay 名称:baselineduplicate_deliveryprovider_timeoutprovider_errorout_of_order_eventsredacted_capture。它们不包含任何真实采集载荷。对应 matrix.py 的SYNTHETIC_MATRIX,每个 overlay 用delivery/provider/expected三元组描述行为预期:

overlaydeliveryproviderexpected
baselinesinglerecordedfinalized(正常终结)
duplicate_deliveryduplicaterecordedidempotent(幂等)
provider_timeoutsingletimeoutrecoverable(可恢复)
provider_errorsingleerrorfailed-safe(安全失败)
out_of_order_eventsreorderedrecordedrejected(拒绝)
redacted_capturesinglerecordedno-sensitive-payload(无敏感载荷)

这组矩阵实际上把重放引擎要验证的六类不变量固化成了命名契约:正常路径、重复投递的幂等性、供应商超时的可恢复性、供应商错误的失败安全、乱序事件的拒绝,以及脱敏捕获确保无敏感载荷。

12. 操作者工作流(仅限 dev)

README 在最后给出三步操作者工作流,也是整套体系的收束:

  1. 本地隔离:把本地包放在仓库之外;绝不提交 cassettes、inputs、载荷或白名单。
  2. 显式门禁:设置OMI_ENV_STAGE=devOMI_PARITY_PACK_CAPTURE=1,以及显式的OMI_PARITY_PACK_ALLOWED_PRINCIPALS白名单。任何其他阶段或缺失白名单都是默认拒绝,且不持久化任何 cassette 字节
  3. Hermetic 重放:用已选择加入的合成/dev principal 运行应用路径,然后执行npm run test:parity-pack-v0进行隔离重放。测试拒绝外网 egress 并要求 Fake 命中记账,不使用任何真实供应商或生产服务。

12.1 hermetic 保障如何落地

runner.py 是第 3 步的技术保障:deny_network()委托给仓库验证过的block_outbound_network(见 backend/testing/hermetic_network.py),连底层socket.connectconnect_ex和 DNS 解析都一并封死;任何被拦截的 egress 都会以UnexpectedEgress断言失败。hermetic_run()在此之上叠加FakeHitRegistry:每个 Fake 被调用一次记一次hit(),最后由require(**expected)精确命中计数比对(多了少了都失败)。清理钩子按逆序执行且彼此隔离——某个清理钩子失败不会阻断其他钩子,body 异常永远优先于清理异常上报。测试 test_parity_pack_v0.py 直接导入了hermetic_runUnexpectedEgress,验证重放确实在完全离线的环境里完成。


小结

Parity Pack v0 是一套把"隐私安全"与"契约测试"焊死在一起的本地重放体系:CaptureWhitelist的默认拒绝门禁、CassetteIdentity/RequestFingerprint的匿名与脱敏、CaptureTap三类方向的事件观测、SurfaceParityCapture的多种记忆表面、STTCassettePlayer/LLMCassettePlayer的严格拓扑重放、double_run_gold的双跑金标与 warn-only 漂移,以及rewrite_launch_descriptor的安全重写槽位,共同构成一条从 dev 采集到 hermetic 重放的完整闭环。所有机制都以"载荷不落 Git、身份不落日志、egress 一律封禁"为底线,任何一环失守都会以测试失败或显式禁用收场。对需要为供应商密集型后端(STT/LLM/记忆写入)建立可回归、可离线验证的契约测试体系的工程团队而言,这套 pack 契约与目录布局是一个可直接借鉴的范本。

【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询