@@ -14,124 +14,134 @@ requirements:
1414 - REQ-F7-A
1515must_haves :
1616 truths :
17- - " 存在 `0014_claude_account_persistent_volume.sql`,对 `claude_accounts` 增加可空 `persistent_volume_name`(语义见 D-02)"
18- - " `ClaudeAccount` 模型可表达该列的 NULL/非 NULL;`GetHostByShortID` 返回 `hosts. template_image_ref` 供 Entry 推导镜像 tag(D-06 前置) "
19- - " `Repository.ResolveClaudeAccountIDForEntry`(或等价命名)严格按 D-05 两条 SQL 顺序解析;无匹配时 `ok=false` 且非错误 "
17+ - " 数据层具备 `persistent_volume_name` 列,且 `NULL` 语义可表达“未分配”状态(D-01, D-02, D-10 )"
18+ - " 仓储层可按 D-05 规则稳定解析 `claude_account_id`,并返回 Entry API 所需 ` template_image_ref` 输入 "
19+ - " 本计划不触碰 HTTP/客户端协议,避免与 Wave 2 计划重叠 "
2020 artifacts :
2121 - path : internal/store/migrations/0014_claude_account_persistent_volume.sql
22- provides : " ADD COLUMN IF NOT EXISTS persistent_volume_name;含可手工执行的 down 片段注释(DROP IF EXISTS) "
22+ provides : " claude_accounts. persistent_volume_name schema 变更与安全回滚注释 "
2323 contains : persistent_volume_name
2424 - path : internal/store/repository/models.go
25- provides : " ClaudeAccount.PersistentVolumeName 可空字段 "
25+ provides : " ClaudeAccount 可空字段映射 "
2626 contains : PersistentVolumeName
2727 - path : internal/store/repository/queries.go
28- provides : " HostSSHAuth 扩展 TemplateImageRef;Resolve 方法 "
28+ provides : " ResolveClaudeAccountIDForEntry + template_image_ref 查询输出 "
2929 contains : ResolveClaudeAccountIDForEntry
3030 key_links :
31+ - from : ResolveClaudeAccountIDForEntry
32+ to : claude_accounts
33+ via : " host_id 优先,fallback 到 user_id + host_id IS NULL(D-05)"
34+ pattern : ORDER BY created_at ASC LIMIT 1
3135 - from : GetHostByShortID
3236 to : hosts.template_image_ref
33- via : " SELECT 列扩展 "
37+ via : " SELECT 列扩展供 Wave 2 推导能力字段 "
3438 pattern : template_image_ref
35- - from : ResolveClaudeAccountIDForEntry
36- to : claude_accounts
37- via : " host_id 优先、再 user_id+host_id IS NULL(D-05)"
3839---
3940
40- ## 目标
41-
42- 落地 Phase 30 数据面:` claude_accounts.persistent_volume_name ` 列(REQ-F7-A / Q4 → D-01、D-02、D-10),并为 Entry 握手提供 ** ` template_image_ref ` 与确定性 ` claude_account_id ` 解析** 的仓储能力(D-05、D-11),** 不** 在本 plan 修改 HTTP handler 或 ` HostActionRequest ` (属 plan 02)。
43-
44- ## 执行上下文
45-
46- - 讨论锁定:` .planning/phases/30-entry-api/30-CONTEXT.md `
47- - 研究备忘:` .planning/phases/30-entry-api/30-RESEARCH.md `
41+ <objective >
42+ 完成 Phase 30 的数据层基础:数据库迁移、仓储模型与账号解析查询,给 Phase 31/33 提供握手和 volume 命名所需的统一数据来源。
4843
49- ## 范围说明
44+ Purpose: 先锁定数据真值,确保 API 层只做协议拼装而不再重复定义规则。
45+ Output: ` 0014 ` migration + repository 查询/测试,形成 Wave 2 的稳定依赖。
46+ </objective >
5047
51- ### 纳入
48+ <execution_context>
49+ @.planning/phases/30-entry-api/30-CONTEXT.md
50+ @.planning/phases/30-entry-api/30-RESEARCH.md
51+ </execution_context>
5252
53- - 新迁移文件名必须为 ** ` 0014_claude_account_persistent_volume.sql ` ** (D-10)。` ALTER TABLE ... ADD COLUMN IF NOT EXISTS persistent_volume_name TEXT ` ;** 禁止** 用空字符串表示未分配(D-02)。列可为 NULL;不设 ` NOT NULL DEFAULT '' ` 。
54- - 文件末尾用 ** SQL 注释** 给出可手工执行的 down 片段:` ALTER TABLE claude_accounts DROP COLUMN IF EXISTS persistent_volume_name; ` ,以满足 ROADMAP「可回滚」叙述且不与当前仅正向的 ` migrator ` 冲突。
55- - ` ClaudeAccount ` 增加 ` PersistentVolumeName *string ` (或 ` sql.NullString ` ,与仓库现有可空风格一致)。
56- - ` HostSSHAuth ` 增加 ` TemplateImageRef string ` (或从 DB COALESCE 为空串),` GetHostByShortID ` 的 ` SELECT ` 增加 ` h.template_image_ref ` (及 ` COALESCE ` 若需)。
57- - 新增 ` func (r *Repository) ResolveClaudeAccountIDForEntry(ctx context.Context, hostID, userID string) (id string, ok bool, err error) ` :
58- 1 . 先查 ` host_id = $hostID ORDER BY created_at ASC LIMIT 1 ` ;
59- 2 . 若无行,再查 ` user_id = $userID AND host_id IS NULL ORDER BY created_at ASC LIMIT 1 ` ;
60- 3 . 仍无行则 ` ok=false ` 、` id=="" ` 、` err==nil ` (D-05 步骤 3)。
61- - 若未来已有 ` ListClaudeAccounts ` 等扫描 ` claude_accounts ` 的查询,本 plan 一并补扫 ` persistent_volume_name ` ;当前仓库若无此类 SQL,仅保证模型与迁移一致即可。
62-
63- ### 排除(D-12 / 其它阶段)
64-
65- - admin / GraphQL 新 surface;` docker volume create ` ;Entry JSON 响应体(plan 02)。
53+ <context >
54+ @.planning/ROADMAP.md
55+ @.planning/REQUIREMENTS.md
56+ @internal/store /migrations/0007_auth_unification.sql
57+ @internal/store /repository/models.go
58+ @internal/store /repository/queries.go
59+ </context >
6660
6761<tasks >
6862
6963<task type =" auto " tdd =" true " >
70- <name >任务 1:迁移 0014 + ClaudeAccount 模型字段</name >
71- <files >internal/store/migrations/0014_claude_account_persistent_volume.sql,internal/store/repository/models.go,internal/store/repository/migration_0014_test.go</files >
64+ <name >Task 1: 落地 0014 migration 与可空字段语义</name >
65+ <files >internal/store/migrations/0014_claude_account_persistent_volume.sql, internal/store/repository/models.go, internal/store/repository/migration_0014_test.go</files >
66+ <read_first>.planning/phases/30-entry-api/30-CONTEXT.md (D-01,D-02,D-10), internal/store/migrations/0007_auth_unification.sql</read_first>
7267 <behavior >
73- - 测试:迁移文件存在且包含 `ADD COLUMN IF NOT EXISTS persistent_volume_name`;注释块中含 `DROP COLUMN IF EXISTS persistent_volume_name` 。
74- - 测试:`ClaudeAccount` 结构体含 `PersistentVolumeName` 字段且 `json` tag 使用 `omitempty`(若序列化用得到) 。
68+ - migration 使用 `ADD COLUMN IF NOT EXISTS persistent_volume_name TEXT`,不设置空字符串默认值 。
69+ - down 注释包含 `DROP COLUMN IF EXISTS persistent_volume_name`,确保回滚路径明确 。
7570 </behavior >
76- <action >按 D-01/D-02/D-10 编写迁移与模型字段;迁移内不写绝对路径;索引仅在为后续 phase 确有查询模式时再加,本 plan 默认不加(RESEARCH 可选索引)。对应 D-01(列就绪)、D-02、D-10 。</action >
71+ <action >按 D-01/D-02/D-10 新增 migration,并在 ` ClaudeAccount ` 增加可空字段映射;保持向后兼容,不修改任何 HTTP/agent 契约 。</action >
7772 <verify >
7873 <automated>go test ./internal/store/repository/... -count=1 -short</automated>
7974 </verify >
80- <done >迁移与模型字段合并;` migration_0014_test.go ` 绿灯。</done >
75+ <acceptance_criteria>空库与升级库均可执行 migration;模型层可区分 ` NULL ` 与非空 volume 名。</acceptance_criteria>
76+ <done >schema 与模型完成并有自动化测试覆盖。</done >
8177</task >
8278
8379<task type =" auto " tdd =" true " >
84- <name >任务 2:GetHostByShortID 扩展 + ResolveClaudeAccountIDForEntry</name >
85- <files >internal/store/repository/queries.go,internal/store/repository/migration_0014_test.go</files >
80+ <name >Task 2: 实现 D-05 账号选择查询与 template_image_ref 输出</name >
81+ <files >internal/store/repository/queries.go, internal/store/repository/migration_0014_test.go</files >
82+ <read_first>.planning/phases/30-entry-api/30-CONTEXT.md (D-05,D-06,D-11), internal/store/repository/queries.go</read_first>
8683 <behavior >
87- - 测试(表驱动):对 `ResolveClaudeAccountIDForEntry` 使用 **fake DB** 或 **仅测 SQL 常量化片段** 不可行时,改为在 `migration_0014_test.go` 或新建 `resolve_claude_account_test.go` 中:用 `pgxmock` **或** 最小接口 `type rowQuerier interface{ QueryRow(...)}` + fake stub,断言两次查询的先后顺序与 `ok` 语义(无第二命中时 `ok=false`)。若引入 `pgxmock` 需改 `go.mod`,优先用 **手写 stub 实现 `Querier` 子集**(仅 `QueryRow`)避免新依赖(D-11)。
88- - 至少覆盖:仅命中 host 绑定;仅命中 host_id IS NULL;全无。
84+ - host 绑定账号优先:`host_id = ? ORDER BY created_at ASC LIMIT 1`
85+ - fallback 账号:`user_id = ? AND host_id IS NULL ORDER BY created_at ASC LIMIT 1`
86+ - 无命中返回 `ok=false`,不是错误。
8987 </behavior >
90- <action >扩展 ` HostSSHAuth ` 与 ` GetHostByShortID ` (D-06 数据输入);实现 D-05 解析逻辑;错误仅来自 DB 故障,无行不算错误。sshproxy 等调用方忽略新字段不受影响 。</action >
88+ <action >新增 ` ResolveClaudeAccountIDForEntry ` (或等价语义命名),并扩展 ` GetHostByShortID ` 查询输出 ` template_image_ref ` ,为 Wave 2 的能力字段推导提供输入 。</action >
9189 <verify >
9290 <automated>go test ./internal/store/repository/... -count=1 -short</automated>
9391 </verify >
94- <done >仓库编译通过;` ResolveClaudeAccountIDForEntry ` 行为与 D-05 一致且有自动化断言。</done >
92+ <acceptance_criteria>解析顺序与 D-05 完全一致;` template_image_ref ` 可被上层读取且不破坏现有调用。</acceptance_criteria>
93+ <done >仓储层提供稳定 API,Wave 2 不再需要改 SQL。</done >
94+ </task >
95+
96+ <task type =" auto " >
97+ <name >Task 3: 数据层完整性回归(防止与 Wave 2 重叠)</name >
98+ <files >internal/store/repository/migration_0014_test.go</files >
99+ <read_first>.planning/ROADMAP.md (Phase 30 Scope/Success Criteria)</read_first>
100+ <action >补充回归断言:本计划仅覆盖 migration/repository,不引入 ` internal/controlplane/http ` 与 ` internal/cloudclaude ` 变更,确保数据层与 API 层职责边界清晰。</action >
101+ <verify >
102+ <automated>go test ./internal/store/repository/... -count=1</automated>
103+ </verify >
104+ <acceptance_criteria>测试通过且无 HTTP/客户端文件变更,满足“数据层先于 API 层”的波次隔离。</acceptance_criteria>
105+ <done >Wave 1 可独立执行并产出可被 Wave 2 直接消费的数据接口。</done >
95106</task >
96107
97108</tasks >
98109
99110<threat_model>
100- ## 信任边界
111+ ## Trust Boundaries
101112
102- | 边界 | 说明 |
103- | ------| ------|
104- | SQL 迁移 | 以文件形式进入部署管线;错误 DDL 可导致启动失败或数据不可用 |
105- | 仓储查询 | 从控制面进程访问 DB;参数化查询边界防止注入 |
113+ | Boundary | Description |
114+ | ----------| -------------|
115+ | controlplane → postgres | schema 与查询变更直接影响认证链路数据正确性 |
106116
107- ## STRIDE 登记
117+ ## STRIDE Threat Register
108118
109- | 威胁 ID | 类别 | 组件 | 处置 | 缓解措施 |
110- | ---------| ------| ------| ------| ----------|
111- | T-30-01 | T | ` queries.go ` / Resolve | mitigate | 仅使用参数化查询( ` $1 ` / ` $2 ` ),禁止字符串拼接用户输入进 SQL |
112- | T-30-02 | I | ` 0014_*.sql ` | mitigate | 迁移仅 DDL;部署前在 CI 跑 ` go test ` + 代码审阅核对 ` IF NOT EXISTS ` / ` DROP IF EXISTS` |
113- | T-30-03 | E | ` claude_accounts ` 行泄露 | accept | 与既有 Entry 认证同属控制面信任域;本方法不扩大 HTTP 暴露面 |
119+ | Threat ID | Category | Component | Disposition | Mitigation Plan |
120+ | ----------- | ---------- | ----------- | ------------- | ------- ----------|
121+ | T-30-01 | T | repository SQL | mitigate | 全部使用参数化查询,不拼接输入字符串 |
122+ | T-30-02 | D | migration 0014 | mitigate | 使用 IF NOT EXISTS / IF EXISTS 与自动化回归测试 |
123+ | T-30-03 | I | claude_account 解析 | accept | 仅在控制面内部使用,不扩大外部暴露面 |
114124</threat_model>
115125
116126<verification >
117- - ` go test ./internal/store/repository/... -count=1 -short ` 通过。
118- - ` go build ./... ` 通过(可由 CI 覆盖)。
127+ - ` go test ./internal/store/repository/... -count=1 -short `
128+ - ` go test ./internal/store/repository/ ... -count=1 `
119129</verification >
120130
121131<success_criteria>
122- - 迁移文件命名与 D- 10 一致;列语义满足 D-02 。
123- - D-05 在仓储层可测且行为固定 。
132+ - D-01/D-02/D- 10 与 D-05 在数据层全部落地且有自动化断言 。
133+ - Wave 1 输出可直接被 Wave 2 的 Entry API 计划消费,不需要重复迁移或重复查询实现 。
124134</success_criteria>
125135
126136<output >
127- 完成后撰写 ` .planning/phases/30-entry-api/plans/01-migration-entry-store/SUMMARY.md ` (由执行器生成)。
137+ After completion, create ` .planning/phases/30-entry-api/plans/01-migration-entry-store/SUMMARY.md `
128138</output >
129139
130- ## 来源审计(规划闭环)
140+ ## Source Audit
131141
132- | 来源 | 条目 | Plan | 状态 |
133- | ------| ------| ------ | ------|
134- | GOAL | 控制面通道 + 数据模型 | 01 | COVERED |
135- | REQ | REQ-F7-A(命名粒度数据模型) | 01 | COVERED |
136- | RESEARCH | migration / repository / D-05 SQL | 01 | COVERED |
137- | CONTEXT | D-01,D-02,D-05,D-10,D-11,D-12(排除项) | 01 | COVERED |
142+ | Source | Item | Coverage |
143+ | -------- | ------| ----------|
144+ | GOAL | Phase 30 数据模型与握手前置 | COVERED |
145+ | REQ | REQ-F7-A(命名约定的数据模型落地) | COVERED |
146+ | RESEARCH | migration + repository + D-05 查询策略 | COVERED |
147+ | CONTEXT | D-01,D-02,D-05,D-06,D- 10,D-11,D-12 | COVERED |
0 commit comments