124 lines
14 KiB
Markdown
124 lines
14 KiB
Markdown
# YoVision 控制面、审计与内部事件传输契约 v1
|
||
|
||
> 冻结日期:2026-08-11。Control API、审计 relay、Brain 事件 ingress 与 Bell 回环 Alert 控制台契约版本:`1.0.0`。Sense 是设备期望态的提供方;Bell 是 Tenant、Site、Area、RBAC、配额、事件、Alert 与全局审计的所有者。T-016 已实现 Sense Outbox 审计 relay,T-019 已实现 Brain 事件 Outbox ingress,T-020 已实现 Bell 规则→Alert→ack/close 工程纵切;Bell 公共管理服务与 JWT/OIDC 仍未实现。
|
||
|
||
## 契约文件
|
||
|
||
| 文件 | 生产者 / 所有者 | 消费者 | 用途 |
|
||
| --- | --- | --- | --- |
|
||
| [`sense-control-v1.openapi.json`](sense-control-v1.openapi.json) | Sense | Bell 管理面、受控集成方 | 设备查询、创建、修改、启停与批量操作 |
|
||
| [`site-quota-v1.sql`](site-quota-v1.sql) | Bell | Sense | 单 PostgreSQL 实例内的站点视频配额只读投影 |
|
||
| [`area-policy-v1.sql`](area-policy-v1.sql) | Bell | Sense | Area 归属与 `capture_policy` 只读投影 |
|
||
| [`sense-device-audit-v1.schema.json`](sense-device-audit-v1.schema.json) | Sense | 本地 Outbox;Bell relay | 脱敏设备操作审计事实,不包含传输协议 |
|
||
| [`sense-device-audit-v2.schema.json`](sense-device-audit-v2.schema.json) | Sense | 本地 Outbox;Bell relay | v1 后继,增加脱敏配置修改受理事实;v1 文件保持不变 |
|
||
| [`sense-audit-relay-v1.openapi.json`](sense-audit-relay-v1.openapi.json) | Bell | Sense | 内部批量端点、HMAC、逐项确认、nonce 防重与重试边界 |
|
||
| [`brain-event-ingress-v1.openapi.json`](brain-event-ingress-v1.openapi.json) | Bell | Brain | 单业务事件入站、producer/key 绑定、Bell ID 与跨重启幂等语义 |
|
||
| [`bell-alert-console-v1.openapi.json`](bell-alert-console-v1.openapi.json) | Bell | 回环工程值班台 | Alert 分页/详情、首次 ack、确认后关闭与永久幂等收据 |
|
||
|
||
OpenAPI 的 `/api/v1` 路径是公共控制面边界;`/healthz`、`/readyz` 仍是非业务运维探针。v1 不提供设备删除:停用设备使用期望态接口,保留设备、操作和审计历史。Site、Area、配额、RBAC 和审计聚合不由 Sense 提供 CRUD。
|
||
|
||
## HTTP 资源与操作
|
||
|
||
| 操作 | 路径 | 关键约束 |
|
||
| --- | --- | --- |
|
||
| 列出 / 创建设备 | `GET/POST /api/v1/sites/{site_id}/devices` | 列表使用不透明 cursor,默认 50、最大 100;创建要求 `Idempotency-Key`,设备 ID 由服务端生成 |
|
||
| 读取 / 修改设备 | `GET/PATCH /api/v1/sites/{site_id}/devices/{device_id}` | 修改要求 `If-Match`,版本不匹配返回 `412 etag_mismatch` |
|
||
| 修改期望态 | `PUT /api/v1/sites/{site_id}/devices/{device_id}/desired-state` | 要求 `If-Match`;受理不表示实际态已经收敛 |
|
||
| 批量修改期望态 | `POST /api/v1/sites/{site_id}/devices:batchDesiredState` | 要求 `Idempotency-Key`,最多 128 项,异步返回逐项结果 |
|
||
| 查询批量操作 | `GET /api/v1/operations/{operation_id}` | 只返回当前租户和站点可见的操作 |
|
||
|
||
列表按 `created_at ASC, id ASC` 稳定排序,cursor 是服务端生成的不透明位置标记。客户端不得解析或拼接 cursor;服务端可以在兼容范围内改变编码。批量不是跨设备全有或全无事务:每项独立接受或拒绝,成功项继续收敛,失败项带稳定错误码。请求内重复 `device_id` 视为对应项 `invalid_request`,不得用“最后一项覆盖”。重试失败项应使用新幂等键;原样重放整个请求必须返回原收据。
|
||
|
||
## 身份、租户与敏感信息
|
||
|
||
- 所有 `/api/v1` 操作都需要 Bearer 认证;具体 token 格式由认证任务冻结。`tenant_id` 只从认证上下文取得,body、query 和 path 均不能自报 tenant。
|
||
- 跨租户访问与资源不存在都返回 `404 not_found`,不得用状态码、消息或耗时泄露资源是否存在。授权范围不足但不涉及资源枚举时返回 `403 forbidden`。
|
||
- `endpoint_ref`、`credential_ref` 和 `profile_token` 是 write-only 输入。不允许 userinfo 形式的完整 RTSP URI;响应、错误、日志与示例不得包含这些引用、密码、token、完整连接串或 MediaMTX 内部配置。
|
||
- 创建设备显式提交 `modality + capabilities`;`video` 模态必须包含 `video_capture`,任何请求了 `video_capture` 的设备都必须同时提供 endpoint 与 credential 引用。服务端仍须由适配器验证能力,不能把客户端声明当作探测成功。
|
||
- `ETag` 表示设备资源版本;`If-Match` 缺失返回 `428 precondition_required`,过期版本返回 `412 etag_mismatch`。相同期望态重复提交不增加 generation,但每次受理仍可产生审计记录。
|
||
|
||
`Idempotency-Key` 的作用域是“认证主体 + tenant + site + operation + key”,服务端至少保存 24 小时。同作用域、同请求体重放返回首次状态码和响应;同 key 不同请求体返回 `409 idempotency_conflict`。幂等收据不等价于实际态完成。
|
||
|
||
## 配额投影与准入
|
||
|
||
Bell migration 最终创建 `bell.site_quota_v1`,列顺序和含义固定如下:
|
||
|
||
| 列 | 含义 |
|
||
| --- | --- |
|
||
| `tenant_id`、`site_id` | 与两个 schema 共享的稳定逻辑 ID |
|
||
| `max_video_channels` | 默认 16,有效范围 1~128 |
|
||
| `source_version` | 站点投影每次变更后单调递增的版本 |
|
||
| `source_updated_at` | Bell 源记录的更新时间 |
|
||
|
||
视图由 `bell_app` 拥有;`sense_app` 只有 Bell schema 的 `USAGE` 和该视图的 `SELECT`,没有 `INSERT`、`UPDATE`、`DELETE` 或 Bell 源表权限。Sense 不得通过任何旁路写 Bell schema。首期使用同一 PostgreSQL 实例及 `sense`/`bell` schema;如果未来分库,必须发布新版本的网络契约,不能在 v1 下静默改变一致性和失败语义。
|
||
|
||
配额只统计 `desired_state=enabled` 且 capabilities 含 `video_capture` 的设备。创建已启用视频设备或把视频设备启用时,Sense 必须在同一设备写路径读取并记录所用 `source_version`,同时验证 Area 策略投影。降低配额不会自动停用已有设备;若当前占用已超限,后续创建/启用返回 `409 quota_exceeded`。配额行缺失、越界、版本回退或投影不可读时返回 `503 quota_projection_unavailable`,只阻止相关创建/启用,读取、非准入属性修改和停用仍允许,已有流保持运行。
|
||
|
||
T-010 冻结 `bell.area_policy_v1` 的列顺序为 `tenant_id/site_id/area_id/capture_policy/source_version/source_updated_at`,策略仅允许 `video_allowed | non_imaging_only`。Sense 对所有 PostgreSQL 新建设备验证 Area 归属;具有 `video_capture` 能力的设备在创建(包括 disabled 创建)和启用时检查策略。缺失、非法、版本回退或不可读映射为 `area_policy_unavailable`,`non_imaging_only` 拒绝成像变更并映射为 `area_policy_denied`。同库视图实时读取,不把长期未修改记录的 `source_updated_at` 年龄误判为过期。
|
||
|
||
## 本地设备操作审计
|
||
|
||
`sense-device-audit-v1.schema.json` 继续冻结创建与期望态两类事实且不原地扩展严格枚举。T-011 新增 v2 后继,兼容 v1 两类事件并增加 `device.configuration.accepted`;该 payload 只保存是否变化、字段名和 Area 逻辑 ID,不保存字段值。主体类型为 `user | service | system`,投影版本与 generation 随事实保存;endpoint、credential、profile token、path、密码、完整流 URI 或 MediaMTX 配置始终禁止进入审计。
|
||
|
||
PostgreSQL repository 必须在设备创建/期望态事务内写 `sense.device_operation_outbox`;Outbox 失败回滚业务写入。相同期望态不增加 generation,但仍产生独立审计事实。事件 schema 继续只定义事实;传输由 `sense-audit-relay-v1.openapi.json` 独立冻结。
|
||
|
||
## Sense → Bell 审计 relay
|
||
|
||
Sense 向 `/internal/v1/audit-events:batch` 每批发送 1~100 个事件,请求体不超过 1 MiB、deadline 10 秒。请求用外部文件中的至少 32 字节 secret 做 HMAC-SHA256,canonical string 为 method、path、Unix 秒、随机 nonce 与 body SHA-256 的换行拼接;非回环地址必须使用 HTTPS。Bell 允许 300 秒时钟偏差并将 `(key_id, nonce)` 收据保留 600 秒:相同请求摘要返回原结果,不同摘要返回 `409 replay_conflict`。
|
||
|
||
Outbox 用 30 秒数据库时钟 lease、单调 fencing token 和 `FOR UPDATE SKIP LOCKED` 协调实例。`accepted/duplicate` 才标记 delivered;逐项 `rejected` 进入 dead letter;网络、5xx、认证失败或缺失结果以 1 秒起步、最多 300 秒指数退避。Sense 不直接访问 Bell schema,Bell 不读取 Sense Outbox;全局 `bell.audit_events` 不自动删除,只有短期 relay receipt 自动过期。
|
||
|
||
两端读取同格式的仓库外 key 文件;Bell 可同时接受多个 key,Sense 用 `SENSE_AUDIT_RELAY_KEY_ID` 选择一个,便于先加新 key、切换发送端、再移除旧 key。占位结构如下,`secret_base64url` 必须替换为至少 32 个随机字节的无填充 base64url,不能提交真实值:
|
||
|
||
```json
|
||
{"version":1,"keys":[{"key_id":"sense-a","secret_base64url":"<external-secret>"}]}
|
||
```
|
||
|
||
## Brain → Bell 业务事件 ingress
|
||
|
||
Brain 向 `POST /internal/v1/event-candidates` 单条投递完整 event v0.1 candidate;candidate 与冻结最终事件的顶层字段完全一致,只省略 Bell 所有的 `id`。envelope 额外携带 `schema_version=1` 和 `producer_id`。Bell 校验 HMAC、key 与 producer 的绑定、当前逻辑设备/Area 映射、event schema 和六项语义断言后生成 `evt_` ULID;首次接受返回 `201 accepted`,同一来源事实重投返回 `200 duplicate` 和原 Bell ID。
|
||
|
||
认证 canonical string 与审计 relay 使用相同五行算法,但 key 文件独立且每项增加 `producer_id`,不得混用审计 key。正文最多 1 MiB、deadline 10 秒、允许 300 秒时钟偏差,nonce 收据至少保留 600 秒;HTTP 只允许回环地址,非回环必须 HTTPS。key 文件形态为:
|
||
|
||
```json
|
||
{"version":1,"keys":[{"key_id":"brain-a","producer_id":"brain-main","secret_base64url":"<external-secret>"}]}
|
||
```
|
||
|
||
Bell 的永久 `event_ingress_receipts` 以 `(producer_id, source_event_id)` 唯一并保存 canonical candidate SHA-256。相同 hash 重投返回原 event ID,不同 hash 返回 `409 source_event_conflict`;事件、来源收据和成功 nonce 响应在一个 PostgreSQL 事务中提交。`event_ingress_nonces` 只是 10 分钟防重表,可清理;来源收据和事件不自动删除、不可更新。Brain 必须先把 candidate 写入仓库外 SQLite Outbox,只有 `accepted/duplicate` 可标记 delivered;401、网络和 5xx 以 1~300 秒退避重试,稳定 4xx 进入 dead letter,最多 100 次、最多 10,000 条待投递。
|
||
|
||
数字 `tenant_id/site_id/device_id` 通过 Bell 所有的 `event_ingress_bindings` 映射到当前 Bell Site/Area 和 Sense Device 逻辑 ID。运行时只读取 Sense 设备的 ID、Area 与 modality 列,不获得 endpoint、credential、profile token 或 path;绑定缺失/禁用、设备或 Area 不一致、删除、非视频或 `capture_policy != video_allowed` 均失败关闭。首版绑定只由受控 migration/admin SQL 配置,没有公共 CRUD。
|
||
|
||
## Bell 回环规则与 Alert 控制台
|
||
|
||
T-020 的 [`bell-alert-console-v1.openapi.json`](bell-alert-console-v1.openapi.json) 只冻结工程 API,不占用未来 Bell 公共 `/api/v1`。整个 `bell-api` 必须显式监听回环,规则、token、tenant/Site 和 actor 上下文均从仓库外启动配置读取;请求不得自报这些上下文。列表按 `created_at DESC,id DESC` 稳定分页,默认 16、最大 100。
|
||
|
||
规则文件 v1 只支持精确 `event_kind`、最小严重度、可选 Site、启用状态和生效时间。相同 `(tenant_id,rule_key)` 的配置按 canonical SHA-256 幂等发布;变化生成不可变新版本。Event 无论有无规则/是否命中都会留下 durable sweep;每个被考虑版本写 matched/no_match evaluation,命中时 Bell 创建独立 `alt_` ULID 和 Event 多对多关联。Event、规则版本、evaluation、Alert 身份、关联和 transition 均只追加。
|
||
|
||
Alert 状态机为 `open → acknowledged → closed`。ack 由数据库事务与 advisory lock 串行化,只有首个竞争者成功;后到者返回 `409 already_acknowledged` 和实际首位 actor/time,不覆盖历史。close 只允许从 acknowledged 进入。所有命令要求 8~128 字符 `Idempotency-Key`;同租户同 key/同命令永久重放原 status/body,同 key/不同命令返回 `409 idempotency_conflict`。当前详情固定返回 `evidence_status=not_enabled`、`delivery_status=not_enabled`,不得伪造切片、升级或送达事实。
|
||
|
||
## 兼容与废弃
|
||
|
||
- v1 可增加不改变已有语义的可选响应字段和新错误细节;客户端必须忽略未知响应字段。
|
||
- 删除/重命名字段、收紧已接受输入、改变状态码/幂等作用域/配额计数或把只读视图改为远程调用,均属于破坏性变化,必须发布新版本。
|
||
- 废弃版本应先在 OpenAPI 标记并公告迁移窗口;服务端在所有已声明消费者完成迁移前继续提供旧版本。
|
||
- OpenAPI 中的稳定错误码用于程序判断,`message` 只用于人读,不得依赖其字面内容。
|
||
|
||
## 验证
|
||
|
||
从仓库根目录执行:
|
||
|
||
```powershell
|
||
python -m json.tool docs/contracts/sense-control-v1.openapi.json | Out-Null
|
||
python -m json.tool docs/contracts/sense-device-audit-v2.schema.json | Out-Null
|
||
python -m json.tool docs/contracts/sense-audit-relay-v1.openapi.json | Out-Null
|
||
python -m json.tool docs/contracts/brain-event-ingress-v1.openapi.json | Out-Null
|
||
python -m json.tool docs/contracts/bell-alert-console-v1.openapi.json | Out-Null
|
||
python -m unittest discover -s tests -p "test_sense_control_contract.py"
|
||
python -m unittest discover -s tests -p "test_sense_control_implementation.py"
|
||
python -m unittest discover -s tests -p "test_sense_audit_relay_contract.py"
|
||
python -m unittest discover -s tests -p "test_brain_event_ingress_contract.py"
|
||
python -m unittest discover -s tests -p "test_bell_alert_contract.py"
|
||
```
|
||
|
||
测试同时校验 OpenAPI 结构、生成 server glue、HTTP handler 与 PostgreSQL migration/事务;它不替代 Bell 消费方联合验收或客户现场容量验证。
|