Files
story/docs/privacy-requests.md
T

131 lines
7.9 KiB
Markdown
Raw Normal View History

2026-08-26 06:41:07 +08:00
# 隐私数据请求开发说明
## 1. 当前交付边界
当前版本完成了`ACCESS`数据访问申请和`DELETE`主体删除申请的端到端闭环。`ACCESS`由Worker在服务端租户/家庭/长者边界内盘点数据范围;`DELETE`在受理事务内立即冻结主体,再由Worker对六个子系统逐项处理、持久化状态并对账,只有全部子任务进入`COMPLETED`或经核验的`SKIPPED`后才将总请求置为`COMPLETED`。
隐私请求结果始终设置`content_included=false`,不把档案、转写、故事或记忆正文复制到请求表、审计记录和普通日志。正文导出由独立私有导出任务实现,见`data-export.md`。当前仍未开放`CORRECTION`和独立的`CONSENT_WITHDRAWAL`批处理请求。
## 2. 权限与安全边界
| 能力 | 家庭所有者 | 管理员 | 编辑者/查看者 |
|---|:---:|:---:|:---:|
| `privacy.read` | 允许 | 允许 | 拒绝 |
| `privacy.manage` | 允许 | 拒绝 | 拒绝 |
服务端从OIDC主体重新解析账号、客户组织、家庭成员和长者归属,不使用浏览器提交的租户条件。创建请求必须提交16至128字符的`Idempotency-Key`;同键同正文返回原请求,同键不同正文返回`IDEMPOTENCY_KEY_CONFLICT`。详情响应使用`Cache-Control: no-store`和强ETag。
`DELETE`还必须满足以下门禁:
- `reason`为1至500字符,但服务端仅持久化SHA-256摘要,不保存原因正文;
- `confirmation`必须与主体当前称呼完全一致,确认值不持久化;
- 主体存在`PREPARING`、`ROOM_ISSUED`、`STARTING_AGENT`、`ACTIVE`或`STOPPING`通话时返回`SUBJECT_ACTIVE_CONVERSATION`,必须先正常结束通话;
- 受理后主体立即变为`deleting`,一般主体、内容、设备和导出接口不再返回该主体;所有有效授权同时撤回,记忆停用并清除可召回向量;
- 删除申请详情使用请求编号仍可查询,即使主体已进入`deleting`或`deleted`。
## 3. 业务流程
```mermaid
sequenceDiagram
participant U as 家庭所有者
participant A as API
participant D as PostgreSQL
participant O as Outbox
participant W as Worker
U->>A: POST /subjects/{id}/privacy-requests
A->>A: 验证OIDC、privacy.manage、长者边界和幂等键
A->>D: 创建SUBMITTED请求与PENDING子任务
A->>O: privacy.access_requested
A-->>U: 202 + 请求编号
W->>D: VERIFYING -> EXECUTING
W->>D: 按冻结分类统计当前长者数据
W->>D: 子任务COMPLETED,请求COMPLETED
W->>D: 写入无正文系统审计
U->>A: GET /privacy-requests/{id}
A-->>U: 状态、分类数量和子任务
```
### 3.1 DELETE删除与对账流程
```mermaid
sequenceDiagram
participant U as 家庭所有者
participant A as API
participant D as PostgreSQL
participant O as Outbox
participant W as Worker
participant R as Redis
participant S as MinIO
U->>A: DELETE申请+原因+精确称呼确认
A->>D: 校验权限、确认值及无活动通话
A->>D: 主体deleting、撤回授权、停用记忆/向量
A->>D: 创建6个PENDING子任务
A->>O: privacy.delete_requested
A-->>U: 202 + 请求编号
W->>R: 删除会话租约和设备在线键
W->>S: 删除所有私有导出对象
W->>D: 核验媒体对象和外部索引引用
W->>D: 清空pgvector及供应商引用
W->>D: 脱敏正文、快照、回调、绑定和档案
W->>D: 六任务对账+最小tombstone
W->>D: 请求COMPLETED或PARTIAL_FAILED
```
六类子任务如下:
| 子系统 | 当前动作 | 成功判定 |
|---|---|---|
| `redis` | 删除该主体历史通话租约键和关联设备在线键 | Redis删除操作成功,允许键本来不存在 |
| `export_files` | 删除全部`export_task.object_ref`对应私有对象,任务改为`EXPIRED`并清空对象元数据 | 每个对象删除成功且数据库引用为空 |
| `object_storage` | 删除该主体全部已登记录音对象,并把分片置为`DELETED`、清空对象引用和文件摘要 | 对象存储删除失败时记`FAILED`并持续重试;无对象时安全完成 |
| `pgvector` | 清空本地embedding和provider引用,索引记`DELETED` | 目标主体本地向量均不可召回 |
| `provider_index` | 检查外部供应商索引引用 | 无外部引用时记`SKIPPED`;存在引用但无删除适配器时失败并使总请求`PARTIAL_FAILED` |
| `postgresql` | 对档案、事实、转写、摘要、故事、记忆、快照、回调和绑定做删除或不可逆脱敏 | 主体为`deleted`,正文为固定删除标记,审计只留最小证明 |
## 4. 接口
| 接口 | 能力 | 关键约束 | 结果 |
|---|---|---|---|
| `POST /api/v1/subjects/{id}/privacy-requests` | `privacy.manage` | 接受`ACCESS`或`DELETE`;必须`Idempotency-Key`;DELETE还需原因和精确确认 | `202`受理,幂等重放为`200` |
| `GET /api/v1/subjects/{id}/privacy-requests` | `privacy.read` | 固定长者边界 | 请求列表及盘点摘要 |
| `GET /api/v1/privacy-requests/{id}` | `privacy.read` | 服务端反查家庭归属;禁止缓存 | 请求详情和子任务 |
可请求的分类为`profile`、`consents`、`device_bindings`、`conversations`、`transcripts`、`summaries`、`stories`和`memories`。不指定分类时盘点全部分类。
## 5. 数据库
| 表 | 作用 | 关键约束 |
|---|---|---|
| `privacy_request` | 隐私请求聚合根 | 租户+申请人+幂等键唯一;请求摘要用于检测正文冲突 |
| `privacy_subtask` | 子系统执行与重试状态 | 请求+子系统+目标唯一;保留尝试次数和最后错误 |
| `tombstone` | 后续删除闭环的最小不可逆证明 | 只保存资源摘要和保留原因,不保存待删正文 |
V9迁移提供了请求、子任务和最小证明模型。当前`ACCESS`和`DELETE`已开放;`CORRECTION`与独立`CONSENT_WITHDRAWAL`仍保持关闭。
独立临时空库已完成`up 1—9 → down -all → up 1—9`,最终迁移状态为`9|dirty=false`,验证库随后已删除。
## 6. 自动化验证
`scripts/smoke-privacy.ps1`使用真实本地OIDC令牌验证:
- 所有者创建、同键重放和同键不同范围冲突;
- 编辑者提交请求返回403;
- Worker完成数据库子任务,请求收敛为`COMPLETED`;
- 返回的分类严格等于请求范围且`content_included=false`;
- 列表可见、测试数据和关联Outbox/审计可清理。
`scripts/smoke-privacy-delete.ps1`使用临时家庭和主体,并验证:
- 错误称呼确认和非所有者提交均被拒绝;
- 存在活动通话时删除返回409,正常结束后才允许受理;
- 受理后普通主体读取和新建导出立即被阻断;
- 冻结后的同键同正文仍能安全重放,不会创建第二个请求;
- 六个子任务全部收敛为`COMPLETED`或经核验的`SKIPPED`;
- 档案和稳定事实正文已脱敏,原因明文未进入请求表;
- 私有导出数据库引用与MinIO对象均已删除;
- 仅留下不含个人正文的SHA-256主体摘要墓碑;
- 测试完成后临时业务数据、Outbox、审计和对象均被清理。
家庭工作台的“提交全量盘点”现通过页面内受控操作区提交。操作区固定当前长者公开编号和八类盘点范围:长者档案、授权记录、设备绑定、通话、话轮、摘要、故事和记忆;同时明确ACCESS结果仅返回各分类数量、处理状态和子任务状态,不会把个人正文复制到隐私请求、审计或普通日志。用户必须显式确认范围后才能提交,取消或切换业务上下文会清除草稿且不会创建请求。
页面按低强度策略使用真实OIDC执行无提交冒烟:检查固定对象、八类范围、正文边界、确认门禁和取消行为,并验证零创建请求、请求数量不变、无横向溢出及无非预期浏览器错误。服务端权限、幂等、租户隔离和Worker闭环继续由既有定向脚本覆盖,不在每次页面改动后重复执行全套隐私删除回归。