Files
qinglong/docs/adr/ADR-0372-two-phase-authenticated-local-run-cancellation-api.md
T

49 lines
7.6 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.
# ADR-0372:两阶段认证的 Local Run Cancellation API
- 状态:Proposed
- 日期:2026-08-11
- 关联:QL-RFC-0001 D-18、D-19、D-44、D-279、D-280、D-282、D-283、D-284ADR-0118、ADR-0119、ADR-0367、ADR-0371
## 背景
D-283 后,Local `/api/v3` 已能按 Project 读取 Run、列表、RunEvent 与 StepRun,但仍不能停止一个正在执行的通用 Run。Cluster 已有 `run.cancel`Local 只有内部 execution control 与 Package Workflow 专用管理入口;用户因此能看见阻塞点,却必须退回旧控制面或专用命令才能取消。直接把 HTTP 请求转给 execution lifecycle 会绕过 durable intent、Project/RoleBinding fence 与重启恢复;直接复用 Package Workflow administration 又会错误绑定 Package/Workflow identity。
现有 Local HTTP transport 还假定全部 operation 都是无 body GET,并在 admission 前拒绝 body。取消需要一个极小 JSON body,但不能因此退化为“先缓冲匿名 body,再鉴权”的单阶段 Controller;低配路由器与安全边界都要求 route、Bearer、Policy 和持久 Audit 成功后才读取 body。
## 决策
1. Local 增加 `POST /api/v3/projects/{projectId}/runs/{runId}/cancellation`,固定 operation `run.cancel`、permission `run.stop`,不得接受 query。成功与 Cluster 同构:首次 durable intent 返回 202exact replay/已请求/已终态返回 200;不存在和跨 Project 统一 404 `run_not_found`,授权 fence 漂移返回 409,存储不可用返回 503。
2. 通用 wire schema 改为 `qinglong/run-cancellation@v1`body 只允许 exact `{schema,mutationId}`。Runtime Core 新增 profile-neutral `run-cancellation` export;现有 `cluster-run-cancellation` export 作为 3.0 Alpha 源码兼容别名保留,但不再定义 Cluster 专属 wire identity。
3. Local admission 升级为两阶段:canonical route resolution、Bearer authentication、Project Policy、持久 security audit、credential/Pepper confirm 全部完成后,transport 才读取 body。取消 body 固定最多 512 bytes、只接受单个 `application/json`、拒绝 transfer-encoding、重复/畸形 content-length、空 body、尾随 JSON、非 UTF-8 与超量;GET 继续不接受 body且不支付 JSON parser 成本。
4. Local SQLite 增加通用 Run cancellation repository,并复用 Runtime Database 已打开的唯一 `LocalSqliteOperationAuthority`。单个 `BEGIN IMMEDIATE` 内重新验证 Project active/version、调用主体最新 RoleBinding active/version/roleowner/admin/operator),验证 Run 的 Project 归属,再原子更新 `cancel_requested_at_ms='now'``cancel_reason='user'`、Run version/event sequence 并追加低敏 `run.cancel_requested` Event。
5. `mutationId` 进入每 Run 唯一的 `user-cancel:{mutationId}` dedupe key;事件 id 与时间由服务端产生。响应丢失后的同 mutation 重放不得追加 Event 或再次递增版本;Run 已存在任何合法 cancellation intent 时只返回 `already_requested`,不得覆盖 reason/time;终态只返回 `already_terminal`
6. repository 只在启用 Local API product surface 时惰性构造,并与 Run/StepRun reader 共用同一 SQLite connection、queue 与 close fence。不得把 mutation method 放入只读 Run reader 或 MCP database,也不得让 HTTP route取得 execution controller/PID/signal authority;后续有界 execution-control lifecycle 从 durable intent 收敛实际进程。
7. MCP 继续保持只读,不增加 `qinglong.run.cancel`。AI 写操作仍必须等待独立 preview/Approval/Audit 产品契约,不能因 HTTP 已有 User cancellation 就取得等价 authority。
8. 不新增 workspace package、第三方依赖、migration、index、数据库连接、listener、sidecar、timer、watcher 或 cache。D-281 的 flash/module/RSS 门不得放宽;完整 backend/packages、14 artifact、默认 Local image、真实 Edge/Standalone Local contract 与 PostgreSQL HA 门必须继续通过。
## 不采用方案
- **复用 Local execution controller**signal 是 intent 之后的收敛副作用,不是认证后的 durable command 边界。
- **复用 Plugin Package Workflow cancellation**:通用 Run 不一定来自 Package Workflow,额外 identity 会把合法目标错误遮蔽。
- **在鉴权前读取 JSON**:匿名请求可消耗 body 内存和 parser CPU,也破坏 Local/Cluster 的安全时序一致性。
- **用 query/header 传 mutationId**:规避 body 只会制造第二套 wire contract,且难以扩展显式 schema version。
- **开放 MCP cancellation**:当前 MCP 是低风险只读诊断面,没有 preview/Approval 消费与写操作强认证闭环。
- **新增 cancellation package 或 SQLite 表**:能力属于既有 Run domain,当前 Runs/RunEvents、Project/RoleBinding 和索引已足够。
## 完成门
- Runtime Core 覆盖 profile-neutral canonical schema、严格 body/result、compat export 与非法状态;
- Local admission/transport 覆盖认证与持久审计早于 body、512-byte hard cap、content-type/content-length/UTF-8/JSON 严格性、GET 零 body parser
- SQLite repository 覆盖 accepted、response-loss replay、already-requested、already-terminal、跨 Project、Role revoke/version drift、counter overflow、Event collision、rollback 与 bounded authority queue
- 真实 SQLite HTTP 覆盖 Owner/Operator allow、Viewer deny、credential confirm、durable Run/Event、取消 lifecycle 收敛与重启后可观察状态;Linux 虚拟化组合门必须在同一进程链证明 API→intent→真实 PID stop,固定物理设备报告仍独立采集;
- Cluster canonical schema、完整源码、制品、Local image 与 PostgreSQL HA 门全绿,且 MCP Tool 清单和默认 Edge/Standalone import closure 不获得写 authority。
## 当前验证证据
- Runtime Core 476/476、Local SQLite 213/213、Local API 32/32、Local MCP 41/41、Local Application 45 pass/4 skip、Cluster Control 196 pass/2 skip;完整 18-package clean build/test 退出 0backend 1,160 pass/2 skip。
- workspace 保持 18 package1,028 个 source 中 1,010 个位于包内领域子目录,18 个根文件均为 public/binary entry`singleSourcePackages=[]``shallowSourcePackages=[]`package/dependency boundary 无 finding。
- 14 个 Profile artifact 全部 compatible。默认 Edge/Standalone 仅包含 SQLite/Runtime Core/SemVer,为 3,694,042/3,694,096 bytes、375 files、50 loaded modulesAPI 组合为 5,113,425/5,113,569 bytes。最紧 Application+AI 为 6,281,428/6,281,560 bytes,距 6 MiB 只余 10,028/9,896 bytes,后续增量必须先恢复包内可达文件裁剪余量,不得提高 cap。
- AI/API-excluded arm64 Local image 为 478 files/4,717,459 bytesEdge 128 MiB/64 PIDs 与 Standalone 256 MiB/256 PIDs 均在只读根、无网络、非 root 条件下 active→graceful stopSQLite integrity 为 `ok`
- PostgreSQL 18.4 arm64 HA 112/112 gates、timeline 1→2;私有报告 SHA-256 为 `8416a26aa6220210961a40e22aec897215a55e59423ad48638ca203a8cb488e6`,离线审计 `compatible=true/findings=[]`Docker 资源零残留。
- D-299/ADR-0387 已把相邻证据收敛为同一真实 arm64 Linux 链:可选 `edge/standalone-application-api` 制品经 HTTP `task.start` 拉起真实子进程,再经 HTTP cancellation 写入唯一 durable intent/Eventexact replay 不重复,execution-control 收敛 `run/attempt=cancelled``/proc/<pid>` 的精确 start identity 消失,重启后仍可读 cancelled。Edge 128 MiB/64 PIDs 与 Standalone 256 MiB/256 PIDs 报告均通过独立离线审计;两份报告明确标记 `physicalDevice:false`,因此不能替代固定型号低配路由器上的物理报告,本 ADR 保持 Proposed。