[T-012] 冻结对账安全闸与多实例可观测性规格 #42
@@ -0,0 +1,94 @@
|
||||
---
|
||||
id: T-012
|
||||
title: 补齐 Sense 对账安全闸、孤儿受控处置与多实例可观测性
|
||||
phase: 2
|
||||
deps: [T-011]
|
||||
status: TODO
|
||||
created: 2026-08-07
|
||||
issue: null
|
||||
context_ref: null
|
||||
claim_branch: null
|
||||
work_branch: null
|
||||
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 任务定义
|
||||
|
||||
- 项目负责人要求在 T-011 后创建并实施 T-012;当前只有 T-007 因外部真实设备条件处于 waiting,无开放 PR 或活跃 claim。
|
||||
- 本任务把路线图的“对账器并发/10% 安全闸”和当前状态建议收敛为 PostgreSQL fencing、可证明 Path 所有权、只读孤儿报告、不可绕过的受控处置和低基数指标;不以未知 Path 的存在推断 Sense 有删除权。
|
||||
Reference in New Issue
Block a user