Files
yovision/docs/tasks/T-011.md
T
QiuSW a0d239811f
Harness governance / validate (push) Has been cancelled
Harness governance / validate (pull_request) Has been cancelled
feat(sense): implement Control API v1 [T-011]
2026-08-07 20:54:41 +08:00

123 lines
13 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-011
title: 实现 Sense Control API v1 与 PostgreSQL 一致性边界
phase: 2
deps: [T-010]
status: DONE
created: 2026-08-07
issue: 39
context_ref: 6402d4384d5c2b1c0f3852858c3cf0d63d948262
claim_branch: claims/T-011
work_branch: agent/codex/T-011
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/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
- 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 完成 Sense Control API v1
- 使用冻结的 `oapi-codegen v2.8.0` 从 T-008 OpenAPI 生成 Go 1.22+ `net/http` server glue(生成 SHA-256 `39a52e4f54bb1a742f58b64dcf15ce98359385236da5ccc0dae77e8ded2b218b`),实现全部 7 个 endpoint、严格 JSON/1 MiB body、稳定 Problem、write-only 字段脱敏和 `Cache-Control: no-store`;生成漂移已进入 `init.ps1`/`init.sh`。
- 新增可替换认证 port 与 `static-sha256` 私有部署适配器:外部注册表只保存 token 摘要、主体、tenant、Site scope 与两项权限,摘要 constant-time 比较;业务路由默认关闭,只能在 PostgreSQL v4 schema 与外部 32 字节 HMAC cursor key 有效时开启。SQLite 默认路径继续只暴露探针。
- 新增 `008`/`009` migration:设备 `resource_version`/write-only profile token、24 小时摘要幂等收据、持久化 batch operation/逐项结果与最小权限。创建收据与设备/审计同事务;batch 以 savepoint 隔离逐项失败,并先按设备 ID 排序锁定目标,避免相反请求顺序死锁。ETag 只在配置/期望态实际变化时前进;相同期望态仍审计但不增加 generation/resource version。
- 保留严格审计 v1 文件不变,新增 v2 后继和 `device.configuration.accepted` 脱敏事实;Area 修改重新准入。停用调和只对台账中的精确 path 做幂等删除,成功后 observed generation 收敛且 actual state 为 offline,不枚举或清理未知 path。
- `./scripts/test_postgres.ps1 -PgRoot D:\pgsql17` 通过:`001`~`009` 连续重放、SQL 权限断言和 25 个 `TestPostgres*` 全绿,覆盖列表稳定分页/过滤、创建并发幂等、收据脱敏/冲突、ETag/PATCH、审计 v2、逐项 batch、operation tenant 隐藏、相反顺序批量锁与 PUBLIC 权限负例;随机回环临时集群已停止并清理,现有 `D:\pgsql17\data`/5432 listener 未读取、停止或修改。
- `./init.ps1` 通过;Python 契约/治理测试共 47 项通过;`go -C Sense test ./...`、`go -C Sense vet ./...`、`go -C Sense build ./...`、生成漂移和 `git diff --check` 均通过;`go test -race ./internal/auth ./internal/controlapi ./internal/reconcile ./internal/store` 通过。不需要摄像头或 UI,本结果不解除 T-007,也不形成 16/128 路容量承诺。
### 2026-08-07 领取任务
- dispatcher `ila` 将 Issue #39 分配给 `codex`;`context_ref` 为 `6402d4384d5c2b1c0f3852858c3cf0d63d948262`,claim 为 `claims/T-011`,工作分支为 `agent/codex/T-011`。
- 已读回 Issue `status/doing`、assignee、dispatcher 发布的结构化 CLAIM 与两个分支 SHA;接受 frontmatter 全部写路径。T-007 仍为 waiting,当前没有活跃写路径冲突。
- 实现中发现标准生成入口还需同步 `docs/00-ai-start-here.md`;dispatcher 复查无活跃冲突后发布完整 CLAIM RENEWAL,本任务据此增加该精确路径。
### 2026-08-07 Gitea 映射
- 任务规格先合入默认分支,再创建唯一主 Issue #39;本提交只回填双向映射,映射合入并读回前不领取任务。
### 2026-08-07 任务定义
- 项目负责人要求创建并实施 T-011;依赖 T-010 已完成,Gitea 当前只有 T-007 因真实设备条件处于 waiting,不占用写路径。
- 选择 PostgreSQL 一致性实现而不是进程内幂等/operation 缓存;选择外部静态 SHA-256 注册表作为私有部署首版认证适配器,同时保留未来 Bell/JWT/OIDC port。
- 公共停用 API 上线前补齐精确 path 删除;该变更不扩大为孤儿清理,也不修改 T-007 门禁。