diff --git a/docs/tasks/T-011.md b/docs/tasks/T-011.md new file mode 100644 index 0000000..993dd4c --- /dev/null +++ b/docs/tasks/T-011.md @@ -0,0 +1,102 @@ +--- +id: T-011 +title: 实现 Sense Control API v1 与 PostgreSQL 一致性边界 +phase: 2 +deps: [T-010] +status: TODO +created: 2026-08-07 +issue: null +context_ref: null +claim_branch: null +work_branch: null +write_paths: + - docs/tasks/T-011.md + - docs/contracts/ + - deploy/postgres/ + - scripts/test_postgres.ps1 + - init.ps1 + - init.sh + - Sense/api/ + - Sense/cmd/sense-api/ + - Sense/internal/auth/ + - Sense/internal/config/ + - Sense/internal/controlapi/ + - Sense/internal/device/ + - Sense/internal/mtx/ + - Sense/internal/reconcile/ + - Sense/internal/store/ + - Sense/README.md + - docs/03-tech-stack.md + - docs/04-architecture.md + - docs/06-tasks.md + - docs/api.md + - docs/current-state.md + - tests/test_postgres_contract.py + - tests/test_sense_control_implementation.py +--- + +## 问题 / 背景 + +T-008 已冻结 Sense Control API v1 的 7 个 endpoint,T-009/T-010 已提供 PostgreSQL Site 配额、Area 策略、设备准入和本地审计事务基础;当前 `sense-api` 仍只暴露 `/healthz`、`/readyz`,没有认证 tenant 上下文、HTTP handler、幂等收据、并发版本、稳定游标或批量 operation。若继续让实验室工具直写 repository,Bell 和受控集成方没有可发布、可审计的生产控制入口。 + +现有调和器还只选择 enabled 设备,停用写入不会删除该设备的已知 MediaMTX path。公共启停 API 上线前必须补齐这一条确定性收敛路径;它只删除台账中该设备的精确 path,不扩大为孤儿枚举或批量清理。 + +## 关联需求与交互(如适用) + +- 用户故事:US-001、US-002、US-008、US-009、US-010。 +- 交互清单:IX-001~IX-004、IX-013~IX-016、IX-019、IX-020;本任务实现已确认 Sense 原型所依赖的设备控制后端,不修改 HTML 页面布局。 +- 公共契约:`docs/contracts/sense-control-v1.openapi.json`;投影 v1 与审计 v1 保持兼容。 + +## 方案 + +1. 使用已冻结的 `oapi-codegen v2.8.0` 从仓库 OpenAPI 生成 Go 类型和 `net/http` server glue,生成物进入标准漂移门禁;业务校验、认证和事务逻辑留在手写层,不修改生成文件。 +2. Control API 通过 `SENSE_CONTROL_API_ENABLED=true` 显式开启,且只允许 `SENSE_DB_DRIVER=postgres`。默认 SQLite 和无认证的 `/healthz`、`/readyz` 继续可用,但不得注册 `/api/v1` 业务路由。 +3. 定义可替换 `Authenticator` port;首期 `static-sha256` 适配器从仓库外 JSON 文件加载主体、tenant、Site scope、`sense.devices.read|write` 权限和 Bearer token SHA-256,不保存、记录或比较明文 token。原始文件只在启动时加载,变更需受控重启;JWT/OIDC/Bell 会话验证属于后续适配器。 +4. Control API 开启时必须从独立仓库外文件读取至少 32 字节的 cursor HMAC key。游标绑定 tenant、Site、全部过滤条件和最后一项 `(created_at,id)`,使用 URL-safe 编码和 HMAC-SHA256;篡改、跨筛选或跨作用域重放返回 `400 invalid_request`。 +5. PostgreSQL 增量 migration 为设备增加 write-only `profile_token` 和独立 `resource_version`,并创建 24 小时幂等收据、批量 operation/逐项结果表。ETag 是由设备 ID 与 `resource_version` 生成的强 opaque 值;只在配置或期望态表示实际改变时增加,后台 actual/reconcile 更新不使操作员写入无故冲突。所有设备响应设置 `Cache-Control: no-store`。 +6. 创建和批量请求的作用域固定为认证主体 + tenant + Site + `operationId` + `Idempotency-Key`;数据库只保存作用域摘要、规范化请求摘要和脱敏响应快照。同作用域同 body 在至少 24 小时内返回首次状态码、body、ETag/Location 和 trace ID;同 key 不同 body 返回 `409 idempotency_conflict`。收据与首个业务结果同事务提交,过期记录只做有界机会清理。 +7. 实现列表、创建、读取、merge patch、单项期望态、最多 128 项批量期望态和 operation 查询。tenant 只来自认证上下文;超出主体 Site scope、跨 tenant 和不存在统一 `404`,已知作用域内缺权限返回 `403`。列表按 `created_at ASC,id ASC`,默认 50、最大 100;读取只回显 configured 布尔值,永不回显 endpoint、credential 或 profile token。 +8. PATCH 与期望态更新在行锁内比较 `If-Match`:缺失 `428`、格式非法 `400`、不匹配 `412`。Area 变更对成像设备重新执行 Area 准入;启用继续按 Area→Site 固定锁顺序检查策略和配额。重复提交相同期望态不增加 generation/resource version,但仍写一条独立审计事实。 +9. 批量请求内所有重复 `device_id` 对应项均拒绝为 `invalid_request`,其他项继续;每项用 savepoint 隔离预期业务失败,成功项持久化。首版在请求事务内完成“期望态受理”并返回已完成 operation,`succeeded` 只表示期望态和审计已持久化,不表示媒体实际态已经收敛。 +10. 保留审计 v1 文件不变,新增向后兼容的设备审计 v2 契约,增加 `device.configuration.accepted` 脱敏事实;PATCH 与该事实同事务,payload 只记录安全的 changed-fields/Area 逻辑 ID。创建和期望态既有事件继续同时满足 v2;Outbox relay 仍不在本任务实现。 +11. 调和器对 disabled 视频设备执行其精确 `path_name` 的幂等删除,再把 observed generation 标记为当前代、actual state 置为 offline;不枚举 MediaMTX、不删除未知 path、不新增设备删除 API。 + +## 不可变约束 + +- 数值边界:设备列表默认 50、最大 100;批量最少 1、最多 128;请求 body 最大 1 MiB;幂等收据至少保存 24 小时;Site 视频配额默认 16、有效范围 1~128。 +- 认证与隔离:Bearer token 至少 128 bit;静态文件只存 64 位小写十六进制 SHA-256;摘要比较使用 constant-time;tenant 不接受 path/query/body 自报。Site scope 不匹配、跨 tenant 和不存在不得产生可区分响应。 +- 幂等与并发:规范化 body 使用确定性 JSON;收据、首个业务写和 trace/响应快照原子提交。ETag 只作为写并发令牌,不作为可缓存快照;`If-Match: *` 和多 ETag 不接受。 +- 资源模型:设备 ID 与 operation ID 由服务端生成并满足冻结 pattern;modality/capabilities/serial 在 v1 PATCH 中不可修改。视频或 `video_capture` 创建设备必须有 endpoint 与 credential ref;endpoint 禁止 userinfo。 +- 投影失败:Area 缺失/非法/版本回退统一为 `503 area_policy_unavailable`,策略拒绝为 `422 area_policy_denied`;配额缺失/非法/回退分别使用冻结错误。失败只阻止相关写,不改变已有设备或流。 +- 秘密边界:token、cursor key、endpoint、credential、profile token、DSN、完整流 URI 和 MediaMTX 配置不得进入 Git、Issue/PR、普通响应、Problem、日志、游标、幂等作用域明文或审计 payload。静态认证和 cursor key 文件必须位于仓库外。 +- 既有契约:不修改 Sense Control OpenAPI v1 的路径、字段、状态码和错误枚举;不原地扩展严格审计 v1 枚举,新增 v2 文件。Bell 继续拥有 Tenant/Site/Area/RBAC/配额;Sense 不读取 Bell 源表或写 Bell schema。 +- 恢复:新 migration 前向可重放,不提供自动破坏性 down;新代码部署前先装 migration,回滚旧二进制可忽略新增列/表。公共 API 可通过关闭 feature flag 回退,已写设备/Outbox/operation/收据不得被自动删除。 + +## 验收要点 + +- 任务相关验证:Go 单元/HTTP 测试覆盖认证失败、权限、tenant/Site 隐藏、7 个 endpoint、严格 JSON、敏感字段不回显、cursor 篡改/绑定、ETag 428/412、创建和批量幂等、重复项、稳定错误与 disabled path 删除;`./scripts/test_postgres.ps1 -PgRoot D:\pgsql17` 覆盖真实 migration、收据原子性、并发重复请求、逐项 savepoint、operation 可见性、审计 v2 和权限。 +- 静态契约:`python -m unittest discover -s tests -p "test_sense_control_implementation.py"` 与既有全部契约测试通过;生成后工作树无漂移,OpenAPI v1 和审计 v1 指纹语义不被改写。 +- 完整门禁:`./init.ps1`、`python scripts/validate_agent_context.py`、`python -m unittest discover -s tests -p "test_*.py"`、`python scripts/validate_harness_governance.py`、`go -C Sense test ./...`、`go -C Sense vet ./...`、`go -C Sense build ./...`、`git diff --check` 全部通过。 +- 人工 / 设备验收:不需要摄像头或 UI;使用临时 PostgreSQL 和 fake MediaMTX/adapter 完成。本任务不解除 T-007 真实多路现场门禁,也不形成 16/128 路容量承诺。 +- 构建产物:生成的 Control API glue、认证/handler/store、增量 migration、审计 v2 schema、外部配置示例(只含占位符)、本机隔离集成测试和同步运维文档。 + +## 边界(不改什么) + +不实现 Bell 管理服务或 UI、JWT/OIDC、静态主体热加载、TLS termination、WAF/公网暴露、Outbox relay/签名/留存、设备删除、孤儿枚举、证据 API、Brain、WireGuard、容量压测或 T-007 现场验收;不把 Control API 下放到 SQLite,不修改已确认 Sense/Bell HTML 原型。 + +## 协作约束 + +- 责任 Agent:由 dispatcher 分配。 +- 唯一写入者:同责任 Agent。 +- 委派:默认不启用。 +- Gitea:任务文件先进入默认分支,再创建唯一 Issue 并回填编号;领取时记录 `context_ref`、claim / 工作分支和全部允许写路径。 + +任何新增写路径先检查与其他活跃任务是否重叠;同一时刻只有一个 Agent 修改本任务的 `write_paths`。 + +## 执行记录 + +### 2026-08-07 任务定义 + +- 项目负责人要求创建并实施 T-011;依赖 T-010 已完成,Gitea 当前只有 T-007 因真实设备条件处于 waiting,不占用写路径。 +- 选择 PostgreSQL 一致性实现而不是进程内幂等/operation 缓存;选择外部静态 SHA-256 注册表作为私有部署首版认证适配器,同时保留未来 Bell/JWT/OIDC port。 +- 公共停用 API 上线前补齐精确 path 删除;该变更不扩大为孤儿清理,也不修改 T-007 门禁。