Files
yovision/docs/tasks/T-012.md
T
QiuSW 677ed732f7
Harness governance / validate (push) Has been cancelled
chore(task): claim T-012
2026-08-07 22:28:16 +08:00

104 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: T-012
title: 补齐 Sense 对账安全闸、孤儿受控处置与多实例可观测性
phase: 2
deps: [T-011]
status: DOING
created: 2026-08-07
issue: 43
context_ref: 25357723a09218bae58626a9934309abcda34899
claim_branch: claims/T-012
work_branch: agent/codex/T-012
write_paths:
- docs/tasks/T-012.md
- Sense/README.md
- Sense/cmd/sense-api/
- Sense/cmd/sense-orphan/
- Sense/internal/config/
- Sense/internal/metrics/
- Sense/internal/mtx/
- Sense/internal/orphan/
- Sense/internal/reconcile/
- Sense/internal/store/
- deploy/postgres/
- scripts/test_postgres.ps1
- docs/00-ai-start-here.md
- docs/03-tech-stack.md
- docs/04-architecture.md
- docs/06-tasks.md
- docs/api.md
- docs/current-state.md
- docs/runbooks/
- tests/test_postgres_contract.py
- tests/test_sense_reconcile_safety.py
---
## 问题 / 背景
T-003 已实现幂等调和与持久化退避,T-011 已完成 Control API、批量期望态和停用设备的精确 Path 删除,但生产 PostgreSQL 路径仍由每个 Sense 实例独立读取同一批 due row;多实例会重复探测、重复写 MediaMTX,并可能由过期 worker 覆盖新 worker 的完成结果。当前 MediaMTX 客户端也不枚举配置 Path,因此无法区分台账、媒体配置和历史归属之间的孤儿差异。
直接把“MediaMTX 中存在、当前设备表中不存在”的所有 Path 自动删除并不安全:MediaMTX 可能包含人工或其他系统管理的 Path,Sense 也没有历史归属证据。本任务先建立可证明的所有权、只读报告、比例安全闸和受控的一次性处置流程,并提供不含租户/设备高基数标签的 Prometheus 文本指标;它是进入 WireGuard 与 16 路批量基准前的 M2 生产安全基础,不等于容量验收。
## 关联需求与交互(如适用)
- 用户故事:US-001、US-002、US-008、US-009;本任务提供接入运维所需的调和与孤儿诊断,不新增最终 Bell 管理页面。
- 交互清单:IX-003、IX-004、IX-015、IX-016 的后端运维状态基础;HTML 原型不改,正式运维中心 API/UI 留给后续契约任务。
- 相关页面 / 路由:新增低基数 `/metrics` 运维端点和本地 `sense-orphan` 命令;不修改 T-008 冻结的 `/api/v1` Control API。
## 方案
1. 增加 PostgreSQL `010`/`011` migration,把 `sense.schema_migrations` 提升到 v5:`sense.reconcile_state` 增加 owner/token/expiry 租约列;新增 MediaMTX Path 历史归属、孤儿扫描租约、扫描报告/发现项和处置结果表,并补齐 `sense_app` 最小权限、PUBLIC 拒绝与启动前置检查。migration 从现有视频设备脱敏回填 Path 归属,不保存 endpoint、credential 或 source URI。
2. 将调和 repository port 改为“领取 due row → 每项开工前续租 → 持 token 完成/失败”的 fencing 模型。PostgreSQL 使用短事务、`FOR UPDATE SKIP LOCKED` 和随机 claim token;完成、失败和续租必须同时匹配 device、owner、token 且租约未被新实例接管。SQLite 保持单进程开发语义,不宣称跨进程互斥。
3. 每项外部调和使用短于租约的 deadline;批量领取后逐项续租,等待期间已被其他实例接管的项只记录 `lease_lost`,不得执行 MediaMTX/ONVIF 变更。启用设备成功收敛时在同一 PostgreSQL 完成事务中刷新其 Path 历史归属;停用设备的既有精确删除仍不经过孤儿批量逻辑。
4. 扩展 MediaMTX 薄客户端,以官方分页 API 枚举 Path 名称;循环设置页数上限、重复页保护和缺失字段校验,只返回排序去重后的名称,永不返回或记录 source URI。现有 `EnsurePath`/精确 `DeletePath` 语义不变。
5. 新增 PostgreSQL 专用孤儿扫描器并在 `sense-api` 中默认按 60 秒执行只读扫描。单例扫描使用带 fencing token 的数据库租约;一次报告将 MediaMTX 配置分为:仍被设备当前占用、Sense 历史拥有但已失配的 `owned_stale`、从未有 Sense 归属证据的 `unowned`。`unowned` 永远只报告;扫描失败不产生可执行快照。
6. 新增本地运维命令 `sense-orphan`:`report` 只产生脱敏 JSON 摘要和 15 分钟有效的 scan ID;`apply` 必须给出该 scan ID、合法 actor 和精确确认文本。执行前重新枚举 MediaMTX、重新读取所有权并重新计算安全闸,只处理仍为 `owned_stale` 的快照项;每项结果持久化,重复 apply 幂等。
7. 孤儿删除安全闸固定为 `候选数 * 100 <= 当前 MediaMTX 配置 Path 总数 * 10`,并同时要求候选数 1~128、报告未超过 15 分钟、当前总数大于 0、当前候选是报告候选的子集。任何条件失败均整批不删除;不提供 force/bypass。删除逐项执行,部分失败可重试,但一次运行不得扩大目标集合。
8. 新增无第三方依赖的 Prometheus/OpenMetrics 文本 handler,暴露构建/实例信息、调和 run/item/lease-lost/时长/未收敛汇总,以及孤儿扫描 observed/owned-stale/unowned、blocked/deleted/failed 汇总。标签只使用固定结果枚举和经过格式校验的 instance ID,不使用 tenant、Site、device、Path、URI 或错误正文。
9. 更新 Sense 启动与文档:生成进程级 instance ID,也允许 `SENSE_INSTANCE_ID` 显式指定;配置调和租约/操作 deadline、孤儿只读扫描开关/周期和指标开关。默认 SQLite 继续可运行;孤儿持久化/处置只在 PostgreSQL v5 上启用。修正当前状态中“T-011 公共设备 API 尚未实现”的过期描述。
## 不可变约束
- 阈值 / 数值边界:调和租约默认 30 秒,单项 operation deadline 默认 20 秒且必须严格小于租约;调和 batch 最大 128。孤儿扫描默认 60 秒;报告有效期固定 15 分钟;单次处置 1~128 项;删除比例最多 10%,使用整数交叉相乘,禁止浮点边界漂移。
- 判定式 / 状态转换:只有 `media_path_ownership` 有历史记录、且当前没有任何视频设备继续声明同一 `(device_id,path_name)` 的运行时 Path 才是 `owned_stale`。未归属 Path 是 `unowned`,永不由 Sense 删除。过期或失配 fencing token 不得更新调和完成/失败,也不得产生可执行扫描报告。
- 安全边界:默认和周期任务只读扫描,绝不自动 apply;`apply` 必须二次确认并重新取运行时/数据库快照。比例闸、快照时效、上限、所有权或确认任一失败时零删除;不提供环境变量、flag 或隐藏接口绕过。完整 URI、endpoint、credential、profile token、DSN、认证 token 和错误响应正文不得进入指标、日志、报告或审计表。
- 既有契约:不修改 Sense Control OpenAPI v1、设备审计 v1/v2 和 7 个业务 endpoint;停用设备按精确台账 Path 幂等删除的行为保持。`site.max_video_channels` 默认 16、上限 128,不把 16 写成业务或数组硬上限。
- 多实例边界:PostgreSQL v5 才提供调和/孤儿租约语义;SQLite 只保留本地单实例开发,不得通过 SQLite 测试宣称生产多实例安全。租约不是长期锁,所有外部调用必须可取消并受 deadline 限制。
## 验收要点
- 任务相关验证:Go 单元测试覆盖 MediaMTX 多页/重复页/脱敏失败、租约失效时零副作用、两个 PostgreSQL store 并发只领取一次、租约过期接管与旧 token fencing、归属回填、三类孤儿判定、10% 边界、15 分钟过期、二次快照收窄、未知 Path 不删除、部分失败幂等和 metrics 无敏感/高基数标签。
- PostgreSQL 门禁:`./scripts/test_postgres.ps1 -PgRoot D:\pgsql17` 连续重放 `001`~`011`、执行权限/约束断言和全部 `TestPostgres*`;随机回环临时集群自动清理,不读取、停止或修改现有 `D:\pgsql17\data`/5432 实例。
- 完整门禁:`./init.ps1`、三条 Python 治理命令、`go -C Sense test ./...`、`go -C Sense vet ./...`、`go -C Sense build ./...`、`go test -race ./internal/metrics ./internal/mtx ./internal/orphan ./internal/reconcile ./internal/store`、`git diff --check` 全部通过。
- 人工 / 设备验收:不需要摄像头、GPU 或客户现场;使用 fake MediaMTX 与隔离 PostgreSQL 验证。真实五路与容量结论仍由 T-007/后续 16 路基准提供。
- 构建产物:`sense-api` 的 `/metrics`、`sense-orphan` 本地命令、PostgreSQL v5 migration、运维 runbook、测试和同步文档;不生成或提交任何私有配置。
## 边界(不改什么)
不实现 Bell 运维 UI/RBAC/JWT、WireGuard、Outbox relay、设备删除 API、跨 MediaMTX 分片编排、16 路容量压测、自动修复 `unowned` Path 或 T-007 现场验收;不修改 MediaMTX 生成代码、冻结 OpenAPI/审计契约和已确认 Sense/Bell HTML 原型。指标只建立进程内 Prometheus scrape 面,不引入 Grafana dashboard、OpenTelemetry SDK 或新的第三方依赖。
## 协作约束
- 责任 Agent:由 dispatcher 分配。
- 唯一写入者:同责任 Agent。
- 委派:默认不启用。
- Gitea:任务规格先进入默认分支,再创建唯一 Issue 并回填编号;领取时记录 `context_ref`、claim / 工作分支和全部允许写路径。
任何新增写路径先检查与其他活跃任务是否重叠;同一时刻只有一个 Agent 修改本任务的 `write_paths`。T-007 虽处于 waiting,但其未来写路径包含 `Sense/`;T-012 活跃期间不得同时恢复 T-007。
## 执行记录
### 2026-08-07 领取任务
- dispatcher `ila` 将 Issue #43 分配给 `codex`;`context_ref` 为 `25357723a09218bae58626a9934309abcda34899`,claim 为 `claims/T-012`,工作分支为 `agent/codex/T-012`。
- 已读回 Issue `status/doing`、assignee、dispatcher 发布的完整 CLAIM 与两个分支 SHA;接受 frontmatter 全部写路径。T-007 继续 waiting,本任务期间不恢复,当前无活跃写路径冲突。
### 2026-08-07 Gitea 映射
- 任务规格先通过 PR #42 合入默认分支,再创建唯一主 Issue #43;本次只回填双向映射,映射合入并读回前不领取任务。
### 2026-08-07 任务定义
- 项目负责人要求在 T-011 后创建并实施 T-012;当前只有 T-007 因外部真实设备条件处于 waiting,无开放 PR 或活跃 claim。
- 本任务把路线图的“对账器并发/10% 安全闸”和当前状态建议收敛为 PostgreSQL fencing、可证明 Path 所有权、只读孤儿报告、不可绕过的受控处置和低基数指标;不以未知 Path 的存在推断 Sense 有删除权。