docs(ops): T20 W4 hardening design decisions
Record recommended approaches, risks, acceptance, and product/env blockers for audit-remainder design items; cross-link F5/I8 to lowrisk.
This commit is contained in:
@@ -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`)
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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, Promise>`;同 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 <dev|test|main> <dir>` + 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)
|
||||
|
||||
@@ -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) |
|
||||
|
||||
## 🔗 相关资源
|
||||
|
||||
|
||||
Reference in New Issue
Block a user