OpenProgram / Provider design / implementation handoff
请求录制与严格离线回放入口
设计审计基线:990bfe36e625579443e1cb01a1b3266c8dbd0e87;生命周期实现复核提交:99252205957a2850e1203a888a0fa0b403b0c3f7。本页定义产品入口、provider runtime 生命周期、registry 包装、文件安全、严格离线和验证合同,并分别记录设计与实现证据。
RecordingProvider、ReplayProvider 和 registry transform;新增显式、线程安全、进程级的 provider runtime 生命周期。导入 openprogram.providers 不注册厂商 provider、不读取 record_replay 配置、不打开录制文件、不安装 transform。第一次实际获取 provider 前完成注册与激活;openprogram recordings 只执行管理操作,不进入 runtime 生命周期。录制默认关闭,脱敏不可关闭;回放不读取凭证、不委托真实 provider、不执行 provider fallback。
01 · 目标、非目标与安全边界
目标
- 从产品 CLI/config 启用完整 provider 请求录制。
- 以同一文件离线复现多次、跨 API 的 provider 调用。
- 文件写入并发安全、默认 owner-only、始终脱敏。
- 提供可脚本化的 list/show/delete/prune。
- 错误定位到文件、行、调用、事件或字段。
- 把 provider 注册、runtime 激活和 recordings 管理分成可独立调用和验证的边界。
非目标
- 不录制 tool、MCP、浏览器或任意 HTTP 流量。
- 不定义、持久化或恢复 workflow、Agent DAG、Session 和工具副作用;回放只替换 LLM provider 请求/事件边界。
- 不把 JSONL 定义为长期稳定的公共交换格式。
- 不做跨模型或跨 prompt 的模糊匹配。
- 不在 Web UI 增加入口;CLI/config 先覆盖 worker。
- 不自动上传、共享或提交录制文件。
- 不重写 provider 插件协议,不改变录制格式,不引入运行中热切换。
安全边界
- “严格离线”只承诺 LLM provider 层没有网络访问。
- 回放中的 tool 仍按自身权限运行;需要全进程断网时继续使用 sandbox/network policy。
- 脱敏只处理凭证;prompt、回复和工具结果可能包含私有业务内容。
show默认只显示元数据,内容必须显式请求。
02 · 基线中的当前实现与缺口
| 能力 | 当前证据 | 状态 | 缺口 |
|---|---|---|---|
| 录制、回放与脱敏 | recording.py、replay.py | 已实现 | 严格 v1 JSONL、并发写入、固定脱敏、离线 mismatch 与 legacy reader 已有测试证据。 |
| Registry transform | api_registry.py::configure_provider_transform() | 已实现 | 覆盖已有和后续注册 provider;同一进程只允许一个 transform。 |
| CLI/config 与文件管理 | openprogram recordings | 已实现 | status/record/replay/off/list/show/delete/prune 与 next-start 配置已落地。 |
| 管理命令恢复 | e8c9f645 | 已实现 | recordings 管理入口不初始化 provider runtime;损坏 replay 文件不再依赖环境变量哨兵恢复。 |
| Provider runtime 生命周期 | initialization.py、register.py | 已实现 | 显式 NEW/INITIALIZING/READY/FAILED 状态、并发首次初始化、失败脱敏、auth/API 原子发布与启动期 fail-fast 已验证。 |
| 简版 Markdown | record-replay.md/.zh.md | 已同步 | 记录当前 CLI、严格离线事实与生命周期实现证据。 |
providers/__init__.py 不注册 built-ins、不读取配置、不打开录制文件、不安装 transform。公共 provider 获取和 Web/worker 启动显式调用 initialize_provider_runtime();recordings 管理直接操作配置与文件,不依赖环境变量协议。03 · 现有 reference corpus 的对应设计
本仓当前参考快照只有 references/pi-ai。本节只描述该快照,不声称覆盖其他版本或整个开源生态。
| 对象 | 实际机制 | 录制/回放能力 | OpenProgram 取舍 |
|---|---|---|---|
references/pi-ai/src/stream.ts |
stream()/streamSimple() 按 model.api 解析一个 provider,并直接调用 provider 方法。 |
快照内未提供。快照没有 recording/replay 模块、配置入口或录制文件管理;stream.ts 也没有 wrapper hook。 |
采用单一 registry 分派点;修改为由 registry 统一变换 provider,而不是改每个 vendor 模块。 |
references/pi-ai/src/types.ts |
共享 Model、Context、stream options 和事件类型。 |
快照内未提供。没有 recording manifest 或 replay mismatch 类型。 | 继续序列化现有 Pydantic 对应类型;不创建第二套请求/事件领域模型。 |
拒绝的做法:逐 vendor 增加 recorder、通过 HTTP 代理捕获原始流量、用测试 mock 替代产品入口。这些做法分别增加重复实现、暴露未脱敏 wire 数据,或不能覆盖真实 worker 配置。
04 · 总体设计
保留
现有 JSONL、严格 parser、共享 sink/replay、registry transform、next-start 配置、管理函数和直接构造 RecordingProvider/ReplayProvider 的库 API。
分离
register_builtins() 只负责 provider/auth adapter 注册;initialize_provider_runtime() 负责一次性配置快照和 transform;管理命令不调用二者。
新增
统一生命周期状态、并发首次初始化、失败缓存、公共 provider 获取前强制初始化,以及 Web/worker 的启动期 fail-fast 调用。
拒绝
不使用环境变量控制 import;不为每个 vendor 增加初始化分支;不热重载配置;不改变录制格式;不把 recordings 管理依赖于 provider runtime。
05 · CLI 与配置合同
CLI 语法
openprogram recordings status [--json] openprogram recordings record [--name NAME] openprogram recordings replay <ID-or-PATH> openprogram recordings off openprogram recordings list [--json] openprogram recordings show <ID-or-PATH> [--json] [--content] openprogram recordings delete <ID> [--yes] openprogram recordings prune --older-than-days N [--dry-run] [--yes]
| 命令 | 确定行为 | 退出码 |
|---|---|---|
status | 显示 effective mode、已选文件、配置来源、文件可用性、是否需要重启。不得读取录制内容。 | 有效为 0;配置无效为 1。 |
record | 创建并校验 managed ID,以 0600 预创建只含 header 的文件,再单次事务写入 mode=record 和 selector;任何一步失败都删除本次新建的空录制并保持旧配置。不立即重启 worker。省略名称时生成 UTC 时间加随机后缀。 | 文件与配置均成功为 0。 |
replay | 先严格解析指定文件,成功后再以一次配置事务写入 mode=replay 和 selector。 | 文件无效时为 1,且配置不变。 |
off | 只把 mode 写为 off,保留最近 selector 便于后续显式复用。 | 成功为 0。 |
list/show/delete/prune | 见 §10。管理命令不改变 mode。 | 参数错误 2;运行错误 1。 |
配置 schema
{
"record_replay": {
"mode": "off", // off | record | replay
"file": "" // managed ID or explicit filesystem path
}
}
| SettingSpec | 类型与默认值 | apply | 校验 |
|---|---|---|---|
record_replay.mode | enum,默认 off | next_start | 只允许 off/record/replay。 |
record_replay.file | text,默认空 | next_start | off 可为空;record 为空时启动失败,CLI record 会预先生成;replay 必须是可严格读取的文件。 |
openprogram config set 仍可逐项修改;推荐的 recordings record/replay/off 用 setup.update_config() 一次写两个字段,避免中间状态。配置仅在 initialize_provider_runtime() 从 NEW 转入 INITIALIZING 时读取,READY 或 FAILED 后不再重读。修改后必须重启相关进程;命令输出必须明确打印这一点。管理命令本身只检查和更新配置,不触发 runtime 初始化。
06 · Registry 包装与生命周期
导入合同
import openprogram.providers 只完成 Python API 定义、模型 API 导出以及既有静态模型模块加载。它不得调用 register_builtins()、不得读取 record_replay 配置、不得打开录制文件、不得安装 transform,也不得通过导入 auth adapter 改变凭证注册表。这个合同用隔离子进程测试验证,不以代码注释代替;本任务不承诺清除模型目录模块已有的其他只读初始化。
职责与接口
| 接口 | 职责 | 禁止行为 |
|---|---|---|
register_builtins() | 线程安全、幂等地解析内建 provider,使用一次 registry batch 原子发布 API provider,并显式加载已有 auth adapter。 | 不读取 record/replay 配置,不访问录制文件,不安装 transform;内部 import 错误不得按“可选 SDK 缺失”静默忽略。 |
initialize_provider_runtime() | 串行执行 built-in 注册、配置快照、record/replay 预构造和 transform 发布。 | 不解析 credential,不创建 vendor client,不发起网络请求。 |
get_api_provider(api) | 作为公共 provider 获取边界,先确保 runtime READY,再从 registry 返回最终 provider。 | FAILED 时不得返回原 provider,也不得绕过录制/回放配置。 |
create_runtime(...) | 先确保 provider runtime READY;off/record 保持现有 runtime 路由,replay 根据显式或非凭证默认 provider/model 构造基础 Runtime。 | replay 下不得执行 CLI/AuthStore detection,也不得实例化会读取 credential 的 subscription runtime。 |
| recordings management | 读取/校验文件和更新 next-start 配置。 | 不调用 initialize_provider_runtime(),不依赖 provider registry。 |
api_registry 保留 register_api_provider(api, provider)、get_api_provider(api) 和一次性 transform。transform 必须同时处理初始化前已有的注册项和初始化后新增的注册项。
configure_provider_transform(transform) - 同一进程只允许设置一次;重复同值是 no-op,冲突值抛 RuntimeError - 保存 original provider,不对已经 transformed 的 provider 再包装 - 原子地替换当前 registry 的全部值 - 后续 register_api_provider(api, original) 自动存入 transform(api, original) record: shared_sink = RecordingSink(path) transform(api, original) -> RecordingProvider(original, shared_sink) replay: shared_replay = ReplayProvider(path) transform(api, original) -> shared_replay original 只用于确认该 api 已注册,不保存到 ReplayProvider,不允许委托
所有 record wrapper 共享一个 sink,所有 replay API 共享一个 ReplayProvider,因此 call_index 表示进程内所有 provider API 调用的总顺序,而不是每个 API 各自从 0 开始。direct library 用法继续支持独立构造器;产品激活不得要求调用方逐个枚举 API。
进程级状态
NEW
└─ initialize_provider_runtime() ─→ INITIALIZING
├─ 成功 ─→ READY
└─ 任意未完成退出 ─→ FAILED(保存不可变 failure descriptor)
- 状态和等待使用一个
threading.Condition;同一时刻只有一个线程执行初始化,其他线程等待 READY 或 FAILED。 - READY 后不再读取配置;配置修改继续遵循
next_start。 - FAILED 只在 built-in 注册阶段出现:runtime 保存 stage、cause type 和脱敏后的 cause message。每个调用方得到新的
ProviderRuntimeInitializationError,字段和消息一致;不得重复使用带旧 traceback 的异常对象。 - record/replay 激活失败不使 runtime FAILED:错误被记录,registry transform 被原子替换为 fail-closed
_BlockedRecordReplayProvider,runtime 进入 READY 且snapshot.mode == "blocked"。任何 provider 调用抛同一诊断,指示运行openprogram recordings status或openprogram recordings off;不解析凭据、不访问网络。 KeyboardInterrupt/SystemExit等控制异常在锁内写入initialization_interruptedfailure descriptor 并notify_all();初始化线程继续抛原控制异常,等待者和后续调用得到稳定的初始化失败。进程内不重试,重启后从 NEW 开始。- 构造
RecordingSink/ReplayProvider和完整校验在 transform 发布前完成;发布失败不得留下部分 registry 替换。 - off/record/replay/blocked/FAILED 的测试隔离使用新子进程,不增加可在生产代码中重置全局生命周期的 API。
Built-in 注册的可见性
register_builtins() 在自身锁内先导入和验证现有内建模块,在局部变量中形成 API provider batch,再由 API registry 一次发布;失败前不改变 API registry,只有完整成功后才设置 registered。Anthropic/Codex/Gemini auth adapter 继续调用各自现有幂等注册函数,但从 package import 移到这个显式阶段。若 auth adapter 失败,provider runtime 进入 FAILED,公共 get_api_provider() 因生命周期门禁不能观察或使用已发布 provider;进程重启后重建。项目当前把 Anthropic/OpenAI/Google SDK 声明为核心依赖,因此不再用包住整个 provider import 的 except ImportError 静默跳过内部缺陷。
显式启动与统一保证
Web 和 worker 在创建恢复线程、dispatcher 或 provider warm-up 线程前显式调用 initialize_provider_runtime(),使无效 replay 在单线程启动阶段失败。公共 get_api_provider() 仍执行幂等检查,覆盖 Python library 调用和未来入口。auth 的延迟 adapter 注册只调用 register_builtins();它不能因为 replay 文件损坏而失败。
Runtime 构造与 credential 边界
严格离线必须覆盖 create_runtime(),不能只覆盖 stream.py。当前 factory 和 subscription runtime 构造器会在 get_api_provider() 前读取 API key/OAuth。目标实现不重构这些 live runtime:off/record 继续走现有 factory;replay 增加一个 credential-free 分支,只接受调用方显式给出的 provider/model,或 AGENTIC_PROVIDER/AGENTIC_MODEL、配置文件 default_provider/default_model 中已有的非凭证选择。没有这些选择时立即返回 replay 配置错误,不继续执行 CLI 探测或 AuthStore 探测。
replay factory 不实例化 subscription runtime,而是按 PROVIDERS 的 provider→model namespace 纯元数据构造基础 Runtime。plain provider 已有该字段;三个 subscription entry 新增 model_namespace:claude-code→anthropic、openai-codex→openai-codex、gemini-cli→gemini-subscription。API-routed provider 继续使用其模型目录 namespace。这个分支不读取录制文件来推断原始 factory provider:现有 v1 request 只保存 wire-level Model,无法区分 claude-code 与 anthropic 等共享 wire 的入口,也不能把多 API 文件的首个 model 当成所有后续 runtime 的 identity。请求进入 ReplayProvider 后由现有完整 request comparison 校验最终 Model、context、tools 和 options;不匹配按既有 ReplayMismatch 报告。
与 OpenProgram 执行模型的关系
record/replay 是 provider 测试与故障复现能力,不是 workflow 能力。Agent loop、Runtime retry、工具执行、Session 持久化和 DAG 状态仍按正常代码执行;仅 registry 返回的 provider 从 live/record provider 变为 ReplayProvider。因此 replay 可以验证这些上层组件在同一模型事件序列下的行为,但不会恢复历史 Session、重放工具副作用、回滚文件或定义执行步骤。
07 · 录制文件、严格解析与并发写入
格式策略
新 writer 继续写 format_version=1 和现有四类行。call_end.outcome 是可选诊断字段:缺失等同 complete,因此旧 v1 文件和旧 reader 保持兼容;它不改变事件模型或 request 匹配。header 可增加可选元数据;v1 reader 必须忽略未知 header 字段。任何 request/event 的必需字段或语义发生变化时才升级版本。
{"type":"header","format_version":1,
"recording_id":"20260811T134502Z-f7a29c1d",
"created_at":"2026-08-11T13:45:02Z","redaction_version":1}
{"type":"request","call_index":0,"model":{...},"context":{...},"options":{...}}
{"type":"event","call_index":0,"event_index":0,"event":{...}}
{"type":"call_end","call_index":0,"event_count":1,"outcome":"complete"}
recording_id、created_at 和 redaction_version 只用于管理和诊断,不参与请求比较。旧 v1 文件没有这些字段时仍可回放,list/show 从 stat 与内容推导缺失信息。
严格读取不变量
文件级
- 第一条非空行必须是唯一 header。
- 每行必须是 JSON object;错误包含行号。
format_version必须是支持的整数。- 只接受 header/request/event/call_end。
调用级
- request 的
call_index唯一且从 0 连续。 - event 必须引用已有、未结束的 call。
event_index从 0 连续。- 每个 request 必须有且只有一个 call_end。
结束级
event_count等于实际 event 数。outcome只允许 complete/error/cancelled/abandoned;缺失按 complete 兼容。- call_end 后不再接受该 call 的 event。
- 未知 event type 在 provider 构造时失败,不延迟到中途。
- 空文件、截断末行和孤立行一律拒绝。
请求级
- 比较前对 incoming 使用同版脱敏。
- model/context/options 三段依序比较。
- 差异保留 call、field、recorded、incoming。
- 超过录制调用数时拒绝,不回退 live provider。
并发写入合同
RecordingSink持有进程内threading.RLock;每条 JSONL 的序列化和 append 在同一临界区完成。- 每次 append 以
os.open(O_WRONLY | O_CREAT | O_APPEND, 0o600)打开数据文件,并用openprogram._compat.flock(fd, LOCK_EX)提供跨进程互斥。 - 首次创建 header 与分配下一个
call_index都在文件锁内完成。最小实现可在 begin_call 时扫描现有 request 的最大索引;这是诊断文件路径,先接受 O(n) 分配,只有测得大文件成为瓶颈时才增加 sidecar counter。 - 一条完整 UTF-8 JSON 加换行使用循环
os.write写完;不得依赖一次 write 写全。 - request 和每个 event append 后不强制 fsync;call_end 写入后 fsync。这样崩溃最多留下一个可被严格 parser 识别的未完成 call。
- 跨线程/跨进程 event 可以交错,但
call_index与event_index把每个调用完整分组。回放顺序按 request 的 call_index。
08 · 0700/0600、路径约束与脱敏
文件系统
- managed root 为
get_state_dir() / "recordings";增加get_recordings_dir(),创建后收紧到 0700。 - managed recordings root 无论新建还是已存在,在 POSIX 上均收紧到 0700;库级显式外部
RecordingSink(path)不修改父目录权限,也不跟随父目录 symlink 执行 chmod。数据文件在第一次写入时以 0600 创建。record 写路径每次打开已有文件后调用openprogram._compat.restrict_to_user(path),修复历史宽权限;随后以fstat确认 mode 为 0600,无法保证时拒绝录制。 - replay 读取区分 managed 与显式外部路径:managed 文件可收紧为 0600;显式外部文件只验证当前为普通文件且 mode 已是 0600,不主动
chmod。外部文件权限过宽时明确拒绝,由文件所有者处理。 - 锁文件同样以 0600 创建。list/show/delete/prune 使用
lstat拒绝 symlink,并在 resolve 后验证 managed root 包含关系。 - record 模式使用
O_NOFOLLOW(平台支持时)和O_EXCL初始化新文件;不得截断已有录制。名称冲突直接失败。没有O_NOFOLLOW时,在持有 managed root 的目录 fd 下按 basename 打开,并用lstat/fstat的设备号、inode 与普通文件类型复核打开结果。 - delete/prune 只删除后缀为
.jsonl的普通文件;不递归、不跟随 symlink、不删除显式外部路径。
脱敏合同
remove_secret_values()在任何序列化之前执行,record 模式不存在关闭选项。- 保留当前大小写不敏感的敏感字段集合,并覆盖嵌套 dict/list。字段名识别
-/_与 camelCase 分段;以token、key、secret、password结尾的复合 header/option 名同样整体脱敏,例如X-Session-Token、service_api_key、refreshToken、clientSecret。不匹配token_count/tokenCount等非凭据计数字段。 - value pattern 覆盖 Authorization 常见 scheme(Bearer、Basic、Bot、token、Key)、URL userinfo,以及 query 中当前已实现的
api_key/apikey/access_token/token;新增 query key 必须同时提交实现和测试,不能只写入设计状态。 - 不采用通用“高熵字符串”规则:它会不稳定地改写正常 prompt,导致回放 mismatch。新增 provider 凭证形态必须以确定 pattern 和回归测试加入。
- record 与 replay 必须使用同一个
redaction_version分派。当前 v1 文件没有该字段时按 redaction v1 处理。
show --content 前的提示都必须说明这一点。09 · 严格离线回放合同
回放的离线保证由结构约束实现,而不是依赖调用方自觉避免网络。
- replay transform 对所有已注册和后注册 API 返回同一个
ReplayProvider。 ReplayProvider不保存 original provider,不 import vendor transport,不提供 fallback。stream.py必须先取得 provider;当 provider 声明requires_credentials = False时,不调用resolve_provider_key()。- 文件在 transform 安装前完整解析和验证;损坏文件阻止 worker 启动,不能启动到一半后才失败。
- 未知 API 仍由 registry 返回 missing-provider 错误;不得为了回放动态加载真实 provider。
- request mismatch、调用耗尽、未知事件、事件校验失败都直接抛异常;任何失败都不能切到真实 provider。
测试必须同时替换 socket.socket.connect、socket.create_connection 和共享 HTTP client 构造函数为失败函数,并断言正常回放、mismatch、损坏文件和调用耗尽四条路径都没有尝试网络。
10 · 录制文件管理
| 命令 | 默认输出/行为 | 安全规则 |
|---|---|---|
list | ID、created_at、格式版本、call/event 数、完整性、字节数、当前 active 标记;按创建时间倒序。--json 输出稳定字段。 | 单个损坏文件显示 invalid 与简短错误,不阻止列出其他文件;不输出请求内容。 |
show | 默认解析并显示 header、统计、每个 call 的 model provider/api/id、event 数和 complete 状态。--content 才打印完整的已脱敏 JSONL。 | 内容输出到 stdout 前打印 stderr 风险提示;JSON 模式不混入提示文本。 |
delete | 删除一个 managed ID。TTY 默认要求输入完整 ID 确认;非 TTY 没有 --yes 时退出 2。 | 拒绝 active selector、外部路径、目录和 symlink;删除不可恢复。 |
prune | 按文件 stat mtime 删除早于 N 天的 managed 文件;--dry-run 只列出。输出 matched/deleted/failed/bytes。 | N 必须为正整数;跳过 active、invalid symlink 和正在持有排他文件锁的文件;没有 --yes 时 TTY 确认,非 TTY 退出 2。 |
list/show 使用同一 inspect_recording(path) 纯函数,replay 激活也使用同一严格 parser。不能为管理命令另写一套宽松解析器。prune 的年龄来源使用 stat mtime;header created_at 只显示,不作为删除依据,避免手工编辑 header 改变保留行为。
11 · 失败语义
| 阶段 | 条件 | 异常/CLI 行为 | 是否允许继续 |
|---|---|---|---|
| Built-in 注册 | vendor 模块导入失败、auth adapter 注册失败、批发布中止 | runtime 进入 FAILED,保存不可变 failure descriptor(stage="builtins");每个调用方得到字段一致的新 ProviderRuntimeInitializationError;等待线程全部唤醒;不得返回 original provider。 | 管理命令可继续 |
| Record/replay 激活 | mode 未知、file 空、record 外部路径、replay 文件丢失、损坏或版本不符 | 结构化错误日志;registry transform 原子替换为 fail-closed _BlockedRecordReplayProvider;runtime READY 且 snapshot.mode == "blocked"。所有 provider 调用抛同一诊断,指示运行 openprogram recordings status/off;不解析凭据、不访问网络。 | 仅诊断 |
| 控制退出 | 初始化线程收到 KeyboardInterrupt/SystemExit | 状态进入 FAILED 并唤醒等待线程;initializer 抛原控制异常,其他调用方收到 initialization_interrupted;只允许进程重启重试。 | 管理命令可继续 |
| 创建 | 同名文件、symlink、目录不可写、无法收紧权限 | RecordingFileError;不覆盖已有文件。 | 否 |
| 写入 | 锁失败、磁盘满、序列化失败 | 终止当前 provider 调用并保留已写内容;不能只记录日志后继续 live 调用。 | 否 |
| 真实 provider | 开始/中途失败、调用取消或消费方提前关闭生成器 | 在 finally 写入带 outcome 的 call_end 并关闭源生成器,随后保留原异常/取消语义。整份文件仍可解析;replay 先校验请求,只在消费到该调用时报告对应中断,调用计数随后允许调用方重试并消费下一条录制。 | 按调用 |
| 解析 | JSON、版本、顺序、计数或事件类型无效 | RecordingFileError(path, line, call_index, reason)。 | 否 |
| 匹配 | incoming 与 recorded 不同 | 现有 ReplayMismatch 字段保留;CLI/Web 错误链显示 call 与 field。 | 否 |
| 调用耗尽 | incoming call 超过已录 call | ReplayMismatch(field_path="call_index")。 | 否 |
| 回放少调用 | 进程/测试结束但 recorded calls 未消费完 | 保留 assert_consumed();one-shot/program/test 在正常结束时调用并失败。长期 worker 的 status 显示 consumed/total,不在进程运行中提前判错。 | 按入口 |
| 管理删除 | 部分文件删除失败 | 逐文件报告;prune 继续其余文件并最终退出 1。 | 是 |
12 · 兼容与迁移规则
- 库 API:
RecordingProvider(provider, path)、ReplayProvider(path)、recording_path、call_count和现有异常属性保持。 - 现有 v1 文件:继续读取;新增严格检查会拒绝之前被宽松 reader 忽略的截断、孤立或重复行。这是有意的 fail-closed 行为。
- 旧 writer:header 只有 type/version 仍有效;管理元数据从 stat/内容推导并标为
legacy-v1。 - 默认运行:
mode=off时首次实际 provider 获取完成一次配置快照,随后请求不增加配置文件 I/O;provider identity 和 credential 解析顺序保持。 - 配置:缺少
record_replaysubtree 等同 off。不会从环境变量自动启用,也不会扫描并选取“最新录制”。 - 导入:
import openprogram.providers不再保证 built-in registry 已填充;实际 provider 获取和显式register_builtins()承担该责任。公开 stream/complete API 行为保持。 - 管理恢复:删除
_OPENPROGRAM_RECORDINGS_MANAGEMENT;status/off 等恢复能力由结构分离保证,不再依赖 import 顺序。 - Markdown:
record-replay.md/.zh.md与本页同步当前 CLI、严格离线和生命周期边界,不再保留“仅测试库模块”的过期描述。 - 未来格式:版本升级使用独立 parser;不原地改写用户文件。只在有明确字段语义变化时提供显式 migrate 命令。
13 · 最小实现文件
| 文件 | 最小改动 | 复用理由 |
|---|---|---|
openprogram/providers/initialization.py | 新增 runtime 状态、Condition、单次 built-in 注册、配置快照、transform 激活和失败传播。 | 生命周期协调独立于 recording 格式与 registry 存储。 |
openprogram/providers/register.py | 让 register_builtins() 线程安全,并只在完整执行后标记成功。 | 保留现有 vendor 注册清单,不复制到生命周期模块。 |
openprogram/providers/api_registry.py | get_api_provider() 在读取 registry 前确保 runtime READY;保留现有 transform 原子发布。 | 所有实际 API provider 获取的共同边界。 |
openprogram/providers/registry.py | 三个 subscription entry 增加纯 model_namespace 元数据;create_runtime() 先取得 lifecycle snapshot,仅 replay 增加无凭据的基础 Runtime 分支。 | off/record 的 live factory、subscription runtime 与 credential 行为保持。 |
openprogram/providers/__init__.py | 删除 built-in 注册和 record/replay 激活调用,仅保留导出。 | 消除包导入对运行状态和文件系统的影响。 |
openprogram/providers/recording.py | 保留格式与管理函数;激活函数由生命周期显式调用,不再参与 import。 | 不移动已经稳定的录制/回放逻辑。 |
openprogram/auth/credential_provider.py | 把 provider side-effect import 改为显式 register_builtins()。 | auth 只需要 adapter 注册,不应激活 replay。 |
openprogram/webui/server.py | 在线程启动前显式初始化 provider runtime。 | 保留现有单线程 preload 意图,并把失败边界变为显式 API。 |
openprogram/worker/runner.py | 在 start_web()、provider warm-up 和 channel 线程之前显式初始化。 | worker 对无效 replay 配置统一 fail-fast,不允许后台线程独立触发首次初始化。 |
openprogram/cli.py | 删除 recordings 环境变量哨兵,继续直接分派管理函数。 | 管理恢复由模块边界保证。 |
tests/providers/test_provider_initialization.py | 新增 import purity、状态机、并发、失败原子性、auth/Web 边界测试。 | 生命周期测试与格式、CLI 测试分开。 |
tests/providers/test_record_replay_cli.py | 补 status/off 对 missing、corrupt、unsupported-version 文件的真实子进程恢复矩阵。 | 验证用户恢复入口,不用单元 mock 代替启动行为。 |
不修改 vendor provider、off/record 的 live factory、Web routes、Node CLI 或录制格式。runtime factory 只增加 replay 分支。若实现时发现调用绕过 get_api_provider() 或插件绕过 register_api_provider(),应改回统一边界,不能在 recorder 或生命周期模块增加 vendor 特例。
14 · 验收与测试矩阵
| 维度 | 用例 | 关键断言 |
|---|---|---|
| Import 合同 | 隔离子进程只执行 import openprogram.providers | 不读取 config、不打开 recording、不导入 vendor SDK/auth adapter、不修改 API registry。 |
| 首次初始化 | 无配置 / mode off / record / replay | built-ins 和配置只处理一次;READY 后 provider identity、credential resolver 次数和现有行为保持。 |
| 并发初始化 | 多个线程同时首次 get_api_provider() | 只有一个初始化执行者;其余线程得到同一 READY provider 或同一 FAILED 原因;无死等。 |
| 失败原子性 | built-in 注册、配置读取、ReplayProvider 构造、transform 发布分别注入异常 | 不返回 original provider,不查询凭证,不构造网络 client;transform 不处于部分发布状态。 |
| 入口一致性 | Web 启动、worker、公共 stream、直接 library provider 获取、auth adapter 加载 | 实际 provider 使用确保 READY;Web/worker 在线程前 fail-fast;auth 只注册 built-ins,不激活 replay。 |
| Registry | 包装前注册、包装后注册、重复配置、两个 API | 已有/后续项均 transformed;冲突 transform 失败;record wrappers 共享 sink;replay 共享 call_count。 |
| 并发线程 | 两个线程各录多次 call/event | 每行合法 JSON;call_index 唯一连续;每个 event_index 连续;call_end count 正确。 |
| 并发进程 | 两个进程写同一 managed file | 单一 header、无半行、无重复 call_index、严格 parser 通过。 |
| 异常与取消 | provider 抛异常、任务取消、消费方 aclose()、同步启动失败 | 原控制流保持;源生成器关闭;call_end outcome 和 event_count 正确;整份文件可解析;replay 在对应调用失败而不影响后续计数。 |
| 激活失败 | replay 文件丢失/损坏、未知 mode、非法 record selector | 应用初始化不抛出;runtime READY,snapshot.mode == "blocked";provider 调用 fail-closed;不解析凭据、不访问网络;管理命令可恢复。 |
| 回放计数 | 同一请求先 mismatch 后修正重试 | mismatch 不增加 call_count;修正请求仍与同一 recorded call 比较;成功或已记录的中断才消费一次。 |
| 权限 | 新目录/文件/锁文件、既有宽权限目录、managed 历史 0644 文件、显式外部 0644 文件 | POSIX 0700/0600;record 与 managed replay 可收紧;外部 replay 只验证并拒绝宽权限,不修改 mode。 |
| 路径 | ../、绝对 record path、symlink、目录、外部 delete | record/delete/prune 拒绝逃逸;replay/show 外部文件只读可用。 |
| 脱敏 | 大小写字段、嵌套值、复合 token/key/secret/password 后缀、常见 auth scheme、URL userinfo/query、普通文本 | 凭证字节不在文件;token_count 等非凭证文本保持;record/replay 使用同版规则。 |
| 严格格式 | 空文件、坏 JSON、双 header、未知行、孤立 event、索引间隔、重复 end、错误 count、未知 event | 构造 ReplayProvider 时报告 path/line/call/reason。 |
| 严格离线 | 正常、mismatch、耗尽、损坏四条 replay 路径 | socket/http client sentinel 均未调用;credential resolver 未调用;无 live fallback。 |
| Runtime factory | 显式 provider/model、非凭证默认配置、无默认配置、plain/subscription provider | replay 使用 entry 的 model_namespace,不导入/构造 runtime_class,不执行 CLI/AuthStore detection,不调用 credential resolver;最终 wire-level Model 仍由 ReplayProvider 严格比较。 |
| CLI 配置 | record/replay/off/status、无效 selector、配置写并发 | 一次事务更新;失败不改配置;输出 restart 提示;profile 隔离;不初始化 runtime。 |
| 恢复命令 | replay 文件 missing、corrupt、unsupported version 时执行 status/off | 真实子进程可启动;status 报稳定诊断,off 成功写配置;不需要环境变量哨兵。 |
| 管理 | mixed valid/invalid list、metadata show、content show、active delete、dry-run prune、partial failure | 稳定 JSON schema;默认无内容;active 不删;结果计数与退出码正确。 |
| 旧文件 | 当前测试生成的 header-only v1 metadata | 正常文件继续回放;结构损坏的旧文件按新严格规则拒绝。 |
| 现有 loop | 多轮 tool loop record → replay | 事件类型、tool execution 和 final message 与当前测试一致。 |
实现后最小验证命令
pytest -q tests/providers/test_provider_initialization.py tests/providers/test_record_replay.py tests/providers/test_record_replay_registry.py tests/providers/test_record_replay_cli.py pytest -q tests/providers/test_scripted_provider.py tests/providers/test_registry_from_config.py tests/unit/test_usage_stream_chokepoint.py tests/unit/test_auth_methods.py tests/unit/test_web_config_schema.py tests/unit/test_config_write_safety.py python -m openprogram recordings --help python -m openprogram config get record_replay.mode python -m tools.docs_site.build python -m tools.docs_site.checklinks git diff --check
15 · 实现与验证记录
RecordingSink 以线程锁、跨进程 flock、完整 os.write 和 call_end fsync 写入单一 v1 JSONL;扩展固定脱敏并执行 0700/0600 与 symlink/普通文件检查。recording.py::RecordingSink、paths.py::get_recordings_dir;spawn 三进程并发测试严格解析 15 个连续 call。api_registry.py::configure_provider_transform、recording.py::activate_record_replay_from_config、test_record_replay_registry.py。replay.py::read_recording_file、ReplayProvider.requires_credentials、stream.py;结构失败、credential sentinel、多轮 tool loop 测试。record_replay.mode/file 为 next_start settings;CLI 提供 status/record/replay/off/list/show/delete/prune,配置失败回滚新文件,破坏性命令包含 active、symlink、外部路径、锁与非 TTY 确认保护。config_schema.py、cli.py、recording.py 和 CLI/管理测试。status/off 可直接恢复配置,环境变量哨兵已删除。e8c9f645、test_off_recovers_from_a_replay_file_that_became_invalid 与无导入副作用子进程测试。snapshot.mode="blocked"),auth/recordings/runtime 分离,以及 replay 模式只读模型解析。4d82d485..99252205;规格复审 PASS,质量复审 PASS;提交态 tests/unit + tests/providers 记录于本次交付说明。c26df53a、a4f27b3b、fe727546、6595d444;公共入口 RED/GREEN、规格复审 PASS、质量复审 PASS。test_record_replay.py 继续覆盖库级录制、两轮 tool loop、版本拒绝和字段差异。openprogram.functions.agentics.test_framework 阻断于 integration collection;本变更未修改该入口。设计来源:当前 OpenProgram provider 实现、CLI/config 入口、测试,以及仓内 references/pi-ai 快照。更新实现状态时必须附代码符号、测试或 commit;不得把“设计已写”标为“功能已实现”。