diff --git a/docs/plans/ios-completion.md b/docs/plans/ios-completion.md new file mode 100644 index 0000000..3d044a2 --- /dev/null +++ b/docs/plans/ios-completion.md @@ -0,0 +1,149 @@ +# ios-completion — 收尾波:6 项整改 + P2 全波 + Android 令牌对齐 + +> 编排 doc。**orchestrator 冻结的跨 agent 契约在 §1,任何 builder 不得偏离**; +> 任务/Owns 表在 §2;验收在 §3。进度记录仍归 `docs/PROGRESS_LOG.md`(orchestrator 独写)。 + +审计依据:2026-07-29 的 16-agent iOS 完成度审计(43 计划任务 27 DONE / 11 PARTIAL / 5 MISSING)。 + +用户已定的三个范围问题: +1. **P2 全 5 项(T-iOS-31…35)在范围内** —— 计划 §7 要求"未按 P0 规格扩写 RED 清单前不得分派", + 故每个 P2 builder 的**第一步就是扩写自己那一项的 RED 测试清单**(写进本 doc §4 追加区),再进入 GREEN。 +2. **Apple 账号 = 免费个人 team**(Team ID `C738Z66SRW`,O=`Yaojia Wang`)。 + ⇒ **Push Notifications capability 不可用**:`aps-environment` entitlement 若默认挂上,真机构建会直接失败。 + entitlements 必须做成**默认不挂、env 开关**(见 A1)。 +3. **Android 的 `WEBTERM_TOKEN` 支持一起补**(与 iOS 侧文件完全不相交,可并行)。 + +--- + +## 1. 冻结契约(FROZEN — orchestrator 已核对服务端源码,builder 只许实现不许改) + +### 1.1 访问令牌(`WEBTERM_TOKEN`)—— 原生客户端接入方式 + +服务端事实(`src/http/auth.ts`、`src/server.ts:340-430,1353-1385`): + +| 项 | 值 | 出处 | +|---|---|---| +| Cookie 名 | **`webterm_auth`** | `auth.ts:30` `AUTH_COOKIE_NAME` | +| Cookie TTL | 2592000 秒(30 天) | `auth.ts:34` | +| 登录端点 | **`POST /auth`** | `server.ts:393` | +| 请求体 | `{"token":""}`,`Content-Type: application/json` | `server.ts:396,408-409` | +| **`Accept` 头** | **必须不含 `text/html`** | `server.ts:366-369,410` —— 含 `text/html` 会被当成表单走 302 重定向,而不是 204/401 | +| 成功 | **204**,带 `Set-Cookie: webterm_auth=…` | `server.ts:412-414` | +| 令牌错误 | **401** + `{"error":"invalid token"}` | `server.ts:418` | +| 限流 | **429**,10 次/分钟/IP | `server.ts:100,399-402` | +| **服务端未启用鉴权** | **204 但没有 `Set-Cookie`** | `server.ts:404-407` | +| 鉴权后 | 每个 HTTP 请求 + **WS upgrade** 都要带 `Cookie: webterm_auth=` | `server.ts:1375` | +| 令牌字符集 | `[A-Za-z0-9._~+/=-]`,长度 16–512 | CLAUDE.md / config 校验 | + +**实现决定(冻结)**: + +- 原生客户端**自己知道令牌**,因此 **直接手写 `Cookie: webterm_auth=` 请求头**即可, + **不解析 `Set-Cookie`**、不依赖系统 cookie 存储。理由:URLSession/OkHttp 的 cookie jar 在 + WS upgrade 上的行为不一致且难测;手写头与既有 `Origin` 手写头是同一个模式(`Endpoints.swift:81`), + 可被纯函数单测钉死。 +- `POST /auth` 只用作**配对期的一次性校验探针**: + - 204 **有** `Set-Cookie` ⇒ 令牌正确,保存; + - 204 **无** `Set-Cookie` ⇒ 该服务器**没开鉴权**,令牌不必保存(**不得**据此认为"已认证"); + - 401 ⇒ 令牌错,UI 报"令牌不正确";429 ⇒ UI 报"尝试过多,稍后再试"。 +- **`Cookie` 头与 `Origin` 头正交**:令牌**不替代** Origin 检查,两者都要带(`server.ts:1363-1379` 是先 Origin 再 cookie)。 +- **令牌是密级材料**:存 Keychain / Android Keystore-backed 存储, + `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`、**禁 `kSecAttrSynchronizable`**(沿用 `SecItemShim.swift` 既有约定); + **绝不写日志、绝不进 URL query、绝不进崩溃报告**。 +- **401 语义**:任何 RO/G 请求收到 401 ⇒ 抛类型化 `.unauthorized`(不是通用网络错), + UI 引导去补令牌。WS upgrade 收 401 ⇒ **终态,不进退避环**(与 `.replayTooLarge` 同级处理)。 + +### 1.2 git 面板端点(照 Android 已实现的形状;服务端为唯一真源) + +Origin-iff-G 规则(`Endpoints.swift:81`)照旧:**写操作必带 `Origin`,只读绝不带**。 + +| 端点 | 方法 | R/W | Android 参考实现 | +|---|---|---|---| +| `/projects/log` | GET | RO | `api-client/.../models/GitLog.kt` | +| `/projects/pr` | GET | RO | 同上 | +| `/projects/worktree/state` | GET | RO | — | +| `/projects/git/stage` | POST | **G** | `models/GitWrite.kt` | +| `/projects/git/commit` | POST | **G** | 同上 | +| `/projects/git/push` | POST | **G** | 同上 | +| `/projects/git/fetch` | POST | **G** | — | +| `/projects/worktree` | POST | **G** | `routes/GitRouteShapeTest.kt` | +| `/projects/worktree/prune` | POST | **G** | 同上 | +| `/sessions` | GET | RO | —(`claude --resume` 历史,T-iOS-32) | +| `/live-sessions/:id/queue` | POST | **G** | —(w2 pty 注入队列) | + +**真源是 `src/` 的实现**(`src/http/projects.ts` / `src/server.ts`),Android 只作交叉校验; +两者若不一致,**以 `src/` 为准并在返回条目里报告该不一致**。 + +### 1.3 免费 team 的签名与 entitlements(冻结) + +- `DEVELOPMENT_TEAM = C738Z66SRW`。 +- `CODE_SIGN_IDENTITY` 的历史值 `"iPhone Developer"` 是**已废弃**串,改 `"Apple Development"`(或删掉让 Automatic 自己选)。 +- `WebTerm.entitlements`(含 `aps-environment`)**默认不挂**。挂载由 **env 开关**控制, + 未设时 `CODE_SIGN_ENTITLEMENTS` 必须为空 —— 免费 team 挂上就真机构建失败。 + 开关名冻结为 **`WEBTERM_PUSH_ENTITLEMENTS`**(值=entitlements 相对路径),并写进 `ios/README.md`。 +- `UIBackgroundModes: [remote-notification]` 可以无条件加(背景模式不是 capability,免费 team 不受限)。 + +--- + +## 2. 任务表(Owns 铁律:不得编辑本任务 Owns 之外的文件) + +### Wave A(串行,1 agent)—— 全员依赖的工程根 + +| ID | 任务 | Owns | +|---|---|---| +| **A1** | 签名解锁 + Info.plist 全量键 + 可选 entitlements | `ios/project.yml`、`ios/App/WebTerm/WebTerm.entitlements`(新) | + +A1 必须一次把**后续所有波次需要的 Info.plist 键**都加好(它是 project.yml 的唯一 owner): +`NSMicrophoneUsageDescription`、`NSSpeechRecognitionUsageDescription`(T-iOS-31)、 +`UIBackgroundModes: [remote-notification]`(T-iOS-21)。 + +### Wave B(并行 5 agent,目录互斥)—— 包层 + CI + Android + +| ID | 任务 | Owns | +|---|---|---| +| **B1** | APIClient 全部新端点(§1.1 `/auth` + §1.2 全表)+ 模型 + 测试,覆盖率≥80% | `ios/Packages/APIClient/**` | +| **B2** | HostRegistry:按主机存访问令牌(Keychain,§1.1 存储约定)+ 迁移安全 | `ios/Packages/HostRegistry/**` | +| **B3** | SessionCore:WS upgrade 带 `Cookie`、401→终态 `.unauthorized`、不进退避环 | `ios/Packages/SessionCore/**` | +| **B4** | ClientTLS 覆盖率 55.76%→≥80% + 纳入覆盖率门 + 修 CI 三条腿 | `ios/Packages/ClientTLS/Tests/**`、`ios/IntegrationTests/scripts/coverage-gate.sh`、`.github/workflows/ios.yml` | +| **B5** | Android `WEBTERM_TOKEN` 支持(§1.1 同一契约,OkHttp 侧) | `android/**` | + +### Wave C(**串行** 4 agent)—— App 层 + +串行原因:新增 `.swift` 文件必须 `xcodegen generate` 重写**共享的** `.xcodeproj`,并行会互相踩; +串行后无文件归属冲突,每个 agent 可自由跑 `xcodegen + xcodebuild` 全量验证。 + +| ID | 任务 | 内容 | +|---|---|---| +| **C1** | 令牌 UI + 传输接线;移除主机 UI(顺带修 `PushRegistrar.handleHostRemoved` 死钩子) | +| **C2** | git 面板 UI + worktree 表(**含 T-iOS-32**)+ `claude --resume` 历史 | +| **C3** | 终端内搜索(**T-iOS-33**)+ 语音 PTT(**T-iOS-31**) | +| **C4** | 主题 + Dynamic Type(**T-iOS-34**)+ web `?join=` 互通(**T-iOS-35**) | + +### Wave D(并行 3 agent,report-only / 文档) + +| ID | 任务 | +|---|---| +| **D1** | 安全复核:令牌路径(两端)+ entitlements + 无令牌泄漏(日志/URL/崩溃) | +| **D2** | 全量验收(**T-iOS-36** + T-iOS-18/19/30 的机器可执行部分):包测/App 测/集成/覆盖率门/模拟器构建/真机构建尝试,只报真实数字 | +| **D3** | 文档:`README.md`、`ios/README.md`、`docs/PLAN_IOS_CLIENT.md`、`docs/PLAN_IOS_IPAD.md` 勾选与过期表述 | + +### Wave E(串行,条件触发) + +| ID | 任务 | +|---|---| +| **E1** | 修 D1/D2 报出的 CRITICAL/HIGH(无则跳过) | + +--- + +## 3. 验收门(每个 builder 自查,D2 复核) + +- TDD:先 RED 再 GREEN(§4 的 RED 清单是 P2 任务的前置交付物)。 +- `swift build` + `swift test` 本包全绿;改到 App 层则 `xcodegen generate` + iPhone 16 Pro 模拟器 `xcodebuild ... test` 全绿。 +- 受门包覆盖率 ≥80%(B4 后 ClientTLS 也进门)。 +- 零 `TODO`/`FIXME`/占位实现;零硬编码密钥;令牌零日志。 +- **诚实报告**:跑不了的(真机/付费账号/CI 平台)写 DEFERRED 并给出手工步骤,**不得报"通过"**。 + +--- + +## 4. P2 RED 测试清单(由各 P2 builder 在开工第一步追加到本节) + +