11 KiB
11 KiB
id, title, phase, deps, status, created, issue, context_ref, claim_branch, work_branch, write_paths
| id | title | phase | deps | status | created | issue | context_ref | claim_branch | work_branch | write_paths | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| T-011 | 实现 Sense Control API v1 与 PostgreSQL 一致性边界 | 2 |
|
DOING | 2026-08-07 | 39 | 6402d4384d |
claims/T-011 | agent/codex/T-011 |
|
问题 / 背景
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 保持兼容。
方案
- 使用已冻结的
oapi-codegen v2.8.0从仓库 OpenAPI 生成 Go 类型和net/httpserver glue,生成物进入标准漂移门禁;业务校验、认证和事务逻辑留在手写层,不修改生成文件。 - Control API 通过
SENSE_CONTROL_API_ENABLED=true显式开启,且只允许SENSE_DB_DRIVER=postgres。默认 SQLite 和无认证的/healthz、/readyz继续可用,但不得注册/api/v1业务路由。 - 定义可替换
Authenticatorport;首期static-sha256适配器从仓库外 JSON 文件加载主体、tenant、Site scope、sense.devices.read|write权限和 Bearer token SHA-256,不保存、记录或比较明文 token。原始文件只在启动时加载,变更需受控重启;JWT/OIDC/Bell 会话验证属于后续适配器。 - Control API 开启时必须从独立仓库外文件读取至少 32 字节的 cursor HMAC key。游标绑定 tenant、Site、全部过滤条件和最后一项
(created_at,id),使用 URL-safe 编码和 HMAC-SHA256;篡改、跨筛选或跨作用域重放返回400 invalid_request。 - PostgreSQL 增量 migration 为设备增加 write-only
profile_token和独立resource_version,并创建 24 小时幂等收据、批量 operation/逐项结果表。ETag 是由设备 ID 与resource_version生成的强 opaque 值;只在配置或期望态表示实际改变时增加,后台 actual/reconcile 更新不使操作员写入无故冲突。所有设备响应设置Cache-Control: no-store。 - 创建和批量请求的作用域固定为认证主体 + tenant + Site +
operationId+Idempotency-Key;数据库只保存作用域摘要、规范化请求摘要和脱敏响应快照。同作用域同 body 在至少 24 小时内返回首次状态码、body、ETag/Location 和 trace ID;同 key 不同 body 返回409 idempotency_conflict。收据与首个业务结果同事务提交,过期记录只做有界机会清理。 - 实现列表、创建、读取、merge patch、单项期望态、最多 128 项批量期望态和 operation 查询。tenant 只来自认证上下文;超出主体 Site scope、跨 tenant 和不存在统一
404,已知作用域内缺权限返回403。列表按created_at ASC,id ASC,默认 50、最大 100;读取只回显 configured 布尔值,永不回显 endpoint、credential 或 profile token。 - PATCH 与期望态更新在行锁内比较
If-Match:缺失428、格式非法400、不匹配412。Area 变更对成像设备重新执行 Area 准入;启用继续按 Area→Site 固定锁顺序检查策略和配额。重复提交相同期望态不增加 generation/resource version,但仍写一条独立审计事实。 - 批量请求内所有重复
device_id对应项均拒绝为invalid_request,其他项继续;每项用 savepoint 隔离预期业务失败,成功项持久化。首版在请求事务内完成“期望态受理”并返回已完成 operation,succeeded只表示期望态和审计已持久化,不表示媒体实际态已经收敛。 - 保留审计 v1 文件不变,新增向后兼容的设备审计 v2 契约,增加
device.configuration.accepted脱敏事实;PATCH 与该事实同事务,payload 只记录安全的 changed-fields/Area 逻辑 ID。创建和期望态既有事件继续同时满足 v2;Outbox relay 仍不在本任务实现。 - 调和器对 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 领取任务
- 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 门禁。