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

57 KiB
Raw Blame History

Pi 支持重设计:冻结契约与实施方案

文档状态:规范性设计契约(normative),是新 implementation 的第一交付物。 定稿日期:2026-07-31(UTC);v2 修订同日。 上游文档: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.rsproxy/usage/logger.rsservices/sql_helpers.rsservices/usage_stats.rssrc/types/usage.ts

类型:

#[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 改走新服务。

:

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:

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

  • 写面只暴露类型化操作:

    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_currentin_failover_queuesort_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_endpointsCustomEndpoint.added_at 在 Rust/IPC/TS 全链路为 nullable,不得用 0COALESCE 或 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)]

协调器:

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 投影。

固定顺序:

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_idget_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.rscommands/settings.rs、新建 pi_config/gateway_token.rsdatabase/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 顺序:

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.rssrc/lib/api/pi.ts;UI 新建 components/providers/PiNativeCatalogPanel.tsx

类型:

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

四层判定:

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 判定图。 其输入与结果类型为无损边界:

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”的闭合 子集,直接消费 PiRawValidProviderPiComposedNativeModel 保留 opaque PiRawApiIdthinking_level_map/compatValue、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 assessmentraw valid → native compositionnative 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 源码中的 composeModelProviderProvider.getModelsresolveCompatibilityRequestConfig 实际执行产生。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)。

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 谓词:

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 字段(compatreasoningthinkingLevelMapinputcontextWindowmaxTokens 等)进 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-versionanthropic-betaopenai-beta/openai-version) 最终有效值进 equality
Candidate auth(Authorizationx-api-keyx-goog-api-key) 逐候选解析
Candidate tenant(openai-organizationopenai-projectx-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:1006MemoryKind 是"一个 app 多个原生文件、按 kind 管理、不造影子状态"的既有模式。Pi 与 hermes 的语义差异:SYSTEM/APPEND 没有原生启用位,文件存在即生效

落点:prompt_files.rs、新建 services/pi_prompt_files.rscommands/prompt.rssrc/lib/api/prompts.tsPromptPanel.tsx 重构。

类型与 API:

#[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。数据面期间以等价检查守护:

    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;
  • 预计体量:65–90 文件、净增 9k–12k 行;超过 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_dirPI_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_idget_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 比较或“结构等价”把输入连接升级为可发布连接。类型屏障 固定为:

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 管线:

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_projectionsskill_deployments 等设备本地数据只从 live 复制
RebuildRuntime provider_health 等运行态不从输入恢复
SeedCanonical 由 current schema/seeding authority 重建

机器测试必须证明 canonical user-table 集合与策略集合完全相等;新增表未选择策略即 失败。每个 spec 固定列名、复制顺序、nullable/storage 类型、row validator 与父子 拓扑,不得用 source PRAGMA table_infoSELECT * 或其他 introspection 决定搬运 列。

Data transfer 在 foreign_keys=ON 的单个 canonical-stage transaction 中使用 plain INSERT;禁止 INSERT OR IGNOREINSERT OR REPLACEREPLACE、目标端 ON CONFLICT DO UPDATE 与复制 sqlite_sequence。重复 canonical key、弱 source UNIQUE 产生的重复行、FK orphan、storage class 或 decoder 错误必须整体 abort。 Provider 行在写 stage 前必须用生产 hydration 的同一 decoder 验证 settings_configmeta、数值与 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=256MiBMAX_BINARY_RESTORE_BYTES=2GiBMAX_SCRATCH_BYTES=2GiB;改值必须同步更新 normative fixture 与 N/N+1 测试。

pi_provider_projectionsskill_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_checkforeign_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.packagestheme 字段。仓库内不可确认: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。