diff --git a/.doc/ops/short-term/2026-09-22-t20-design-items.md b/.doc/ops/short-term/2026-09-22-t20-design-items.md index 86c6d574..f8c84326 100644 --- a/.doc/ops/short-term/2026-09-22-t20-design-items.md +++ b/.doc/ops/short-term/2026-09-22-t20-design-items.md @@ -30,11 +30,13 @@ remote_sync: not_requested # T20 设计项决策批(W4) +设计正文(taxonomy:`system-design/`):[../../system-design/2026-09-22-t20-hardening-design.md](../../system-design/2026-09-22-t20-hardening-design.md)。 + ## 用户目标与授权 - 用户目标:对 [audit-remainder T20 增补区](../long-term/2026-09-19-audit-remainder.md) 中需设计的项产出**推荐做法 / 风险 / 验收 / 是否卡产品或鉴权**,非完整实现(除非 trivial 且安全)。 -- 交付终点:本任务文 + [系统设计](../../system-design/2026-09-22-t20-hardening-design.md);本 worktree 提交;不 push;不合入;不改业务代码。 -- 明确禁止:实现 B1/B3/B4/B5/B7/B11/F1/F4/F7/F8/I1/I4/I6;不操作共享环境;不 push。 +- 交付终点:本任务文 + 系统设计决策文;本 worktree 提交;不 push;不合入;不改业务代码。 +- 明确禁止:实现 B1/B3/B4/B5/B7/B11/F1/F4/F7/F8/I1/I4/I6;不操作共享环境;不 push;I6 不写宿主调度。 - I6 仅文档化;实施需环境授权。 ## 范围与非目标 @@ -59,7 +61,7 @@ remote_sync: not_requested | ID | 状态 | 交叉链接 | |----|------|----------| -| **F5** ErrorBoundary 分栏 | 已在 W2 实现(`AppShell` 聊天栏/文档栏各包一层) | 现场:[`.worktrees/t20-lowrisk-hardening-2026-09-22/.../2026-09-22-t20-lowrisk-hardening.md`](../../../../t20-lowrisk-hardening-2026-09-22/.doc/ops/short-term/2026-09-22-t20-lowrisk-hardening.md)(合入后路径改为同目录 `./2026-09-22-t20-lowrisk-hardening.md`) | +| **F5** ErrorBoundary 分栏 | 已在 W2 实现(`AppShell` 聊天栏/文档栏各包一层) | [t20-lowrisk-hardening](./2026-09-22-t20-lowrisk-hardening.md) · worktree `.worktrees/t20-lowrisk-hardening-2026-09-22` | | **I8** short 跳过外网 docker | 已在 W2 实现(`testing.Short()` Skip) | 同上 | 本批**不**再设计或实现 F5/I8。 @@ -68,18 +70,18 @@ remote_sync: not_requested | ID | 推荐 | 卡产品/鉴权? | 实施前置 | |----|------|---------------|----------| -| B1 | 默认忽略 XFF;仅 `TRUSTED_PROXY_CIDRS` 命中 RemoteAddr 时取 XFF 最右/配置跳数 | 否(运维配置) | 配置项 + 负例测试 | +| B1 | 默认忽略 XFF;仅 `TRUSTED_PROXY_CIDRS` 命中 RemoteAddr 时按 hops 解析 | 否(运维配置) | 配置项 + 负例测试 | | B3 | 锁外 hydrate;按 `doc_id` 分锁;DB 写用短临界区 + fencing | 否 | 竞态基准 + 回归 | -| B4 | 主池 `RuntimeParams.statement_timeout` 默认 30s;长任务 `SET LOCAL` 抬高 | 否 | 池工厂统一入口 | +| B4 | 主池默认 `statement_timeout=30s`;长任务 `SET LOCAL` 抬高 | 否 | 池工厂统一入口 | | B5 | **保留** `FailChatTurn` 补偿;补齐可观测与孤儿清理;同事务列为可选后续 | 否 | 指标/扫描即可先落地 | -| B7 | 独立 `SHUTDOWN_DRAIN_TIMEOUT` + compose `stop_grace_period`;停机拒新任务(不必等满 `AGENT_TIMEOUT`) | 否(运维调参) | compose/env 对齐 | -| B11 | 生产仅 POST;GET 仅 debug 且禁 `message` query 或整端点删 | 否 | 前端已 POST;后端收紧 | -| F1 | 所有窗口写路径共用 generation + abort;过期响应禁 `setState`/写缓存 | 否 | 单测扩面 | +| B7 | 独立 `SHUTDOWN_DRAIN`(默认可对齐 `AGENT_TIMEOUT`);停机拒新任务;compose grace 对齐 | 否(运维调参) | compose/env 对齐 | +| B11 | 生产仅 POST;GET 默认 405 且永不读 `message` query | 否 | 前端已 POST;后端收紧 | +| F1 | 所有窗口写路径共用 generation + abort;过期响应禁 setState/写缓存 | 否 | 单测扩面 | | F4 | **隐藏按钮**直至真实 API;或产品选「服务端生成临时口令 + 强制改密」 | **是(产品)** | 产品选路径后再编码 | | F7 | 专项:`Set-Cookie` HttpOnly + SameSite + 全局 401 清会话 | **是(鉴权专项)** | 独立授权/ADR | -| F8 | 统一 in-flight Map 按 key 去重;stats 绑 generation | 否 | 前端小改 | -| I1 | Dockerfile `USER` + compose `mem`/`cpus`;先非 privileged | 部署共享环境需授权 | 卷权限验证 | -| I4 | 三环境统一挂 `extensions_init.sql`;表只走 goose | 改卷/共享环境需授权 | 全新卷冒烟 | +| F8 | 统一 in-flight Map 按 key 去重;stats 绑 generation;blob revoke | 否 | 前端小改 | +| I1 | Dockerfile `USER` + compose mem/cpus;先非 privileged | 否(运维;部署共享环境另授权) | 卷权限验证 | +| I4 | 三环境统一挂 `extensions_init.sql`;表只走 goose | 否 | 全新卷冒烟 | | I6 | systemd/cron 调 `backup-system-db.sh`;RPO 60m | **是(环境授权)** | 仅文档;不实施 | 详细论证见 [2026-09-22-t20-hardening-design.md](../../system-design/2026-09-22-t20-hardening-design.md)。 @@ -88,17 +90,18 @@ remote_sync: not_requested 1. [x] 核对代码与 audit-remainder 条目 2. [x] 交叉确认 F5/I8 在 lowrisk worktree -3. [x] 写入 short-term + system-design +3. [x] 写入 short-term + system-design;INDEX/README 回链 4. [x] Conventional Commit(不 push) | 范围 | 命令 | 预期 | 实际 | |---|---|---|---| -| 文档 diff | 本仓库两文 + 索引链接 | 可审、路径合 taxonomy | 本提交 | +| 文档 | short-term + system-design + 索引 | 可审、taxonomy 正确;无业务行为变更 | 本分支提交 | ## 收口与交接 - `review_state=pending`;`integration_state=pending`;`deployment=not_required`;`remote_sync=not_requested`。 -- 实施跟进建议:可并行小改(B1/B11/F1/F8/I4)与需决策项(F4/F7/I6)拆批;B3/B4/B7/I1 需回归与环境冒烟。 +- 实施跟进:可并行小改(B1/B11/F1/F8、F4-A 隐藏按钮)与需决策项(F4-B/F7/I6)拆批;B3/B4/B7/I1/I4 需回归与环境冒烟。 +- **待用户决策**:F4 选隐藏 vs 真实临时口令 API;F7 鉴权专项授权;I6 目标环境与备份路径授权。 - cleanup:合入后由独立维护者复查 remove。 ## 相关链接 @@ -106,4 +109,4 @@ remote_sync: not_requested - 清单:[../long-term/2026-09-19-audit-remainder.md](../long-term/2026-09-19-audit-remainder.md) - 审计:[../../audit/2026-09-20-system-triage-audit.md](../../audit/2026-09-20-system-triage-audit.md) - 设计正文:[../../system-design/2026-09-22-t20-hardening-design.md](../../system-design/2026-09-22-t20-hardening-design.md) -- 低风险批(F5/I8):[sibling worktree 任务文](../../../../t20-lowrisk-hardening-2026-09-22/.doc/ops/short-term/2026-09-22-t20-lowrisk-hardening.md)(合入后改为 `./2026-09-22-t20-lowrisk-hardening.md`) +- 低风险批(F5/I8):[./2026-09-22-t20-lowrisk-hardening.md](./2026-09-22-t20-lowrisk-hardening.md)(合入前见 worktree `t20-lowrisk-hardening-2026-09-22`) diff --git a/.doc/remediation/INDEX.md b/.doc/remediation/INDEX.md index ca0532f6..8bbbecfc 100644 --- a/.doc/remediation/INDEX.md +++ b/.doc/remediation/INDEX.md @@ -2,7 +2,7 @@ | 文档 | 说明 | |------|------| -| [../system-design/2026-09-22-t20-hardening-design.md](../system-design/2026-09-22-t20-hardening-design.md) | T20 W4 加固设计决策(XFF、锁拆分、timeout、shutdown、SSE POST、前端 generation/cookie、compose non-root/init、backup 调度文档);F5/I8 见 lowrisk 批。配套 [任务](../ops/short-term/2026-09-22-t20-design-items.md) | +| [2026-09-22-t20-hardening-design.md](./2026-09-22-t20-hardening-design.md) | T20 W4 加固设计决策(XFF、锁拆分、timeout、shutdown、SSE POST、前端 generation/cookie、compose non-root/init、backup 调度文档);F5/I8 见 lowrisk 批。配套 [任务](../ops/short-term/2026-09-22-t20-design-items.md) | | [../system-design/2026-09-20-stage-backtrack-and-impact-loop-design.md](../system-design/2026-09-20-stage-backtrack-and-impact-loop-design.md) | 阶段回溯与影响闭环:L1 修订 / L2 再打开、待复核软标记、对话提案桥。追踪留存定为 `record`;候选边由下游编制人裁决。配套 short-term 切片均为 planned | | [2026-09-20-module-decoupling-batch-plan.md](./2026-09-20-module-decoupling-batch-plan.md) | 耦合残留分批 A–G(server 装配、逻辑环、docgov 词汇、工作流依赖、巨型文件、前端 client、例外清零);保持 planned,不改代码。配套 [审计](../audit/2026-09-20-module-coupling-audit.md) · [规范](../system-design/2026-09-20-module-development-standards.md) | | [2026-09-20-p0-remediation-plan.md](./2026-09-20-p0-remediation-plan.md) | 系统体检 P0:迁移 00058、三项 IDOR、panic/race/XSS、凭证与口令轮换;配套 [审计](../audit/2026-09-20-system-triage-audit.md) | diff --git a/.doc/system-design/2026-09-22-t20-hardening-design.md b/.doc/system-design/2026-09-22-t20-hardening-design.md index 2b55ce6b..afb15e68 100644 --- a/.doc/system-design/2026-09-22-t20-hardening-design.md +++ b/.doc/system-design/2026-09-22-t20-hardening-design.md @@ -1,529 +1,14 @@ --- -title: "T20 加固设计决策(W4)" -status: proposed +title: "T20 加固设计决策(W4)— 迁址说明" +status: redirected created: 2026-09-22 updated: 2026-09-22 -owner: Cursor -base_dev_sha: "364ed7f1a3f0393ef77ec4623a9518116ed2cb87" -source_audit: "../audit/2026-09-20-system-triage-audit.md" -source_backlog: "../ops/long-term/2026-09-19-audit-remainder.md" -task: "../ops/short-term/2026-09-22-t20-design-items.md" --- -# T20 加固设计决策(W4) +# 迁址 -> 本文件是**设计决策**,不是实施计划全文。实现另开 short-term;本批不改业务代码。 -> 低风险已实现项(F5/I8)见 sibling worktree [t20-lowrisk 任务文](../../../t20-lowrisk-hardening-2026-09-22/.doc/ops/short-term/2026-09-22-t20-lowrisk-hardening.md)(合入后改为 [ops/short-term/...](../ops/short-term/2026-09-22-t20-lowrisk-hardening.md))。**本文不重复设计 F5/I8。** +本决策正文已迁至整改方案目录(符合 W4「short-term / remediation」落盘要求): -核对基线:`feature/t20-design-items-2026-09-22` @ `364ed7f1`。 +**[../remediation/2026-09-22-t20-hardening-design.md](../remediation/2026-09-22-t20-hardening-design.md)** ---- - -## 总表 - -| ID | 推荐摘要 | 卡产品/鉴权/环境? | 建议优先级 | -|----|----------|-------------------|------------| -| B1 | 默忽视 XFF;仅可信代理 CIDR 命中时解析 | 否(运维 CIDR) | P1 | -| B3 | 按 docID 分锁;hydrate/DB I/O 出全局锁 | 否 | P1(独立大改) | -| B4 | 主库池级 `statement_timeout` 分级 | 否 | P1 | -| B5 | **保留** `FailChatTurn` 补偿;补可观测;同事务可选 | 否 | P2 | -| B7 | drain 超时与 `AGENT_TIMEOUT` / compose grace 对齐 | 否(运维调参) | P1 | -| B11 | 生产禁 GET 传 `message`;保留 POST body | 否 | P1(小) | -| F1 | 所有窗口写路径统一 generation + Abort | 否 | P1 | -| F4 | 先隐藏死按钮;完整能力需产品选型 | **是(产品)** | 决策后 | -| F7 | 先全局 401;HttpOnly cookie 独立 epic | **是(鉴权专项)** | Cookie=P2 | -| F8 | poll/stats 去重 + generation;blob revoke | 否 | P1 | -| I1 | 非 root USER + mem/cpu limits | 部署需环境授权 | 设计可合 | -| I4 | 三环境统一 `extensions_init.sql` | 改卷需环境授权 | 设计可合 | -| I6 | systemd/cron 调备份脚本,RPO≤60m | **是(环境写)** | **仅文档** | - ---- - -## B1 — XFF 可信代理 - -### 现状 - -`internal/server/login_rate_limit.go` 的 `clientIP` **无条件**取 `X-Forwarded-For` 首跳,否则 `RemoteAddr`。攻击者可伪造 XFF,使登录限流桶分散,绕过爆破防护。 - -### 推荐做法 - -1. 新增配置 `TRUSTED_PROXY_CIDRS`(逗号分隔 CIDR;空=不信任任何代理)。 -2. 仅当 `RemoteAddr` 落入可信 CIDR 时解析转发头: - - 取 XFF 链中**最右非可信**地址(或配置 `TRUSTED_PROXY_HOPS=N` 剥掉右侧 N 跳)。 - - 否则一律用 `RemoteAddr`(剥端口)。 -3. 可选:同样规则用于审计日志 IP;登录限流 key 继续 `ip|username`。 -4. 默认生产 compose **不**填 CIDR(直连或仅 docker 内网时安全);有 nginx/traefik 时由运维填代理网段。 - -### 风险 - -- 配错 CIDR → 全站限流落到代理 IP,正当用户互锁。缓解:启动日志打印有效 CIDR;监控 `login_rate_limited` 按 raw RemoteAddr。 -- 多级代理跳数错误 → IP 仍可伪造。缓解:文档要求「只信直连上一跳」;优先 CIDR 白名单而非盲目 `HOPS`。 - -### 验收 - -- 单元:伪造 XFF + RemoteAddr∉CIDR → key 用 RemoteAddr;∈CIDR → 用解析后的客户端 IP。 -- 负例:空 CIDR 时带任意 XFF 不得改变限流 key。 -- 不要求浏览器 E2E。 - -### 是否卡产品/鉴权 - -否。运维配置即可。 - ---- - -## B3 — docgov / docrevision 锁拆分 - -### 现状 - -- `docrevision.Service`:进程级 `sync.Mutex`;`hydrateDocLocked` 在持锁时打 PG。 -- `docgov.GovernanceEngine`:`RWMutex`;`AcquireLease` 等路径在锁内 `getDoc` / `persistLease`(含 DB)。 - -结果:单文档慢查询阻塞**全部**文档的修订/租约路径。 - -### 推荐做法(两阶段) - -**阶段 A(优先,行为不变)** - -1. **锁外 hydrate**:先在无锁或 doc 级锁下从 PG 加载到临时结构,再短临界区合并进内存 map(CAS / version 校验)。 -2. **按 `doc_id` 分锁**:`map[string]*sync.Mutex` + 全局 `RWMutex` 仅保护「分锁表」本身;文档间并行。 -3. DB 写:短临界区准备参数 → 锁外执行 SQL → 短临界区提交内存;失败回滚内存镜像。 -4. 租约继续依赖既有 fencing token / TTL;不引入新分布式锁。 - -**阶段 B(可选)** - -- 热路径逐步改为「PG 为 SSOT、内存为缓存」,减少 hydrate 频率;需单独容量评估,不与 A 绑死。 - -### 风险 - -- 分锁 + 锁外 I/O 易引入 TOCTOU;必须用 fencing / 乐观版本。 -- 死锁:统一锁序 `globalMeta → docID`(docID 字典序若需多文档)。 -- 回归面:`document_commit_concurrency_pg_test`、lease 单测、修订 idempotent 路径。 - -### 验收 - -- 基准:两文档并行 hydrate/commit,全局锁持有时间不覆盖对方 DB RTT(可用 race + 计时断言或日志采样)。 -- 既有 PG 并发提交 / lease busy 负例全绿。 -- `go test -race` 覆盖 docrevision + docgov 包。 - -### 是否卡产品/鉴权 - -否。工程改造。 - ---- - -## B4 — `statement_timeout` 分级 - -### 现状 - -- 主库:`bizdev.NewService` 用 `pgxpool.New(dsn)`,**无** `statement_timeout` / MaxConns 调优。 -- KB:`kb.OpenPool` 仅设 `MaxConns=8`,仍无 timeout。 - -长查询可占满连接,拖垮健康检查与短 API。 - -### 推荐做法 - -1. 抽出共享 `db.OpenAppPool(ctx, dsn, PoolProfile)`(或扩现有 `internal/db`): - - **default**:`statement_timeout=30s`,`MaxConns` 按环境(建议 dev 8 / test 16 / main 可配)。 - - **migrate**:启动迁移会话显式抬高或 `0`(仅迁移连接)。 - - **long**:PI/索引/报表等路径在事务内 `SET LOCAL statement_timeout = '15min'`(或绑定 `AGENT_TIMEOUT`)。 -2. 禁止在业务 SQL 字符串里散落裸 timeout;分级只通过池默认 + `SET LOCAL`。 -3. 超时映射为 typed 错误信封(503/504 或既有 code),不把 PG 原文回前端。 - -### 风险 - -- 误伤合法长事务(修订大文档、embedding 批)。缓解:long profile + 白名单调用点。 -- 与上下文 `context.WithTimeout` 双重取消;以更严者为准,日志区分来源。 - -### 验收 - -- 单元/集成:池 `RuntimeParams` 含 `statement_timeout`;人为 `pg_sleep` 超过默认 → 连接释放且 API 失败可控。 -- 迁移路径仍能跑完整 goose 链。 -- 健康检查不因无关慢查询长期饿死(抽样)。 - -### 是否卡产品/鉴权 - -否。 - ---- - -## B5 — chat 消息与 turn 原子性 - -### 现状 - -- `chat_turns` 与 `chat_messages` **非**同一 SQL 事务写入。 -- **补偿已存在**:`session.sendMessageInternal` 在 `clientTurnID != ""` 时 `defer FailChatTurn`;用户消息落库失败显式 `FailChatTurn`;成功路径 `CompleteChatTurn`(见 `internal/chat/session.go`)。 -- `AcquireChatTurn` 提供租约/幂等;过期 processing 可打 failed。 - -因此「完全无补偿」的审计表述已过时;剩余是**窗口期孤儿**与可观测性,不是从零设计补偿。 - -### 推荐做法 - -1. **本阶段不强制同事务**(改动面大,且消息与 turn 可能跨连接)。 -2. 加固现有补偿: - - `FailChatTurn` / `CompleteChatTurn` 错误打结构化日志 + 计数(勿静默 `_ =`)。 - - 周期/启动扫描:`status=processing AND lease_expires_at < now()` → failed(已有单条路径,升为批量)。 - - 可选:用户消息已写、turn 未绑定 `user_message_id` 的孤儿告警。 -3. **可选后续(P2)**:同一 `pgx.Tx` 内 `INSERT message` + `UPDATE turn`;失败整回滚。仅在确认补偿指标仍噪声大时做。 - -### 风险 - -- 同事务可能拉长锁时间、与 stream 生命周期纠缠。 -- 过度扫描压力;限制 batch size 与频率。 - -### 验收 - -- 文档/测试明确:persist 失败 ⇒ turn=failed;完成 ⇒ completed + assistant id。 -- 注入 fail 点:用户消息成功后进程崩溃 → 租约过期后 turn 终态可查,可重放同 `client_turn_id` 不双写助手(幂等语义保持)。 -- **不**把「缺同事务」单独列为发布 blocker。 - -### 是否卡产品/鉴权 - -否。 - ---- - -## B7 — graceful shutdown vs `AGENT_TIMEOUT` - -### 现状 - -- `cmd/server/main.go`:`Shutdown` 固定 **5s**。 -- Compose / `.env`:`AGENT_TIMEOUT` 默认 **30m**(示例可达 60m)。 -- Compose **无** `stop_grace_period` → Docker 默认 ~10s 后 SIGKILL。 - -结果:滚动重启时长任务被砍;in-flight SSE/PI 无法 drain。 - -### 推荐做法 - -1. 引入 `SHUTDOWN_DRAIN_TIMEOUT`(默认 `min(AGENT_TIMEOUT, 35m)` 或显式 31m),`main` 用其做 `Shutdown` context。 -2. Shutdown 顺序: - - 停止接受新 HTTP(`http.Server.Shutdown`); - - 标记「拒新 Agent/taskrun」(若尚无 gate,加 `atomic` 或复用 ready); - - 等待 in-flight 结束或 drain 超时; - - 再关池 / worker。 -3. 三环境 compose:`stop_grace_period` ≥ `SHUTDOWN_DRAIN_TIMEOUT`(建议 +30s 缓冲)。 -4. **不**要求 drain 等满整个 AGENT_TIMEOUT 才退出运维窗口:可由运维设更短 drain,并接受超时强杀;但 5s 与 30m 的错配必须消除。 - -### 风险 - -- 过长 grace 拖慢部署。缓解:test/dev 可较短;main 对齐真实 Agent 上限。 -- 任务被拒新后客户端重试风暴。缓解:503 + Retry-After;前端识别。 - -### 验收 - -- 配置单测:env 解析。 -- 手册/注释:compose grace ≥ drain。 -- 手工(授权环境):起长 sleep handler 或短 Agent,SIGTERM 后观察是否在 grace 内结束或明确超时日志(实施时做)。 - -### 是否卡产品/鉴权 - -否。运维调参。 - ---- - -## B11 — GET→POST EventSource(消息体) - -### 现状 - -- `handleChatStream`:POST JSON **与** GET query(含 **`message`**)并存,注释为 EventSource/调试。 -- 生产前端 `web/src/api/chatStream.ts` **已用 POST + body**,不依赖 EventSource GET。 - -GET 将用户正文写入 URL → 代理/访问日志泄露;长度受限。 - -### 推荐做法 - -1. **生产路径**:仅允许 `POST /api/v1/chat/stream` 带 JSON body(维持现状前端)。 -2. GET: - - **首选**:直接拒绝 GET(405),或仅当 `APP_ENV=dev` 且 `CHAT_STREAM_GET_DEBUG=1` 允许,且**禁止** `message` query(只允许已有 run 订阅类参数,若有)。 - - 过渡:GET 若带非空 `message` → 400,文案指向 POST。 -3. 文档/OpenAPI 删除「EventSource 传消息」示例。 - -### 风险 - -- 外部脚本仍用 GET。缓解:变更说明;405 明确。 -- 无。前端已 POST。 - -### 验收 - -- 测试:GET+`message` → 4xx;POST 同行仍 200 流式(现有测即可)。 -- `rg 'EventSource|/chat/stream.*GET' web/` 无生产依赖。 - -### 是否卡产品/鉴权 - -否。 - ---- - -## F1 — `useChatWindow` generation 所有权 - -### 现状 - -- `loadInitialWindow` 已用 `generationRef` + `AbortController`。 -- `loadOlder` / `loadFullDay` / 锚点加载等**未**绑定同一 generation:切会话后晚到响应仍可能 `setDayStack` / 写 `saveSessionWindowCache`。 -- 已有 `useChatWindow.race.test.ts` 覆盖「主窗口」模式,未覆盖 loadOlder。 - -### 推荐做法 - -1. 约定:**凡会写 `dayStack` / 缓存 / phase 的异步函数**必须: - - 入口捕获 `const gen = generationRef.current`(或 `++` 仅由 session effect 负责); - - `fetch` 传 `AbortSignal`; - - resolve 后若 `gen !== generationRef.current` 则 return,禁止 setState/写缓存。 -2. `sessionId` 变化时现有 effect 已 `abort` + `generation++`;保持单一所有者(该 effect),子函数只读。 -3. 扩展单测:切会话后 resolve 的 loadOlder 不得污染新会话缓存。 - -### 风险 - -- 漏改某一路径。缓解:checklist + 测试表驱动。 -- abort 触发 `AbortError` 日志噪音;吞掉或降级 debug。 - -### 验收 - -- 单测:A→B 切换后 A 的 loadOlder resolve 不改变 B 的 stack/cache。 -- 手工:快速连点会话,历史不串台。 - -### 是否卡产品/鉴权 - -否。 - ---- - -## F4 — `adminResetPassword` 产品选择 - -### 现状 - -- UI:`UsersSection` 硬编码临时口令 `ResetPass123`,调用 `authClient.adminResetPassword`。 -- API 客户端:直接 `throw new Error('请通过后端管理员接口重置密码')` —— **死功能**。 -- 后端:有 `POST /api/v1/auth/change-password`(自助),**无**管理员重置端点;创建用户已支持初始口令 + `must_change_password`。 - -### 产品选项(择一) - -| 选项 | 做法 | 安全 | -|------|------|------| -| **A. 隐藏/禁用(推荐短期)** | 去掉或 disable「重置密码」按钮;文案引导「停用 + 新建」或工单 | 无假成功 | -| **B. 服务端生成临时口令** | `POST /api/v1/admin/users/{id}/reset-password`:服务端随机口令、哈希入库、`must_change_password=true`、审计;响应**一次性**明文仅回管理员 | 禁硬编码;禁日志打口令 | -| **C. 仅发重置链** | 邮件/线下渠道发 token | 需通知系统;当前栈可能不具备 | - -**推荐**:先 **A**(trivial、可立即做);产品确认后做 **B**(对齐创建用户语义)。禁止继续展示会失败却暗示成功的按钮;禁止客户端固定 `ResetPass123`。 - -### 风险 - -- B 的明文回传需 HTTPS + UI 一次性展示;截图泄露。 -- 管理员重置自己账户的策略需产品定(禁止或二次确认)。 - -### 验收 - -- A:UI 无可用重置按钮或明确「未开通」;无硬编码口令字符串。 -- B:API 鉴权 admin-only;负例非管理员 403;用户登录后强制改密;审计有 actor。 - -### 是否卡产品/鉴权 - -**是(产品)**。A 可不经产品;B/C 需产品签字。 - ---- - -## F7 — HttpOnly cookie - -### 现状 - -- 会话 token 在 `localStorage`(`smdf_auth_session`);请求头 `Authorization: Bearer`。 -- XSS 可窃 token;无统一「任意 API 401 → 清会话回登录」网关(多处各自处理)。 - -### 推荐做法(分阶段) - -**阶段 0(可先做,不改 cookie)** - -- 集中 `apiFetch`:401 → `clearSession` + 跳转登录;避免半登录态。 - -**阶段 1(独立鉴权 epic / ADR)** - -1. 登录响应 `Set-Cookie`:`HttpOnly; Secure; SameSite=Lax`(跨站场景再评 `Strict`/`None`)。 -2. 服务端读 cookie 建会话;逐步废弃 localStorage token(兼容双读一期)。 -3. 若 cookie 会话:CSRF 策略(SameSite + 写操作 CSRF token 或自定义头)。 -4. SSE/`fetch` stream 带 `credentials: 'include'`。 - -**不在本设计批实现阶段 1。** - -### 风险 - -- CSRF、多标签登出、移动/非浏览器客户端。 -- 与现有 Bearer 测试夹具大面积改动。 - -### 验收 - -- 阶段 0:任意受保护 API 401 后 UI 回登录且 storage 清空(单测/组件测)。 -- 阶段 1:ADR + 安全负例(JS 读不到 cookie;无 token 的 XSS 载荷不能复用会话)。 - -### 是否卡产品/鉴权 - -**是(鉴权专项)**。阶段 0 否;阶段 1 需独立授权。 - ---- - -## F8 — poll 去重 + App.tsx stats generation - -### 现状 - -- `App.tsx`:`recoverableRunIds` 每 3s `Promise.all(getTaskRun)`;依赖 `recoverableRunKey`,但**无 in-flight 合并**——慢请求可重叠。 -- 同文件:`fetchChatCategories` / `fetchChatDates` 随 `activeSessionId` 触发,**无 generation**——快切会话可后写先到。 -- 多处 `URL.createObjectURL`;部分路径有 `revokeObjectURL`,需审计遗漏。 - -### 推荐做法 - -1. **Run 轮询**:模块级或 hook 内 `Map`;同 runId 共用飞行中请求;终态入 `skipped` 后不再请求(已有 403/404 skip,扩展终态)。 -2. **Stats**:`statsGenRef`;session 变更 `++`;resolve 时比对再 `setCategoryStats`/`setDateStats`。 -3. **Blob**:导出/预览统一 `try/finally revoke`;React 卸载时 revoke。 -4. 优先抽 hook(`useRecoverableRunPoll` / `useSessionChatStats`),减轻 `App.tsx` 继续膨胀(与既有拆分方向一致)。 - -### 风险 - -- 去重导致状态稍旧一拍;可接受(3s 轮询)。 -- 漏 revoke → 内存涨;用测试断言调用次数。 - -### 验收 - -- 单测:快切会话时旧 stats 不落地;并行 poll 同 runId 只 1 个 in-flight。 -- 手工:多 streaming 卡片时网络面板无指数级 getTaskRun。 - -### 是否卡产品/鉴权 - -否。 - ---- - -## I1 — 非 root + 资源限制 - -### 现状 - -- `deploy/app/Dockerfile`:最终镜像以 root 跑 `/app/server`;无 `USER`。 -- Compose:无 `mem_limit` / `cpus` / `deploy.resources`。 - -### 推荐做法 - -1. Dockerfile:创建 `appuser`(固定 uid/gid,如 10001);`chown` 可写目录(`DATA_DIR`);`USER appuser`。 -2. 确认挂载卷权限:宿主机 `data/*` 对 uid 可写;文档写明。 -3. Compose(dev/test/main 的 app + events):设 `mem_limit`/`mem_reservation`、`cpus`(数值按主机容量,main 更严);避免无上限 OOM 连坐。 -4. 保持 `cap_drop: [ALL]` 等为后续可选项;本设计最低线是非 root + 内存上限。 - -### 风险 - -- 卷权限导致写入失败。缓解:先在隔离 compose 冒烟。 -- 限制过紧 → PI 侧杀进程。缓解:分服务限额(runner 另算)。 - -### 验收 - -- 镜像 `docker run` 内 `id` 非 root。 -- 健康检查与一次写 data 冒烟 PASS。 -- compose config 含 resources。 - -### 是否卡产品/鉴权 - -设计与 Dockerfile 改动不卡产品;**部署到共享 test/main 需环境授权**。 - ---- - -## I4 — init mount 统一 - -### 现状 - -| 环境 | init 挂载 | -|------|-----------| -| dev | `context_init.sql`(扩展+大量业务 DDL) | -| test | `extensions_init.sql`(仅扩展) | -| main | `goose/00001_baseline.sql` | -| shared KB | `kb_init.sql`(保留,不在本项) | - -全新卷行为三套不一致;dev 易与 goose 双源漂移。 - -### 推荐做法 - -1. **业务库**(dev/test/main):一律挂 `deploy/postgres/extensions_init.sql` → `/docker-entrypoint-initdb.d/01-extensions.sql`。 -2. **表结构只走 goose**;废弃业务库对 `context_init.sql` / baseline 文件作为入口(文件可留在 archive 或注释「勿挂载」)。 -3. 文档:空卷启动顺序 = Postgres init 扩展 → app 迁移。 -4. 已有数据卷**不**因改挂载自动重建;仅影响新卷。 - -### 风险 - -- 有人依赖 context_init「一键空库有表」而无 app 迁移。缓解:操作规范写明必须跑迁移。 -- main 从 baseline 文件习惯变更;晋升/灾备手册更新。 - -### 验收 - -- 三环境 compose diff:业务 PG 的 init 路径相同。 -- 新卷:仅扩展存在 → 起 app → goose 到当前 schema → health schema 一致。 - -### 是否卡产品/鉴权 - -设计否;**重建卷/改共享环境需授权**。 - ---- - -## I6 — 备份调度(仅文档) - -> **本项不实施。** 在宿主机安装 systemd/cron 属于环境写,需单独授权。 - -### 现状 - -- 脚本:`scripts/backup-system-db.sh ` + restore drill。 -- 无仓库内定时器;RPO 依赖人工。 - -### 推荐做法(授权后由运维执行) - -1. **调度**:每环境独立 timer(建议 main 每 60m;test 每 6h;dev 每日或关)。 -2. **单元示例**(示意,勿把 secret 写入仓库): - -```ini -# /etc/systemd/system/agent-system-db-backup-main.timer -[Timer] -OnCalendar=hourly -Persistent=true - -# service ExecStart= -# /path/to/repo/scripts/backup-system-db.sh main /var/backups/agent-system/main -``` - -或 cron:`0 * * * *` 调同一脚本。 - -3. **保留**:本地保留 ≥24 份小时备份或 7 日;远端拷贝另策。 -4. **口令**:`${HOME}/.secrets/backup-env` 或环境文件;禁止进 git。 -5. **验收(授权实施后)**:连续 2 个周期出现新 dump + manifest;`restore-system-db-drill` 在隔离目录 PASS;RPO 墙钟 ≤60m(main)。 - -### 风险 - -- 备份含 chat 明文 → 目录 ACL / 加密。 -- 与运行库抢 IO:错峰或 `nice`/`ionice`。 - -### 是否卡产品/鉴权 - -**是(环境授权)**。本文只登记方案;实施另开 operations 任务。 - ---- - -## 明确不在本文范围 - -| ID | 说明 | -|----|------| -| F5 | ErrorBoundary 栏级 — W2 lowrisk 已实现;见该任务文 | -| I8 | `testing.Short()` 跳过 docker build — W2 lowrisk 已实现 | -| B2/B6/B8/B9/B10, F2/F3/F6, I2/I3/I5 | W2 低风险批 | - ---- - -## 实施拆批建议(供后续认领) - -| 批 | 项 | 备注 | -|----|----|------| -| S1 小改 | B11, F1, F8, F4-A | 可无环境写 | -| S2 配置/池 | B1, B4, B7 | 需回归 | -| S3 锁 | B3 | 独立 mutex | -| S4 镜像/compose | I1, I4 | 部署另授权 | -| S5 决策后 | F4-B, F7-1 | 产品/鉴权 | -| S6 运维 | I6 | 仅授权后 | -| 观测 | B5 | 可不阻塞 S1 | - ---- - -## 相关链接 - -- 任务:[../ops/short-term/2026-09-22-t20-design-items.md](../ops/short-term/2026-09-22-t20-design-items.md) -- 清单:[../ops/long-term/2026-09-19-audit-remainder.md](../ops/long-term/2026-09-19-audit-remainder.md) -- 审计:[../audit/2026-09-20-system-triage-audit.md](../audit/2026-09-20-system-triage-audit.md) -- 低风险批:[sibling worktree](../../../t20-lowrisk-hardening-2026-09-22/.doc/ops/short-term/2026-09-22-t20-lowrisk-hardening.md) +配套任务:[../ops/short-term/2026-09-22-t20-design-items.md](../ops/short-term/2026-09-22-t20-design-items.md) diff --git a/.doc/system-design/README.md b/.doc/system-design/README.md index 920d45fe..b91a5fbe 100644 --- a/.doc/system-design/README.md +++ b/.doc/system-design/README.md @@ -11,7 +11,8 @@ | 项 | 链接 | |-----|------| -| 主报告 | [2026-09-22-t20-hardening-design.md](2026-09-22-t20-hardening-design.md) | +| 主报告(SSOT) | [../remediation/2026-09-22-t20-hardening-design.md](../remediation/2026-09-22-t20-hardening-design.md) | +| 本目录迁址桩 | [2026-09-22-t20-hardening-design.md](2026-09-22-t20-hardening-design.md) | | 任务 | [../ops/short-term/2026-09-22-t20-design-items.md](../ops/short-term/2026-09-22-t20-design-items.md) | | 一句话总结 | XFF/锁拆分/statement_timeout/turn 补偿/shutdown/SSE POST/前端 generation/密码与 Cookie/容器与备份等设计决策;F5/I8 见 lowrisk 批 | @@ -237,8 +238,7 @@ ├── 2026-09-14-module-decoupling-architecture.md ← 模块化单体解耦 ├── 2026-09-20-module-development-standards.md ← 开发者入口:契约 / 接口 / 故障隔离 ├── 2026-09-20-stage-backtrack-and-impact-loop-design.md ← 阶段回溯与影响闭环 -├── 2026-09-22-t20-hardening-design.md ← T20 W4 加固设计决策(非实现) -├── 2026-09-22-t20-hardening-design.md ← T20 加固设计决策(W4;非实施) +├── 2026-09-22-t20-hardening-design.md ← 迁址桩 → remediation T20 设计 SSOT .tmp/ ← 中间分析文件 ├── intent-recognition/ @@ -263,7 +263,7 @@ | 文档 | 说明 | |------|------| -| [2026-09-22-t20-hardening-design.md](./2026-09-22-t20-hardening-design.md) | T20 W4 设计决策(XFF/锁/timeout/shutdown/SSE/前端竞态/cookie/容器/备份调度);配套 [任务](../ops/short-term/2026-09-22-t20-design-items.md) | +| [../remediation/2026-09-22-t20-hardening-design.md](../remediation/2026-09-22-t20-hardening-design.md) | T20 W4 设计决策 SSOT(本目录仅留迁址桩);配套 [任务](../ops/short-term/2026-09-22-t20-design-items.md) | ## 🔗 相关资源