113 lines
11 KiB
Markdown
113 lines
11 KiB
Markdown
---
|
||
id: T-302
|
||
title: 已授权任务原子领取与租约(F-005)
|
||
phase: 3
|
||
deps: [T-301, T-203]
|
||
status: DONE
|
||
created: 2026-08-04
|
||
vikunja_task_id: 34
|
||
context_ref: 2331882
|
||
work_branch: task/t-302-atomic-claim
|
||
needs_device: false
|
||
needs_human_review: false
|
||
write_paths:
|
||
- docs/tasks/T-302.md
|
||
- admin/migrations/00005_task_claims.sql
|
||
- admin/internal/migrations/migrations_test.go
|
||
- admin/internal/taskclaim/**
|
||
- admin/internal/config/**
|
||
- admin/internal/storage/evidence/**
|
||
- admin/internal/taskdetail/store_test.go
|
||
- admin/internal/server/**
|
||
- admin/cmd/server/**
|
||
- admin/README.md
|
||
- docs/api.md
|
||
- docs/04-architecture.md
|
||
---
|
||
|
||
<!-- BEGIN VIKUNJA EXPORT id=34 synced=2026-08-04T14:33:59Z sha256=85cf1ed0ce7ba219accfaee3bcbb39c9f1f584541446ea37df846db9ac63fa1e -->
|
||
## 问题 / 背景
|
||
|
||
T-203 已把管理员明确授权的任务原子转为 PENDING,T-301 已提供可撤销的真实设备身份,但采购工具还不能安全领取任务。T-302 建立服务端原子领取、幂等重放和租约续期边界,使同一授权只能产生一条可恢复 attempt;不实现客户端轮询、页面执行、事件、提交围栏或结果上报。
|
||
|
||
## 关联需求与交互
|
||
|
||
- 功能:F-005。
|
||
- 用户故事:US-003、US-005、US-007。
|
||
- 交互:为 IX-007、IX-008 提供服务端契约;本任务无新增管理页面。
|
||
- 依赖:T-301、T-203;后续消费者:T-303、T-205、T-208、T-306。
|
||
- API:`POST /api/v1/tasks/claim-next`、`POST /api/v1/tasks/{id}/lease/renew`。
|
||
|
||
## 方案
|
||
|
||
1. 新增 `00005_task_claims.sql`。用独立 claim、claim request、lease renewal 表绑定 task、authorization、attempt、设备、session、generation、随机 nonce、token hash 和租约;同一授权最多一个 attempt,同一设备最多一个未关闭 claim。迁移遇到无法推断归属的既有 attempt/submission/evidence 事实时拒绝升级,已有领取事实时拒绝降级。
|
||
2. Claim token 使用独立 `CMBUYER_CLAIM_TOKEN_SECRET`(64 位小写十六进制,解码为 32 字节),不复用管理员 session 或设备 token。每条 claim 生成 32 字节随机 nonce,用带版本域分隔的 HMAC-SHA256 绑定 device/task/authorization/attempt/generation/nonce;返回 64 位小写十六进制 token,数据库只保存 nonce 和 token SHA-256。服务重启后可重建同一 token;secret 不匹配已有 claim 时启动失败闭合。
|
||
3. 新增显式 `CMBUYER_CLAIM_LEASE_TTL`,必须为正且严格短于授权 TTL。领取和续租只使用服务端 UTC 时间;边界相等视为已过期,没有宽限或隐式续租。
|
||
4. `claim-next` JSON 只接受规范 UUIDv4 的 `session_id`、`claim_request_id`;设备 id 只来自 T-301 认证主体。严格校验 Content-Type、UTF-8、大小、未知字段和额外 JSON。设备认证仍先于请求体读取。
|
||
5. Claim/renew 的有界 SQLite 事务必须先取得写入线性化位置,并以条件更新确认设备仍为 ACTIVE(RowsAffected 必须为 1);只有在此之后才可读取或重放 request、返回 EMPTY/冲突、选择候选或续租。领取再稳定选择最早的 `PENDING + ACTIVE + 未过期 + task/version/规格/数量/总价快照一致` 授权,创建 attempt/claim/request,并原子执行 authorization `ACTIVE→CLAIMED`、task `PENDING→CLAIMED` 且 version+1。任一步行数或约束不符全部回滚;并发设备只能一个成功。SQLite 无行锁,不得用先 SELECT 或先返回 replay 代替该写入线性化。
|
||
6. 同一 claim request 同载荷在响应丢失和服务重启后返回原结果与同一 claim token;无候选也持久化 EMPTY 并稳定重放。同一设备已有未关闭 claim 时,同 session 且租约有效重放原 attempt;不同 session、租约过期或状态异常固定返回需人工处理,不转领、不创建第二个 attempt。
|
||
7. 续租请求绑定 renew_request_id、session、attempt、generation、claim token 和 expected_lease_expires_at。只允许原设备/会话/attempt,当前租约和授权都严格未过期,且 expected 值精确匹配;新到期时间为 `min(server_now + lease_ttl, authorization.expires_at)`。同键同载荷稳定重放,异载荷冲突;续租不改变 token、generation、任务版本或业务状态,过期租约不能复活。
|
||
8. 设备撤销与领取并发必须在线性化位置再次查 ACTIVE:撤销先提交则领取/续租失败,领取先提交后撤销不自动释放 claim。撤销、租约过期、停止轮询均不能证明手机已停止,另一设备不得自动接管;后续 T-207 负责人工安全恢复。
|
||
9. 用统一 claim 所有权校验收紧 T-204 证据存储:设备只能向自己当前 attempt 上传截图;不得因本任务扩大截图 kind、文件类型或隐私边界。
|
||
10. 本任务不新增 heartbeat/events/fail/fence/result,不实现 client HTTP、真机选择器、下单函数、提交订单点击或任何付款动作。claim token 永远不是提交许可,响应不得包含自由动作脚本、坐标、选择器或 `click_permitted`。
|
||
|
||
## 验收要点
|
||
|
||
- Migration 覆盖升级/重开、外键/唯一/partial index、既有事实拒绝升级和有领取事实拒绝降级。
|
||
- 覆盖领取 eligibility、快照一致、稳定排序、事务回滚、两设备并发唯一、同设备单开放 claim、成功/EMPTY/冲突幂等及服务重启重放。
|
||
- 覆盖 HMAC 域隔离、nonce 随机、数据库无明文、错误 secret 启动失败、跨设备/attempt/generation/token 拒绝和常量时间比较。
|
||
- 覆盖续租 CAS、授权到期封顶、无宽限、过期不复活、乱序/并发/同键异载荷,以及续租不改变业务状态。
|
||
- 覆盖撤销并发线性化、认证失败 body 零读取、设备 A 不能上传设备 B attempt 证据。
|
||
- HTTP 错误固定且不泄露 token、SQL、路径或候选任务;静态检查确认没有事件、围栏、提交、付款或页面自动化能力。
|
||
- `go test ./...`、`go test -race ./...`、`go vet ./...`、`go build ./...`、完整 init、上下文校验与 diff-check 全部通过。
|
||
|
||
## 执行记录
|
||
|
||
### 2026-08-04T13:03:26Z · ila
|
||
|
||
2026-08-04 开始 T-302:依赖 T-301、T-203 均已完成,任务定义提交 590c846。实现范围限于服务端原子 claim/renew、可恢复 HMAC claim token、租约 CAS、撤销并发线性化和证据 attempt 所有权收紧;不新增事件、围栏、提交、付款、客户端 HTTP 或真机能力。工作分支 task/t-302-atomic-claim。
|
||
|
||
### 2026-08-04T13:10:15Z · ila
|
||
|
||
2026-08-04 预实现审计收紧撤销线性化:claim/renew 事务必须先取得 SQLite 写入位置并条件确认设备 ACTIVE,之后才允许 request 重放、EMPTY/冲突返回或业务写入;只 SELECT 或先返回 replay 均不成立。同时补充 docs/current-state.md 为显式 write_path,避免实现后共享文档越界。
|
||
|
||
### 2026-08-04T13:11:10Z · ila
|
||
|
||
2026-08-04 write_path 更正:上下文门禁发现 T-103 仍为 DOING 且已拥有共享文档 docs/current-state.md,T-302 同时声明会违反唯一写入者规则,因此不纳入、不修改。T-302 完成事实先记录在本任务;待 T-103 释放路径后由项目级文档同步任务统一更新。撤销线性化 P1 收紧不变。
|
||
|
||
### 2026-08-04T13:15:18Z · ila
|
||
|
||
2026-08-04 预实现审计继续定值:claim 的 CLAIMED/EMPTY/需人工结果与 renew 成功都必须持久化幂等,同键重放不再次 CAS/延长;secret 启动时拒绝与 session 原始值或 key bytes 相同,并拒绝命中任何设备 token hash,所有 claim 逐行重建恒定时复核;证据首次写入要求同设备未关闭 claim,但已成功 upload_key 在 claim 关闭后仍先按原载荷稳定重放。HTTP JSON 上限 4096 bytes;claim 200、EMPTY 204,renew 200;400/413/415 固定错误,401/503 空,409 只用 idempotency_conflict、claim_requires_manual、claim_not_current。
|
||
|
||
### 2026-08-04T14:32:06Z · ila
|
||
|
||
2026-08-04 完成:实现已授权任务原子 claim/renew、可恢复 HMAC claim token、租约 CAS、设备撤销线性化、attempt/generation 四列数据库约束及 evidence claim owner 收紧。三轮独立审计后修复 ORDERING/CLAIMED 状态版本一致性、generation lineage、双 sql.DB 确定性并发测试和 API UUID/token 示例。主 agent 与终审均通过 focused -count=25、go test、race、vet、build、完整 init(client 112 tests)、上下文和 diff-check;未实现 event/fence/submit/payment。
|
||
<!-- END VIKUNJA EXPORT -->
|
||
|
||
## 边界
|
||
|
||
- 只领取 `PENDING` 且存在同版本 `ACTIVE`、未过期、完整快照一致的授权;领取事务必须再次验证
|
||
设备仍为 `ACTIVE`。任何缺失、畸形、并发冲突或存储异常都失败闭合,不能用应用层先读后写代替
|
||
数据库条件更新与唯一约束。
|
||
- 一条授权最多创建一个 attempt;同一设备最多一个未关闭 claim。响应丢失、服务重启、同请求重放、
|
||
续租或同会话恢复均不得递增 generation、轮换 claim token、领取另一任务或创建第二条 attempt。
|
||
- Claim token 是 attempt 归属凭据,不是采购授权,更不是提交许可。明文不得进入 SQLite、日志、错误、
|
||
Git、Vikunja 或测试 fixture;HMAC secret 必须与管理员 session secret、设备 token 分离。错误 secret
|
||
面对已有 claim 时必须拒绝启动,不能签发替代 token。
|
||
- Secret 分离必须由启动检查执行:claim secret 的原始配置或解码 key 不得等于 session secret,
|
||
其 SHA-256 不得命中任何设备 token hash;所有既有 claim(包括以后已关闭的)都必须用当前 secret
|
||
逐条重建并恒定时复核,不能只检查开放 claim,也不能为通过启动而改写旧 hash。
|
||
- 租约和授权边界相等即过期,无宽限。过期租约不得续租、复活、自动关闭 attempt、释放授权或转给
|
||
另一设备;设备撤销、停止轮询和进程退出也不得触发自动接管。安全恢复归 T-207。
|
||
- 设备身份认证仍必须在解析 Content-Type 或读取 body 前完成;claim/renew 事务的第一条数据库业务
|
||
语句还必须先取得 SQLite 写入线性化位置并条件确认设备 `ACTIVE`,之后才允许查询或重放 request、
|
||
返回 EMPTY/冲突或写业务事实。认证、token、session、generation、当前租约或快照任一不匹配均
|
||
不得产生业务写入。
|
||
- 本任务只增加 claim/renew,并按 claim 所有权收紧已有证据上传;不得新增或提前实现 heartbeat、event、
|
||
fail、submission-fence、result、客户端 HTTP 适配、截图 kind 或页面自动化。
|
||
- 证据首次写入必须属于当前认证设备的未关闭 claim;但相同设备与 `upload_key` 已成功落库的同载荷
|
||
在 claim 后续关闭后仍须先按 T-204 原结果稳定重放,不能把关闭 claim 变成幂等契约失效。
|
||
- 不接触拼多多页面判据、规格选择、数量、确认页或真机流程;不编写或引用点击“提交订单”的代码,
|
||
不编写支付、免密支付、先用后付或任何扣款控件代码。响应不得返回自由动作脚本、坐标、选择器或
|
||
`click_permitted`。
|