Files
CC-Switch/docs/pi-support-contracts-zh.md
T

823 lines
57 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pi 支持重设计:冻结契约与实施方案
> 文档状态:规范性设计契约(normative),是新 implementation 的第一交付物。
> 定稿日期:2026-07-31(UTC);v2 修订同日。
> 上游文档:[pi-support-handoff-zh.md](./pi-support-handoff-zh.md)。本文不重复其背景与七轮审查历史,只固定新实现的契约。
> 收敛过程:基于 handoff 诊断,经四轮独立架构对审(Claude Fable 5 × GPT-5.6)逐项交叉验证代码后收敛;双方多个初始立场被代码证据推翻,本文只记录最终结论。
> 注意:本文含诊断与决策背景,不适合作为 blind reviewer 的唯一材料;commit 1 需另交付纯规范性的 `docs/pi-support-review-contract-zh.md`(不含历史 finding、实现摘要与设计论辩)。
>
> 范围决定(用户 2026-07-31 拍板,v2):
> - 单个 PR 完成全部任务(handoff §2.2 的首选项),不再拆控制面/数据面两个 PR;
> - Pi 官方原生支持、且落在 CC Switch 既有管理域内的能力全部纳入:Provider/Model、Skills、AGENTS.md、**SYSTEM.md / APPEND_SYSTEM.md**、prompt templates、Sessions、proxy/failover(产品层例外);
> - **extensions:本 PR 只交付 design-only 附录(§13),不实现、不留代码占位**;
> - themes / keybindings:排除;MCP:维持排除(Pi 无原生 MCP,`McpAppId` 已正确排除 pi);OAuth `auth.json`:维持 Pi 所有权,不管理。
>
> Pi 官方实现 authority 固定为
> `ab366ebe94cacd419d986be454f12b1b9913aaca`。2026-07-31 Phase 0 刷新
> `ls-remote` 时远端 `HEAD` 已前进到
> `977ec833bbb86e245057e9162dbc1443c7b6e707`;后者仅是远端观察值,不是第二个
> implementation pin。所有 schema/oracle/源码注释以 `ab366ebe…` 的干净源码
> 快照为准。
## 0. 总诊断(一段话)
041ff113 七轮不收敛的根因是状态模型而非功能边界:Pi 是第一个打破仓库四个隐含 1:1 假设的 app——`app_type ↔ wire family``目录存在 ≈ 我方部署``Provider = 单行``settings 表 = 可迁移`——实现用调用点 `if app == "pi"` 分支模拟这些新区分,每修一处,fresh reviewer 就在另一处发现同类泄漏。修复方式是把区分写进类型与表(下述契约),并在单 PR 内用"控制面冻结点 + 累积检查点盲审"把每轮认证的新增面缩到"零 high"在概率上可达成的规模。
## 1. 契约 1:显式 Input Token Semantics
**落点**:新建 `src-tauri/src/proxy/usage/semantics.rs`;修改 `proxy/handler_context.rs`(或 handler config)、`proxy/usage/calculator.rs``proxy/usage/logger.rs``services/sql_helpers.rs``services/usage_stats.rs``src/types/usage.ts`
**类型**:
```rust
#[repr(i64)]
pub enum InputTokenSemantics {
TotalIncludesCacheBuckets = 1, // OpenAI/Gemini 系:input 已含 cached
FreshExcludesCache = 2, // Anthropic 系:input 即 fresh
}
pub enum StoredInputTokenSemantics {
Live(InputTokenSemantics),
LegacyTotalIncludesCacheRead = 3,
UnknownLegacy = 4,
}
```
**Authoritative source**:
- 新请求:wire adapter/response parser 的 `UsageParserConfig.input_semantics`,在请求上下文创建时由 wire family 显式确定;
- 落库后:`proxy_request_logs.input_token_semantics` 列;
- `app_type` 只保留逻辑归属与 UI 筛选;TS `KNOWN_APP_TYPES` 加入 `pi`,但永远不参与 token 语义。
**Legacy 迁移边界**:
- schema migration 是唯一允许从旧 `app_type` 推断语义的地方;
- 旧值 `0`:codex/gemini/grokbuild → `LegacyTotalIncludesCacheRead`;其他已发布 app → `FreshExcludesCache`;意外出现的未发布 Pi 旧行 → `UnknownLegacy`(不猜 family、不自动回填成本,UI 显示"语义未知");
- rollup 旧 `0` 全部迁为 `FreshExcludesCache`(rollup 已归一化);
- 迁移后 `0` 不得存在。不使用 DB trigger(仓库无此惯例):迁移事务消灭全部 `0` + typed DAO 写入签名不接受 `0` + runtime decoder/import 校验拒绝 `0`;所有 session import raw SQL 显式写语义。
**Success postcondition**:cost、明细、dedup hash、backfill、rollup、前端 fresh-input 展示读取同一个 stored semantics;Pi→OpenAI/Gemini 行 `app_type=pi` 且正确扣 cache,Pi→Anthropic 行不扣。
**Failure postcondition**:live writer 未提供语义时拒绝写入,不落"看似成功但计费错误"的行。
**并发边界**:语义在 request context 创建时复制,随异步日志任务移动;catalog/provider 后续变化不改变已 admission 请求的语义;request-id collision equality 包含语义。
**故障注入测试**(3):
1. 四 family cache-heavy 表:`1000 input / 800 cached`,Anthropic fresh=1000,其余三类 fresh=200,逻辑 app 均为 Pi;
2. 日志 spawn 后切换 provider/catalog,落库仍用请求 lease 中的语义,dedup 不把不同语义当同一事件;
3. 迁移中断后重跑幂等、无 `0` 残留、新 raw insert 缺语义被拒、Unknown Pi 行不被猜测。
## 2. 契约 2:Skill Deployment Ownership(三态模型)
**落点**:`database/schema.rs`、新建 `database/dao/skill_deployments.rs`、新建 `services/skill_deployment.rs`;`services/skill.rs` 的 Pi toggle/sync-all/uninstall/storage migration 改走新服务。
**表**:
```sql
CREATE TABLE skill_deployments (
app_type TEXT NOT NULL CHECK (app_type = 'pi'),
skill_id TEXT NOT NULL,
destination TEXT NOT NULL, -- 部署时词法规范化的绝对路径
destination_key TEXT NOT NULL, -- 按目标卷大小写语义生成,不用 SQLite NOCASE
method TEXT NOT NULL CHECK (method IN ('symlink', 'copy')), -- Auto 必须落地实际方法
source_identity TEXT NOT NULL,
deployed_digest TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY (app_type, skill_id, destination_key),
UNIQUE (app_type, destination_key)
);
```
- 不加 cascade FK:portable restore 可能替换 `skills` 表,不能因此丢本机删除证据;
- 本表进入 portable export/sync 的 skip + preserve 列表(device-local);
- 主键含 `destination_key`,允许 `PI_CODING_AGENT_DIR` 迁移期间同一 skill 短暂持有新旧两条记录。
**三态分离(authoritative source 各自独立,不得互相覆写)**:
- `desired_enabled`:`skills` 表的 Pi 启用位——用户意图;
- `owned_deployment`:`skill_deployments` 有匹配记录——所有权证据;
- `effectively_discovered`:实时 Pi discovery scanner(frontmatter name、first-wins、shadow)——实际生效。
禁止回归:`apply_effective_pi_status` 改写 `apps.pi`、迁移逻辑把 `apps.pi` 当所有权证据,均废除。API 返回三个独立字段(`SkillAppStatus { desired_enabled, owned_deployment, effectively_discovered, ownership, discovery, issue }`);UI toggle 只绑定 desired,"CC Switch 已部署"与"Pi 实际加载/被 shadow/原生活跃"独立显示;`desired=false, owned=false, discovered=true` 显示"原生 Skill 活跃",toggle 不点亮。
**Success postcondition**:
- enable:碰撞检查 → 临时路径原子部署(atomic rename)→ 单 DB 事务写 ledger + desired=true;
- disable:先持久化 desired=false → 仅当 ledger 存在且 identity/digest 匹配时删除目标 → 删 ledger。
**Failure postcondition**:
- 部署成功但记账前崩溃:留下 unowned orphan,不自动删除、不自动收养;
- 同名目录、同内容 copy、指向 SSOT 的 symlink 均为 unowned collision,默认拒绝;
- owned copy digest 漂移或 symlink retarget 后拒删,保留 ledger 与显式冲突状态;
- `PI_CODING_AGENT_DIR` 改变后旧记录显示 `stale_destination`,reconcile 先部署登记新目标,再仅在 digest/source identity 匹配时删旧目标。
**并发边界**:toggle、sync-all、uninstall、Skill 更新、storage migration 共用一个 deployment mutex;文件操作前后重新 `symlink_metadata`/identity check;本 PR 只启用 Pi,其他 app 未来通过显式 schema migration 扩充 CHECK。
**故障注入测试**(3):
1. 预置三种 collision(同名目录/同内容 copy/指向 SSOT 的 symlink),enable 不覆盖、不建 ledger、不改 desired;
2. 原子部署后、ledger 事务前崩溃,重启后显示 unowned/discovered,disable/uninstall 不删除;
3. 篡改 owned copy 或 retarget owned symlink 后执行 disable/storage migration 必须拒删;portable restore 不转移 ledger。
## 3. 契约 3:Provider Aggregate Hydration + Pi Catalog Coordinator
**落点**:`database/dao/providers.rs`(统一 hydration)、新建 `services/pi_catalog.rs`(协调器);`services/provider/mod.rs` 中所有 Pi 分支与手工补偿收编。
**统一 hydration**:
```rust
pub struct ProviderAggregate {
pub provider: Provider,
pub endpoints: IndexMap<String, CustomEndpoint>,
}
fn get_provider_aggregate(&self, app_type: &str, id: &str) -> Result<Option<ProviderAggregate>>;
fn get_all_provider_aggregates(&self, app_type: &str) -> Result<IndexMap<String, ProviderAggregate>>;
```
`ProviderAggregate` 只用于完整读取、rollback snapshot 与 restore,不是通用
mutation DTO。`get_provider_by_id()` / `get_all_providers()` 只能从上述函数转换;
禁止再存在 row-only Provider 读取路径。
**写所有权**:
- 删除 `save_provider()``save_provider_row_on_tx()`、一切
`upsert_provider*`、通用 aggregate save 及保持旧签名的兼容壳。read DTO
(`Provider`/`ProviderAggregate`/hydration 后的 `ProviderMeta`)不得作为写 DTO;
不提供从 read DTO 到 write DTO 的隐式 `From`/`Into`
- 写面只暴露类型化操作:
```rust
fn create_provider(input: NewProviderAggregate) -> Result<(), AppError>;
fn update_provider(key: &ProviderKey, row: &ProviderRowUpdate) -> Result<(), AppError>;
fn rename_db_only_additive_provider(input: RenameProvider) -> Result<(), AppError>;
fn add_provider_endpoint(key: &ProviderKey, endpoint: NewEndpoint) -> Result<(), AppError>;
fn remove_provider_endpoint(key: &ProviderKey, url: &str) -> Result<(), AppError>;
fn touch_provider_endpoint(key: &ProviderKey, url: &str, at: i64) -> Result<(), AppError>;
```
- `ProviderRowUpdate` 不含 `id`、endpoint 集合、`is_current`、
`in_failover_queue`、`sort_index`;这些状态只由专用 authority 修改。
`update_provider` 必须恰好更新一行,零行返回 NotFound,绝不转为 INSERT。
- `create_provider` 在单事务内严格 INSERT 主行与全部 `initial_endpoints`;目标已存在、
endpoint 重复或任一插入失败时整体零副作用,绝不转为 update/upsert。
- existing-provider 表单与 update IPC 不携带 endpoint 集合;后端收到非空
`custom_endpoints` 必须显式拒绝。create IPC 在 service boundary 拆出
`NewProviderAggregate.initial_endpoints`。`CustomEndpoint.added_at` 在
Rust/IPC/TS 全链路为 nullable,不得用 `0`、`COALESCE` 或 unwrap 默认值代替 NULL。
- rename 只允许 OpenCode/OpenClaw 的 DB-only、非 live、非 OMO additive provider。
单事务严格插入新主行、逐字段复制全部 endpoint
`url/added_at/last_used`(含 NULL)、删除旧主行;目标冲突与所有不允许的来源均整体
失败。`provider_health` 可重建并可丢弃;usage/history 保留旧 provider id。
- live import、seed、universal sync 等 reconcile 必须显式执行
“读取 → strict create 或 strict update”;并发 create 冲突返回 conflict,或重验
source version 后重试,不得覆盖更新。
- 完整 replacement 只允许 coordinator compensation 使用
`pub(super)`/sealed permit 的 `restore_provider_aggregate_on_tx()`;endpoint
add/remove/touch 只走专用操作。删除 Provider 继续由 FK cascade 删除 endpoints。
- 结构测试必须证明禁止符号为零,并只允许 provider row/state DAO、endpoint DAO、
schema migration 与 canonical data copier 对 `providers` /
`provider_endpoints` 发出生产 DML;扫描器须按 AST/SQL 类别区分并忽略
`#[cfg(test)]`。
**协调器**:
```rust
pub enum PiCatalogMutation {
CreateProvider { provider: Provider, initial_endpoints: Vec<NewProviderEndpoint> },
UpdateProvider { provider: Provider },
AddEndpoint { provider_id: String, endpoint: NewProviderEndpoint },
RemoveEndpoint { provider_id: String, url: String },
DeleteProvider { provider_id: String },
ImportNative { provider_key: String, expected_fingerprint: String },
SetDefault { provider_id: String, model_id: String },
// 数据面 commits 增加 route/failover/takeover variants,不改调用协议
}
pub async fn apply_pi_catalog_mutation(state: &AppState, mutation: PiCatalogMutation)
-> Result<PiCatalogMutationOutcome, PiCatalogMutationError>;
```
纯排序不在枚举内:`update_pi_route_order()` 只拿 Pi switch lock、执行单个 DB 事务,并(数据面起)原子发布下一份 even-epoch runtime snapshot,不碰 listener lock、odd epoch、admission 和 `models.json` 投影。
**固定顺序**:
```text
Pi switch lock
→ listener lock(数据面;冻结已绑定 origin)
→ begin epoch(odd / close admission)
→ load complete PiCatalogSnapshot
→ DB transaction apply mutation
→ atomic exact-key projection
→ build runtime from the same hydrated catalog
→ publish even epoch
→ activate listener generation
```
projection helper 不再自行 `begin_mutation()`;epoch 只能由协调器管理。
Provider 表单的 update payload 不得携带 endpoint 集合;endpoint 控件可位于同一页面,
但 submit 与 endpoint IPC 的写集合必须分离。
**Authoritative source**:managed aggregate=SQLite;exact key ownership=`pi_provider_projections`;`models.json`=共享投影;runtime=已成功发布 generation 的 immutable snapshot;`provider_health` 可重建,不进 rollback snapshot。
**Failure postcondition(显式状态机,绝不假成功)**:
- DB mutation 前失败:DB/文件不变,旧 runtime 以新 even epoch republish;
- DB mutation 后投影/runtime 失败:用完整 `PiCatalogSnapshot` 单事务恢复 provider rows + endpoints + claims + Pi route state,用 before/attempted claims 精确集合恢复文件,恢复成功后返回原错误,`authoritative_state = previous_restored`;
- DB restore 事务失败:其本身回滚,mutated DB 保持权威,按实际 DB reconcile,报告 `mutated_database_authoritative`;
- DB 已恢复但文件恢复失败:旧 DB 权威、gateway admission 关闭,报告 `projection_pending`。
**并发边界**:Provider CRUD、sort、default、Pi endpoint CRUD、failover queue、takeover start/stop、database restore 全部走同一 Pi switch boundary;锁序固定 `switch → listener → epoch → 短 DB 事务`;不跨文件 I/O 持 SQLite connection mutex;旧 lease 可完成响应,但 epoch 不匹配时不得写 usage/health。
**真实入口与故障注入测试**:
1. 经 `ProviderService::add` 创建含多个 initial endpoints 的 Provider,完整 hydration
逐字段读回相同集合;duplicate create 后主行、endpoint、current/failover 全不变;
2. existing-provider update 明确拒绝非空 endpoint payload;表单已去除 endpoint 后,
在 update 前经公开 service add/remove/touch 的结果全部存活;
3. 经 `ProviderService::update` 覆盖 DB-only rename 矩阵:成功保留全部 endpoint 与
NULL 时间戳;目标冲突、live、OMO、非 additive 来源均零副作用失败;
4. endpoint mutation 后 projection 失败:完整 aggregate 恢复且单项/全量 hydration
完全一致;
5. 带多个 `provider_endpoints` 的 Provider 删除后令 `models.json` atomic replace
失败:完整 aggregate、claims、旧 runtime 全恢复,`get_by_id` 与 `get_all`
hydration 完全一致;
6. rollback-reject trigger + projection failure 双注入:错误明确报告 mutated DB
authoritative;runtime 要么从实际 DB 重建,要么保持 admission closed,绝不发布旧
Provider;
7. projection 阶段阻塞并并发 provider edit、default switch、restore 和请求:
mutation 串行、odd epoch 拒绝新 admission、旧 writeback 被 fence、最终三层同
generation。
## 4. 契约 4:设备本地稳定 Gateway Token
**落点**:`settings.rs`、`commands/settings.rs`、新建 `pi_config/gateway_token.rs`、`database/backup.rs`。
**前置加固**(必须先做):`save_settings_file` 当前是 `.truncate(true)` 直写;改为同目录临时文件 + `0600` + flush/fsync + rename,并对已存在文件校验/收紧权限(Unix `.mode(0o600)` 只约束新建文件)。
**生命周期三分(彻底分开,不再混用)**:
- `gateway token`:设备安装级机密,跨进程重启稳定,存 `~/.cc-switch/settings.json`;仅显式"重置网关凭据"时旋转,并警告运行中的 Pi 会话需重启;
- `server_generation`:本进程 listener 实例级,只防旧 listener/runtime 误 admission;
- `catalog_epoch`:catalog mutation 级,只防请求与 writeback 跨 catalog 污染。
**Authoritative source**:设备文件中的 token;SQLite `settings.pi_gateway_token` 只作旧版迁移输入,永远不是新 authority。
**前端安全**:`get_settings_for_frontend()` 清空 token;`merge_settings_for_save()` 无条件从 existing 恢复(前端 payload 不能设置/清空/旋转);token 不进 TS settings 类型;`GatewayToken` newtype 的 Debug/Display 脱敏;token 比较不记录原值,使用常量时间比较。
**Legacy 迁移**:仅应用启动时从当前 live DB 迁移;device 文件已有 token 时 device 胜出并删 DB key;staged import/portable restore 的远端 token 直接删除,绝不 adopt;`export_sql_string()` 与 `export_sql_string_for_sync()` 各自断言产物不含 token key 也不含 value(不依赖迁移已完成)。
**Bind/reconcile 顺序**:
```text
bind exact loopback host/port
→ 持久化首次动态端口(persist_ephemeral_listen_port_if_needed 既有机制)
→ 读取稳定 token
→ close Pi admission
→ compare exact owned projection
→ mismatch 时 atomic reconcile(不旋转 token)
→ publish runtime(origin + token + catalog)
→ activate listener generation
```
**Success postcondition**:重启后同 host/port/token,停机期间连接失败的旧 Pi 进程恢复后可无缝重连;旧端口被占用时显式报"无法恢复原 listener",不偷偷换端口。
**Failure postcondition**:bind、settings durable write、projection reconcile 任一步失败:token 不旋转、Pi admission 保持关闭;listener 可继续服务其他 app,Pi takeover 显示 degraded/pending,不自动清除用户 desired takeover。
**故障注入测试**(3):
1. 模拟 Pi 只读一次投影,销毁旧 ProxyService、同端口建新实例:文件 token 不变、无需重投影、旧 bearer 成功;
2. 迁移在 settings fsync 后、DB delete 前崩溃并重跑:token 稳定、frontend/save roundtrip 不泄露不清空、两个 SQL export 均不含 key/value;
3. bind 后制造错误 port/token 投影并令文件只读:admission 从未打开、token 未旋转、unknown keys 不变;恢复权限后同 token reconcile 成功。
## 5. 契约 5:Pi Native Diagnostics 与显式导入
**落点**:新建 `pi_config/native.rs`(或 `diagnostics.rs`)、`commands/pi.rs`、`src/lib/api/pi.ts`;UI 新建 `components/providers/PiNativeCatalogPanel.tsx`。
**类型**:
```rust
pub struct PiNativeDiagnostic {
pub provider_key: String,
pub display_name: Option<String>,
pub fingerprint: String,
pub kind: PiNativeEntryKind, // BuiltInOverlay | CustomCatalog | ExtensionOverlay | UnknownShape
pub raw_validity: PiRawNativeValidity, // Valid | Invalid | Unknown
pub managed_assessment: PiManagedAssessment,
pub composition_status: PiCompositionStatus,
pub management_status: PiManagementStatus, // Importable | Managed{provider_id} | Unsupported
pub gateway_status: PiGatewayStatus, // Proxyable | DirectOnly | Unknown
pub reasons: Vec<PiDiagnosticReason>,
}
```
Reason 必须结构化为 `{layer,code,json_pointer?}`。raw schema failure、managed
narrowing、composition failure 与 gateway limitation 不得合并成一个
`malformed_native_entry`。
**四层判定**:
```text
importable = raw_validity == Valid AND managed_assessment == Manageable
proxyable = raw_validity == Valid
AND composition_status == Composed
AND gateway 已识别 API family
AND 完整 CandidateHeaderPlan 可构造
```
Raw 层只解释固定 Pi `models.json.providers[*]` TypeBox schema 的纯 JSON 判定图。
其输入与结果类型为无损边界:
```rust
struct PiRawApiId(String); // 任意 Pi-valid 非空 API ID
struct PiRawValidProvider { raw: serde_json::Value }
```
只有输入实际经过的 operator 全部位于 provenance manifest 的显式 allowlist 时才可
返回 Valid/Invalid;未知 operator、custom validator/transform、schema pin/hash
漂移或适用 schema 不确定时返回 Unknown。Raw 层:
- 直接消费 raw `serde_json::Value`,不得先反序列化为 managed 类型;
- 精确处理 absent/null、object/record/array/union/literal/enum、string/boolean/
Pi Number、additionalProperties、provider/model/modelOverrides、cost、headers、
input、thinking map与完整递归 `compat`;
- 不解释 built-in/extension registry、OAuth/env/`!command`、文件/平台/网络、
credential、composer fallback 或 gateway 能力。
Managed 层评估 CC Switch 可管理性。API ID 必须以 opaque string round-trip,不能由
四 family enum 收窄;仅出现新的 Pi API ID 不构成 managed rejection。每个其他额外
限制都返回独立 managed-layer reason:
model id 唯一非空、override key 必须闭合于显式 model 集、present URL 必须非空且为
绝对 HTTP(S)。Pi Number 不得收窄为整数;小数必须保持 JSON number 语义并可
round-trip。Managed 拒绝不得改写成 raw malformed,也不得控制 composer 是否执行。
Composer 层只组合“显式 custom catalog + 显式 models + matching overrides”的闭合
子集,直接消费 `PiRawValidProvider`。`PiComposedNativeModel` 保留 opaque
`PiRawApiId`、`thinking_level_map`/`compat` 的 `Value`、header map,以及
provider/model/override 中未知 shaping 字段。composer 是 credential-blind 纯函数:
OAuth、env、`!command`、文件或网络表达式只作为 deferred transport material 保留,
不得执行,也不得因此返回 Unknown。需要 pinned built-in/extension catalog/default
model context 且无法真实执行的语义才 fail-closed 为 Unknown,并给精确
catalog-required reason。
Gateway 层是四 family enum 唯一允许存在的位置。转换严格单向:
`raw valid → managed assessment`、`raw valid → native composition`、
`native composition → gateway candidate assessment`;gateway 不支持 family 是
DirectOnly 的已知事实,不得污染 composition。capability 阶段必须构造完整
`CandidateHeaderPlan`,验证 header 名/值表达式以及 protected/hop-by-hop 冲突;
request 阶段逐候选、发网前物化 auth/tenant/custom/protocol header。失败只淘汰当前
候选,不得复用上一候选的 URL、Host、auth 或 header。`Host` 只由当前 URL 生成;
protocol/wire-contract header 的最终值进入 failover identity,auth/tenant/普通及未知
自定义 header 不进入。
状态矩阵固定为:未知 API 的显式 custom catalog =
`raw Valid + Composed + Importable + DirectOnly(api_family_not_gateway_supported)`;
built-in/extension 仅因缺 catalog/default-model context =
`raw Valid + Unknown composition + Unknown gateway`。
Composer oracle 的 expected output 必须由固定 Pi 源码中的
`composeModelProvider`、`Provider.getModels`、
`resolveCompatibilityRequestConfig` 实际执行产生。provenance 记录 Pi commit、
入口、源码 hash、harness hash 与不可执行语义;本仓库手写 mirror 不得充当 oracle。
**Authoritative source**:diagnostic 每次读取当前 `models.json` 得出,不持久化;
managed Provider 只有显式 import 成功后由 SQLite 权威;raw validity、
manageability、composition 与 gateway capability 独立展示。
**Success postcondition**:import 经 `PiCatalogMutation::ImportNative` 在 switch lock 下重读 exact key 并比较 fingerprint(TOCTOU 防护);DB 事务原子插入 aggregate + exact projection claim;不写 `models.json`、不切默认、不解析 auth command/env;成功后 diagnostic 变为 Managed,Provider 列表出现;只有随后的用户操作才能进 failover queue。
**Failure postcondition**:inspection 永远无副作用;fingerprint 改变、claim collision、DB failure、unsupported shape 均不产生 Provider 或 ownership,原生 entry 原样保留并继续显示原因。彻底替代 041ff113 的"debug log + 静默跳过"。
**UI**:Pi Provider 页顶部独立"Pi 原生配置(只读)"面板,不用 ProviderCard,无 edit/delete/sort/failover actions;只有 `Importable` 显示"导入并由 CC Switch 管理";DirectOnly 导入后在 managed 卡片上明确显示"仅直连"。
**真实入口与 oracle 测试**:
1. 公开 inspection service 覆盖未知 API custom catalog 的
Valid/Composed/Importable/DirectOnly,以及 built-in/extension 的
Valid/Unknown/Unknown;managed rejection 不得改变 composition;
2. raw oracle 逐项匹配 TypeBox 1.3.7 实际执行,含嵌套 compat invalid、unknown
thinking key/value 与 union 边界;composer oracle 覆盖 fallback、重复 model、
三层 precedence、小数、unmatched override、URL、未知 API 与未知 thinking;
3. OAuth/env/`!command`/header material 无损保留且 composer 不执行;未知
provider/model/override 字段逐层保留;
4. gateway 覆盖非法 header、protected header、缺失 deferred value、跨 origin
URL/Host/auth 隔离、protocol identity 与四 family 成功物化;
5. 模块依赖结构测试禁止 raw/composer 导入 managed/gateway family,并禁止 gateway
反向构造 raw-valid 类型;inspect 后 fingerprint 改变或 import/claim 失败仍不产生
Provider/claim。
## 6. Managed Provider 数据模型(选项 b:provider 默认 + model 覆盖)
**落点**:新建 `pi_config/model.rs`(替代 041ff113 的 `PiProviderShape` 收窄式 validator)。
```rust
pub struct PiManagedProviderConfig {
pub base_url: Option<String>, // provider default
pub api: Option<PiManagedApiId>, // opaque provider default
pub api_key: Option<String>, // unresolved expression(env/!command 不解析)
pub headers: HeaderMap,
pub models: Vec<PiManagedModel>,
pub model_overrides: BTreeMap<String, PiManagedModelOverride>,
pub compat: Option<Value>,
pub extra: BTreeMap<String, Value>,
}
pub struct PiManagedModel {
pub id: String,
pub name: Option<String>,
pub base_url: Option<String>, // model override
pub api: Option<PiManagedApiId>, // opaque model override
// reasoning / input / contextWindow: PiNumber / maxTokens: PiNumber /
// compat(Value) / cost / headers / unknown extra(Value)...
}
pub fn effective_pi_model(provider: &PiManagedProviderConfig, model_id: &str)
-> Result<PiEffectiveModel, PiConfigError>;
```
managed 唯一继承规则:`api_family = model.api ?? provider.api`;`endpoint =
model.base_url ?? provider.base_url`。每个 managed model 必须能解析出两者;provider
默认可为空,只要所有 model 显式填写。路由单位固定为 `(provider, model,
effective opaque API id)`——一个 Pi provider 可以合法地 1:N。只有 gateway
adapter 把 opaque ID 映射为四种受支持 family。Native composer 的
默认模型 fallback 属于 Layer 3,不得偷偷扩张 managed 两级继承规则。
控制面的 Provider 表单必须支持 provider/model 两级 api 与 baseUrl,避免数据面 commits 反向改 schema(这是 041ff113 中 managed validator 收窄导致 M1 的历史教训)。
## 7. Failover:同 family、跨 host/base path、请求级
**落点**:重写 `proxy/pi_route.rs` 的 wire profile(endpoint/origin/API-root 移出 equality)、`proxy/forwarder.rs` 的候选物化。
**Eligibility 谓词**:
```text
eligible(primary, candidate, route) :=
candidate exposes route.model_id
AND effective_api_family(primary) == route.api_family
AND effective_api_family(candidate) == route.api_family
AND canonical_request_wire_profile(primary) == canonical_request_wire_profile(candidate)
AND canonical_protocol_header_profile(primary) == canonical_protocol_header_profile(candidate)
```
明确不进 equality:scheme/host/port/base path、provider/model `baseUrl`、API key 与认证 header、tenant headers、provider/model/modelOverrides 中普通自定义 headers、display/pricing/排序/默认模型等管理元数据。
**Wire shape 保守规则**:已知 shaping 字段(`compat`、`reasoning`、`thinkingLevelMap`、`input`、`contextWindow`、`maxTokens` 等)进 equality;除显式 transport/auth/display denylist 外的**未知字段默认纳入 equality**——Pi 新增 shaping 字段在 CC Switch 尚未理解时保守地阻止 failover,不错误放行。JSON object key 顺序不敏感,array 顺序敏感,不做数字/字符串宽松转换。
**Header 六分类**:
| 类别 | 处理 |
|---|---|
| Gateway/HTTP owned(`Host`、framing、hop-by-hop、proxy trace、客户端 gateway bearer) | 剥离或重建,不进 equality |
| Protocol/wire contract(`anthropic-version`、`anthropic-beta`、`openai-beta`/`openai-version`) | 最终有效值进 equality |
| Candidate auth(`Authorization`、`x-api-key`、`x-goog-api-key`) | 逐候选解析 |
| Candidate tenant(`openai-organization`、`openai-project`、`x-goog-user-project`) | 逐候选解析 |
| Provider custom(`headers` 容器中除 protocol registry 外全部) | 逐候选解析(属于目标中转商的传输身份) |
| Incoming request-local | 统一过滤后同值用于每个候选,不参与 equality |
未知 header 规则:来自 Pi 配置 `headers` 容器的一律 candidate-local(projector 不把上游 headers 暴露给 Pi,它们不改变网关收到的请求形态);未来某 header 被协议定义为 wire-semantic 时,必须在同一个 adapter 变更中加入 protocol registry 与谓词测试。已知 protocol header 使用不可预测 `!command` 表达式时,该候选标记 failover-ineligible;普通认证/私有 header 仍按 attempt 延迟解析。
**候选物化**:`materialize_pi_candidate()` 经 `effective_pi_model()` 得到该候选的 family/base URL/headers/auth,复用对应 family adapter 的 URL builder(删除 041ff113 的通用字符串拼接器);最终 `Host` 从该候选最终 URL 重建,不来自客户端或 primary。SSE 只允许在首个语义事件提交给 Pi 之前切换候选(comment keep-alive 不算提交);同一序列化请求 clone 给所有候选,不做候选级 body/model 改写。
**测试矩阵**(8 个函数):
1. `pi_route_identity_matrix`:provider 默认 API、model override API、缺模型、family 不同;
2. `pi_cross_origin_materialization_matrix`:不同 scheme/host/port/base path,最终 URL、Host、auth、tenant、自定义 header 全取自当前候选、不串值;
3. `pi_wire_profile_canonical_matrix`:key reorder 相等;已知/未知 shaping 变化排除;baseUrl/auth/display/pricing 变化不排除;
4. `pi_protocol_header_profile_matrix`:协议头差异排除;tenant/自定义/未知配置 header 差异允许;protected/hop-by-hop 拒绝;
5. `pi_four_family_candidate_materialization`:四 family、两级 api/baseUrl、header override 与各 adapter URL/auth 形式;
6. `pi_failover_error_policy_matrix`:网络/超时/401/403/404/408/409/429/其他非结构性 4xx/5xx 可换候选;400/405/406/413/414/415/422/501 不换;候选收到字节等价 body;
7. `pi_sse_commit_boundary_matrix`:连接失败/首包失败/keep-alive 后失败可切换;首个语义事件后禁止 replay;
8. `pi_snapshot_epoch_race`:并发编辑/排序与长请求使用单一 immutable snapshot,旧 epoch 不回写 health/usage、不改全局 current。
## 8. 契约 6:Pi 原生指令文件(AGENTS.md / SYSTEM.md / APPEND_SYSTEM.md)
先例:`prompt_files.rs` 已将每个 app 映射到一个原生指令文件并由 prompt library 投影;`hermes_config.rs:1006` 的 `MemoryKind` 是"一个 app 多个原生文件、按 kind 管理、不造影子状态"的既有模式。Pi 与 hermes 的语义差异:SYSTEM/APPEND 没有原生启用位,**文件存在即生效**。
**落点**:`prompt_files.rs`、新建 `services/pi_prompt_files.rs`、`commands/prompt.rs`、`src/lib/api/prompts.ts`、`PromptPanel.tsx` 重构。
**类型与 API**:
```rust
#[serde(rename_all = "snake_case")]
pub enum PiPromptFileKind {
GlobalContext, // ~/.pi/agent/AGENTS.md
SystemOverride, // ~/.pi/agent/SYSTEM.md
SystemAppend, // ~/.pi/agent/APPEND_SYSTEM.md
}
pub struct PiPromptFileRevision { pub exists: bool, pub sha256: Option<String> }
pub fn read_pi_prompt_file(kind: PiPromptFileKind) -> Result<PiPromptFileSnapshot>;
pub fn replace_pi_prompt_file(kind: PiPromptFileKind, expected: PiPromptFileRevision, content: &str)
-> Result<PiPromptFileSnapshot>;
pub fn delete_pi_prompt_file(kind: PiPromptFileKind, expected: PiPromptFileRevision) -> Result<bool>;
```
IPC 的 direct replace/delete 只允许 `SystemOverride` / `SystemAppend`;`GlobalContext` 由现有 Prompt library service 内部调用(不绕过单选启用与备份逻辑)。
**Enable/disable 语义(不造影子状态)**:`exists=true → active`,`exists=false → inactive`;创建即启用,确认删除即停用;UI 不提供 Switch、不写 DB enabled 位、不用 `.disabled`/改名/空内容 sentinel;whitespace-only 内容拒绝保存,用户必须显式选择"删除并停用"。状态每次从当前 Pi agent root 实时读取;`PI_CODING_AGENT_DIR` 改变时不迁移、不清理旧 root 文件。
**Prompt library 投影**:只投影 `GlobalContext`(与其他 app 的 CLAUDE.md/AGENTS.md 同构);SYSTEM/APPEND 不进 `prompts` 表、不参与 profile 切换/deep-link import/portable sync,只提供当前设备原生文件的直接查看、编辑、删除。新增共享文件保护:若 live AGENTS.md 的 revision 已偏离最后投影内容,disable/切换/清空必须报 conflict 并保留文件,不能凭 DB `enabled=true` 覆盖外部修改。
**UI 结构**:Pi 顶层 tab 从 `System Prompt | Prompt Templates` 改为 `Instruction Files | Prompt Templates`;Instruction Files 内三个子页:
1. **Global Context · AGENTS.md** — 现有 Prompt library 列表,文案"注入全局上下文",禁止称 system prompt;
2. **System Override · SYSTEM.md** — 直接 Markdown 编辑器 + 红色危险提示("`SYSTEM.md` 会整体替换 Pi 的内置 system prompt,可能移除 Pi 默认的工具说明、行为约束和运行指令;仅在明确需要完全接管时创建")+ 存在/不存在 badge,创建与删除均需确认;
3. **System Append · APPEND_SYSTEM.md** — 直接编辑器 + 琥珀色"追加到 Pi system prompt"说明,同样以存在/删除表达启停。
SYSTEM 与 APPEND 同时存在时的拼装次序与生效时点需对照 Pi 官方源码 commit 验证;验证前 UI 不提供"最终合成预览",不声称即时影响已运行会话。
**所有权与安全边界**:三个文件均为"用户 + CC Switch 共享";只访问 `get_pi_agent_dir().join(kind.filename())` 三个精确路径,不递归扫描、不清理其他文件;只接受 UTF-8 regular file(上限 1 MiB);symlink/目录/设备文件 fail closed;同目录临时文件 + atomic rename 并保留既有权限;replace/delete 要求 expected revision,外部修改后返回 conflict;三个文件操作共用进程内 mutex;不因隐藏 Pi、关闭 gateway、切 provider、恢复数据库而删除;SYSTEM/APPEND 不进 DB 备份/Profile/自动同步。Prompt Templates 仍在 `prompts/*.md`,即使存在名为 `SYSTEM.md` 的 template 也与 agent root 的 SYSTEM 是不同资源,UI 显示路径区分。
**故障注入测试**(4):
1. 三 kind 精确映射 agent root,SYSTEM/APPEND 存在状态不产生任何 DB 行;
2. 外部修改后用旧 revision 写/删必须 conflict;symlink 目标不读、不覆盖、不删除;
3. library prompt 切换只改 AGENTS.md,SYSTEM/APPEND 与未知文件逐字节不变;
4. 创建 SYSTEM 后 active、确认删除后 absent;空白保存不能暗中充当 disable。
## 9. 单 PR 结构与盲审协议
### 9.0 Canonical schema 与 native oracle artifacts
`schema.rs` 的 current-schema factory、current DDL 与 migration chain 是完整
canonical schema authority;其中 `CanonicalTableSpec` / `SchemaInvariantSpec`
机械描述 Provider/endpoint/device-local ledger 的承重 invariant,机器可读审查快照
位于 `tests/fixtures/pi/canonical-schema-manifest-v1.json`。测试必须从代码 spec
生成等价值并与该文件比较,禁止运行时反向读取 fixture 作为 authority。其余 current
user table 由下述穷举 `RestoreTableSpec` 锁定恢复策略与固定列。任何 manifest 只能
用于 code review/CI drift detection,不得认证外部 imported schema 或使其获得
publish 资格。
Native oracle 六件套固定在 `tests/fixtures/pi/native-oracle/`:
1. `provider-schema.snapshot.json`:固定 TypeBox schema JSON;
2. `raw-oracle-v1.json`:官方 TypeBox 1.3.7 `Value.Check` 的 valid/invalid 样例;
3. `composer-oracle-v1.json`:由固定上游 composer 入口实际执行得到的输入与期望结果;
4. `transport-oracle-v1.json`:由固定上游 transport resolver 实际执行得到的 deferred
value/header 解析结果;
5. `field-coverage-v1.json`:枚举 schema 的全部 canonical field path,并为每个字段
绑定成功的 TypeBox 执行和其所属 provider/model/override 层的成功 Pi composer
执行;
6. `provenance-v1.json`:Pi repo/commit、TypeBox version、schema/composer 源文件
路径与 SHA-256、schema/oracle SHA-256、generator 与 evaluator operator
allowlist。
CI 只消费 vendored artifacts,不运行 Node、不联网。更新 Pi pin、TypeBox 版本、源码
hash、schema snapshot 或 operator allowlist 必须显式重新生成并人工审查全部 oracle。
snapshot/provenance hash 不一致时 evaluator 返回 Unknown,不得继续猜测。
任一 schema field 缺少实际成功执行、引用不存在的 case、输入中没有该字段或只由
本仓库手写期望覆盖时,整个 bundle 认证失败;Pi 无法执行或无法证明的语义必须
fail-closed 为 Unknown。
另有三份机械边界快照:
1. `tests/fixtures/pi/provider-write-api-v1.json`:Provider write DTO 字段与唯一允许的
Database mutation signatures;
2. `tests/fixtures/pi/restore-policy-v1.json`:每张 canonical user table 恰好一种 restore
policy、固定列/父子拓扑及资源限制;
3. `tests/fixtures/pi/module-boundaries-v1.json`:raw/managed/composer/gateway 的允许依赖
方向。
快照由测试从代码 authority 生成等价值再比较;fixture 从不驱动生产行为。
### 9.1 Commit 序列(16 个,每个独立 green)
| # | Commit | 交付 |
|---:|---|---|
| 1 | `docs(pi): freeze support contracts and extension design` | 纯规范性审查契约(`pi-support-review-contract-zh.md`)、状态机、fixture 规范、extensions design-only 附录;themes 明确排除 |
| 2 | `domain(pi): add managed model and capability contracts` | capability、API family、两级 api/baseUrl、`effective_pi_model()`、diagnostic 类型、纯函数测试 |
| 3 | `db(pi): add aggregate hydration and device-local ledgers` | aggregate hydration、projection/skill ledger、迁移、portable/local 边界、DAO 测试 |
| 4 | `config(pi): add read-only native catalog inspection` | JSONC/CST reader、native diagnostics;不写原生配置 |
| 5 | `provider(pi): add catalog mutation coordinator` | CRUD/default/import、exact-key projection、完整 aggregate compensation |
| 6 | `skill(pi): add ownership-safe deployment reconciliation` | 三态、碰撞、崩溃、root relocation |
| 7 | `ui(pi): expose provider native and skill control plane` | Provider 表单、native panel、Skill 三态、四语 i18n |
| 8 | `prompt(pi): support native instruction files and templates` | AGENTS library 投影、SYSTEM/APPEND direct CRUD、PromptPanel 重构、templates(slug 收紧) |
| 9 | `session(pi): add tree-aware session management` | active branch、恢复/删除、relative sessionDir 显式不可枚举 |
| 10 | `test(pi): close and freeze control-plane contracts` | 控制面集成测试;生成并冻结 `tests/fixtures/pi/control-contract-v1.json` |
| 11 | `settings(pi): harden local settings and persist gateway credential` | settings 原子写/0600、stable token、legacy 清理、导出断言 |
| 12 | `gateway(pi): add listener admission and immutable runtime` | 端口连续性、bind reconcile、server generation、catalog epoch、takeover |
| 13 | `usage(pi): make input token semantics explicit` | typed 写入契约、legacy 迁移、四 family usage、UI filter |
| 14 | `routing(pi): materialize four native API families` | request route、`effective_pi_model()` 消费、逐候选 URL/auth/header |
| 15 | `failover(pi): add cross-host failover and SSE commit fence` | wire predicate、circuit breaker、retry policy、SSE semantic commit |
| 16 | `test(pi): close recovery concurrency and end-to-end boundaries` | restore、并发 mutation、stale writeback、长会话 token、完整 UI/i18n 验收 |
Extensions 只出现在 commit 1 的设计附录;无 Rust/TS/schema/UI 占位。
### 9.2 三个累积盲审检查点
- **检查点 A(commit 14)**:两位 fresh blind reviewer 审完整 `base..C4`;聚焦类型契约、DB 形态、只读 native 分类;未清零 validated blocker/high/data-integrity 不进入写配置阶段。
- **检查点 B(commit 110)**:两位全新 reviewer 审完整 `base..C10`;控制面必须零 validated blocker/high;通过后记录 `CONTROL_FREEZE_SHA=C10`,数据面开始。冻结内容:`PiManagedProviderConfig`/`PiEffectiveModel` 及继承规则、Provider IPC payload 与表单字段、managed/native/direct-only 分类、aggregate 与 ledger schema、`control-contract-v1.json`。数据面期间以等价检查守护:
```bash
git diff --exit-code "$CONTROL_FREEZE_SHA"..HEAD -- \
src-tauri/src/pi_config/model.rs \
src/components/providers/forms/PiProviderForm.tsx \
tests/fixtures/pi/control-contract-v1.json
```
(共享 `schema.rs` 因 usage migration 不能整文件冻结,由 schema snapshot/contract tests 约束 managed 表形态。)若数据面需要改上述字段或解释:禁止在 commit 1116"顺手修正"——停止数据面、修订规范与控制面 commits、检查点 B 失效并重新累积盲审,七轮计数不重置。
- **检查点 C(commit 116)**:两位全新 reviewer 审完整 `base..HEAD`(不能只看数据面);跑完整 Rust/frontend/migration/故障注入/renderer/diff 检查;主审逐条复现 finding 并审计整个 diff。
Reviewer 材料:纯规范性审查契约、精确冻结 SHA、中立仓库地图与 authority 表、
canonical schema manifest、Pi pin/provenance manifest、schema snapshot、带期望结果的
raw/composer oracle fixtures、完整验证命令与通用审查维度;不提供 handoff、既有审查
输出、实现摘要、设计论辩、疑似文件/行号、定向检查提示或另一 reviewer 结论。
### 9.3 七轮预算、体量红线与止损
- 三个计划检查点占三轮,保留四轮给 material repair;material fix 后由两位 fresh reviewer 重审完整累积范围,仅在明显收敛且补丁局部化时允许单 reviewer;
- 预计体量:6590 文件、净增 9k12k 行;**超过 12k 行或 90 文件触发一次 scope audit**,不得以"已经写完"为继续扩面的理由;
- 风险声明:9k12k 单 PR 在七轮内零 high 是现实可行但非高置信;三个累积检查点能提前暴露架构错误,但不消除最终 reviewer 审完整范围的认知负荷;
- 止损策略:
1. 检查点 C 后新出现且局部的数据面问题:只做一个聚合修复批次,下一轮重新双审完整范围;
2. 要求反改控制面 schema、或重复 A/B 已出现的不变量:立即停止局部补丁,回契约层并使 B 失效;
3. 同一 invariant 连续两轮失败:提前硬停止,不消耗剩余轮次做变体补丁;
4. 第七轮后仍有 blocker/high/data-integrity:不启动第八轮、不合并、不静默延期;提交审查历史、未解不变量与可选缩围方案。
当前重认证轮次固定为:R1–R3 已消耗;R4 只认证 commit 14 的新 checkpoint。
R4 若再次出现 endpoint ownership、canonical restore、native layering 任一家族,
立即项目级停止且不启动 R5。R4 clean 后 R5=检查点 B、R6=检查点 C、R7 仅可用于
最终局部修复重认证。R4 若出现新的 material finding,下一轮只能用于检查点 A 修复
重认证,不得同时计作 B。
### 9.4 Phase 0(开工前)
- 从最新 `main` 建新分支(041ff113 只作 diff/测试/UI 素材,不在其上追加);
- 重新执行 handoff §10.5 的 `git ls-remote` 并记录 Pi 官方与参考项目 commit;
- 重跑 handoff §16 状态检查;不动 `stash@{0}`。
2026-07-31 UTC 刷新记录:
| Repository | `git ls-remote … HEAD` | 本实现角色 |
|---|---|---|
| `earendil-works/pi` | `977ec833bbb86e245057e9162dbc1443c7b6e707` | 远端观察值;实现 authority 仍固定为经实际执行 oracle 的 `ab366ebe94cacd419d986be454f12b1b9913aaca` |
| `CallmeLins/pi-switch` | `5cfa2e8d3f6657b0508ee66ea63acdedfb53388d` | 产品层参考 |
| `Ginkgoooo/pi-cc-switch-provider` | `740c06692b15ad891e6d3ac3ee24c053d2626a9a` | 互操作参考 |
| `Wing900/Pi-switch` | `e2cc7f7837136277d317b742b650f402048ca80d` | 交互参考 |
## 10. 041ff113 复用判定
**可搬运(核心或测试夹具)**:`pi_config/document.rs`(JSONC/CST、unknown-field preservation、原子 patch 及测试);`get_pi_agent_dir` 与 `PI_CODING_AGENT_DIR` 解析;`pi_provider_projections` 表与大部分 DAO;session tree/active-branch parser 及 ID/cycle/size/path guards(仅替换 relative-root fallback);prompt template 单文件 CRUD 核心(收紧 slug 与 SYSTEM 分类);`PiRuntimeStore` 的 immutable lease、odd/even epoch、writeback fence 机制(snapshot 内容与调用顺序更换);Pi handler 的 path/model 提取与四 family parser fixtures;app icon、presets、默认模型 dialog、Prompt UI JSX、四语文案骨架。
**必须重写**:`PiProviderShape` validator 与全量 import/projector orchestration(→ typed model + diagnostics + coordinator);`previous Provider` 手工补偿;row-only `get_provider_by_id` 与 update 不协调 endpoints 的路径;`apply_effective_pi_status` 及 Pi toggle/sync-all/storage migration/uninstall;`services/proxy.rs` 的 token/投影/runtime 编排(保留 listener lock 思路);`backup.rs` 的 DB token preserve/restore;`pi_route.rs` 的 endpoint-in-wire-profile 与 provider-level shape;forwarder 的 provider-only base URL/header 解析(→ effective model);usage 三处 app whitelist 推断;`runtime.rs` 直接 child kill + 无界 reader join;native entry 的 debug-only skip;`PiProviderForm.tsx` 的必填 provider api/baseUrl;session relative fallback、prompt whitespace slug、AGENTS/SYSTEM 术语;SQLite gateway token 及一切依据 `enabled_pi`/path/content 自动生成 ownership 的迁移。
## 11. 相比 041ff113 的净减项
- 删除 SQLite token 存储、import 端保留特判及其协调概念(token 归 device 文件);
- 删除 endpoint/origin/API-root 的 wire equality 与 Pi 专属 same-origin 收窄;
- 删除 Pi 通用 URL 字符串拼接器,复用 family adapter transport;
- 删除 provider-level-only API 限制与"按 family 拆 synthetic provider"路径;
- 删除 `apps.pi` 同时充当 intent/ownership/discovery 的状态改写;
- 删除从既有路径/content 自动收养 Skill 所有权的迁移;
- 不引入通用 `managed_resource_deployments`、通用事务框架、未落地 compatibility group;
- 删除静默跳过 native entry(→ 只读诊断 + 显式导入);
- 删除 Pi failover 对全局 current 的持久切换与一切跨协议预留 glue;
- 删除 Rust/SQL/TS 三处 cache 语义平行推断;
- SYSTEM/APPEND 不建 enabled 影子状态、不进 DB,消除一整类同步/备份边界。
## 12. 验收标准
Handoff §14 全部条款继续适用,增补:
- `input_token_semantics` 迁移后无 `0` 残留;两个 SQL export 产物断言不含 token key/value;
- Skill 三态在 API/UI 可独立观察;三类 collision fixture 全部 fail closed;
- `get_provider_by_id` 与 `get_all_providers` 对同一 provider 返回逐字段一致的 aggregate;
- 契约 3 的三个 authoritative_state(`previous_restored` / `mutated_database_authoritative` / `projection_pending`)均有对应测试与用户可见错误报文;
- 跨 host failover 的 8 个矩阵测试全过;
- SYSTEM/APPEND 的 4 个契约测试全过;Pi Prompt UI 不再出现"system prompt"指代 AGENTS.md 的文案(四语);
- `CONTROL_FREEZE_SHA` 等价检查在数据面每个 commit 后通过;
- 同一 invariant 在检查点审查中重复失败两次即停并回契约层。
### 12.1 Canonical restore
外部数据库永远只是 data source,其 schema 不能获得 authority。运行时不得以
manifest、`sqlite_schema` 比较或“结构等价”把输入连接升级为可发布连接。类型屏障
固定为:
```rust
struct UntrustedScratch(Connection);
struct CanonicalStage(Connection); // 字段私有,仅 schema.rs current-schema factory 构造
```
`publish_canonical_stage` 只消费 `CanonicalStage`;不存在
`UntrustedScratch → CanonicalStage` 转换,也不存在接收裸 `Connection` 的 publish。
SQL import 与 binary restore 共用以下 data-only 管线:
```text
untrusted input
→ UntrustedScratch
→ scratch 中跑 current migration
→ 逐行 decode + RestoreTableSpec 固定列数据搬运
→ schema.rs 从空磁盘库创建全新 CanonicalStage
→ 从 live DB 固定列复制 device-local rows
→ canonical validation
→ SQLite Backup API publish
```
Canonical stage 必须由 `schema.rs` 建立当前全部 table/index/collation、PK conflict
policy、CHECK、FK action、canonical trigger/view、PRAGMA 与 `user_version`;不得从
source `sqlite_schema` 复制 table/index/trigger/view。restore 生产代码不得包含
`CREATE TABLE/INDEX/TRIGGER/VIEW` 字符串。
`RestoreTableSpec` 必须对当前每张 user table 恰好指定一种策略:
| 策略 | 当前语义 |
|---|---|
| `PortableIncoming` | 从不可信输入逐行验证并搬运 portable 数据 |
| `PreserveLive` | `pi_provider_projections`、`skill_deployments` 等设备本地数据只从 live 复制 |
| `RebuildRuntime` | `provider_health` 等运行态不从输入恢复 |
| `SeedCanonical` | 由 current schema/seeding authority 重建 |
机器测试必须证明 canonical user-table 集合与策略集合完全相等;新增表未选择策略即
失败。每个 spec 固定列名、复制顺序、nullable/storage 类型、row validator 与父子
拓扑,不得用 source `PRAGMA table_info`、`SELECT *` 或其他 introspection 决定搬运
列。
Data transfer 在 `foreign_keys=ON` 的单个 canonical-stage transaction 中使用 plain
`INSERT`;禁止 `INSERT OR IGNORE`、`INSERT OR REPLACE`、`REPLACE`、目标端
`ON CONFLICT DO UPDATE` 与复制 `sqlite_sequence`。重复 canonical key、弱 source
UNIQUE 产生的重复行、FK orphan、storage class 或 decoder 错误必须整体 abort。
Provider 行在写 stage 前必须用生产 hydration 的同一 decoder 验证
`settings_config`、`meta`、数值与 NULL,并保留未知 JSON 字段原字节语义,不得通过
重序列化抹掉未知字段。
Scratch 在执行不可信内容前必须关闭 trigger、关闭 `trusted_schema`、开启
defensive、设 attached-database limit 为 0,并设置 SQL/value/page-count 限制、有限
VM-step budget 与取消检查。不注册应用函数、不启用 load-extension;authorizer 至少
拒绝 ATTACH/DETACH、vtable、load-extension、writable-schema、非白名单 PRAGMA 与
unknown action。外部 batch 完成后删除 source trigger/view/显式 index,再运行可信
migration。`user_version > SCHEMA_VERSION` 立即拒绝;每个
`0..=SCHEMA_VERSION` 都必须走现有 migration chain,迁移后缺任一固定列或行 decode
失败即 abort。
文件入口必须 nofollow、regular、identity-stable、size-bounded。SQL 入口复用
`symlink_metadata → nofollow open → opened/current identity` 检查并有限读取;
binary 入口使用 `READ_ONLY | NOFOLLOW | PRIVATE_CACHE` 与同样 identity 检查。
边界冻结为 `MAX_SQL_IMPORT_BYTES=256MiB`、
`MAX_BINARY_RESTORE_BYTES=2GiB`、`MAX_SCRATCH_BYTES=2GiB`;改值必须同步更新
normative fixture 与 N/N+1 测试。
`pi_provider_projections` 与 `skill_deployments` 永不读取 source rows,只在既定
live/switch commit boundary 持 live DB lock 复制。AUTOINCREMENT 表可搬运显式 ID,
但不复制 `sqlite_sequence`,并验证下一次 INSERT 的 ID 大于当前最大值。WAL/SHM
永不复制、替换或重命名;binary source 只经 SQLite Backup API 克隆到 private
scratch,publish 也只经 Backup API。任一 publish 前失败保持 live 及本机 ledger
逐行不变;publish 后 reconcile 失败必须报告真实 authority/admission。
Publish 前必须执行 `integrity_check`、`foreign_key_check`、全表 decoder sweep 与
行为断言,至少实际证明 providers PK 为 BINARY、精确 duplicate 为 ABORT、duplicate
不会 REPLACE/级联删除 endpoints。C13 激活 usage 表语义时同步修改 canonical DDL、
migration/data mapping 与 restore validator。
SQL `import_sql_string*` 与 binary `restore_from_backup` 两个公开入口必须参数化覆盖:
1. NOCASE/REPLACE provider PK、弱 endpoint UNIQUE/FK、注入 trigger/view/index,
发布后只剩 current canonical schema;
2. 不可解码 Provider JSON、错误 storage class、重复 canonical key、FK orphan,
abort 且 live 与本机 ledgers 逐行不变;
3. NULL `added_at` 等 nullable 列与显式 AUTOINCREMENT ID 无损往返;
4. symlink、目录、FIFO、N+1、VM/page budget、ATTACH/VACUUM INTO 在 publish 前
失败;
5. 每个受支持 `user_version=0..SCHEMA_VERSION` 至少一个 migration sentinel,
current、上一版、最老版同时走 SQL 与 binary。
## 13. 附录:Pi Extensions(design-only,本 PR 不实现)
### 13.1 信息边界(诚实声明)
仓库内可确认:handoff 记录 Pi 核心有 extensions/packages/project resources;社区示例用 `pi install npm:...` / `pi install git:...`;`pi_config/tests.rs:392` 证明投影必须保留未知 `settings.json.packages` 与 `theme` 字段。仓库内**不可确认**:extension manifest、全局目录、load order、启停字段、inventory/remove 命令、trust 流程。社区安装示例不是 Pi 原生契约;以下所有条件能力在未来实现前必须对照 Pi 官方文档与固定源码 commit 验证。
### 13.2 未来可安全支持的窄能力(全部有条件)
1. **只读 inventory**:仅当可经官方 machine-readable CLI 或静态 manifest/config 获取、且不加载不执行 extension 代码;展示 native id、来源、scope、位置、原生启停状态与诊断;
2. **原生 enable/disable passthrough**:仅当 Pi 存在明确持久的原生启停位或官方 CLI 操作;状态 authority 始终是 Pi 原生配置,CC Switch 不建 DB mirror;
3. **install/remove 委托**:仅当官方 CLI 提供 scope 明确、非交互(或可安全交互)、结果可判定的接口;用户逐次确认后调用;不预设命令名/参数/返回格式;
4. **固定 extension 窄集成**:参考 `claude_plugin.rs` 的"检测 + 修改一个固定 owned key + 清理该 key"边界,但必须补 atomic write、revision conflict、exact-key ownership,不复制其整文件容错写法。
### 13.3 绝对不做
不实现 registry/依赖解析/semver/lockfile/升级策略等第二套 package manager;不下载/解压/安装 npm/git 内容;不替用户决定代码 trust、不自动批准首次执行;不管理 project-local extensions/resources;不做跨来源版本仲裁;不执行 extension 获取 metadata;不复制/接管 extension 凭据;不自动安装/更新/卸载;不把 extension 提供的 MCP 伪装成 Pi 原生 MCP(`McpAppId` 维持排除 pi);不因安装 extension 自动 claim/import 其 provider overlay;不顺带加入 themes/keybindings。
### 13.4 数据所有权
| 数据/资源 | Authority | CC Switch 允许 |
|---|---|---|
| Global extension inventory | Pi 官方配置/CLI | 请求期只读 DTO,不建持久 mirror |
| 原生 enabled 状态 | Pi 原生启停位(若存在) | exact-key/官方 CLI passthrough,无 DB enabled |
| Package bytes/依赖/lockfile | Pi 原生 package 机制 | 不直接写 |
| Project-local extension 与 trust | Pi + 项目用户 | 不管理、不修改 |
| Extension provider overlay | `models.json` | 复用 `PiNativeEntryKind::ExtensionOverlay`,不自动 claim |
| Extension 暴露的 Skills | Pi discovery | 显示为 native discovered,不写 `skill_deployments` |
| 凭据与私有配置 | Pi auth/env/extension | 不读取、不迁移、不导出 |
| 安装操作输出 | Pi CLI 子进程 | 仅本次 UI 展示与脱敏日志,不成为状态 authority |
### 13.5 与既有契约的复用点
Provider overlay 复用契约 5 的分层诊断;extension Skill 复用 discovery 展示(不伪造 ownership);若官方存在 enable key,复用 exact-key JSONC mutation + unknown-field preservation + revision conflict;若允许 CLI 委托,复用有界 stdout + 完整进程树 timeout/cancel;extension 安装不纳入 Provider/Skill mutation coordinator(安装目录与回滚 authority 属 Pi)。
### 13.6 未来实现前必须回答(对照固定官方 commit)
1. 官方 manifest、全局目录、project-local 目录分别是什么?
2. `settings.json.packages` 的确切 schema 与 load order?
3. extension 与 package 是否一一对应,是否存在非 package extension?
4. inventory 能否在不执行任意代码的前提下完成?
5. 是否有原生 enable/disable 位;若没有,放弃开关 UI;
6. 官方 list/install/remove 命令与 machine-readable 输出?
7. install/remove 是否触发 trust、postinstall、网络或项目文件修改?
8. Pi 与 CC Switch 并发修改原生配置的锁/冲突检测机制?
9. 卸载是否删除用户数据,是否有 dry-run/保留数据选项?
10. extension 注册的 provider/model 如何归因到具体 extension?
11. 操作失败后 Pi 的权威恢复/重扫机制?
以上未回答前,extensions 只停留在本附录:不创建 disabled UI、IPC command、数据库表或 capability flag。