@sema-agent/server 7.43.0 → 7.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -1,24 +1,11 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
2
  import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
3
3
  import { RETENTION_AUDIT_DEFAULT_LIMIT, RETENTION_AUDIT_MAX_LIMIT } from "../../plugins/retention-lane-store-sql.js";
4
- /** hold 路径。**`*` 不是 `+`**:空段 = 无主桶(见文件头「域的两个边界」)。 */
5
4
  const HOLD_RE = /^\/v1\/ops\/retention\/holds\/([^/]*)$/;
6
5
  const AUDIT_PATH = "/v1/ops/retention/audit";
7
- /** `retention_hold.domain` / `retention_audit.domain` 的列宽(车1 的 DDL:`VARCHAR(190)`)。 */
8
6
  const DOMAIN_MAX_CHARS = 190;
9
- /** hold 备注的上界。`note` 列是 TEXT(不会截断),但一个无界的 operator 输入不该原样进一张**追加式**
10
- * 的治理表 —— 4000 字符足够写清案号与出处,再长的属于工单系统。 */
11
7
  const NOTE_MAX_CHARS = 4000;
12
- /** operator 面的**唯一**授权文案(三条路由同受众、同理由 ⇒ 同一句;分成三句只会让 freeze 表长三倍
13
- * 而消费端读到的判据一模一样)。 */
14
8
  const OPERATOR_ONLY_MESSAGE = "managed-retention governance is an operator-only deployment action";
15
- /**
16
- * 审计游标 = `{id}` 的不透明 base64url。
17
- *
18
- * 🔴 排序键是**自增 `id` 单列**,不是 roster 那样的二元组:`id` 本身全序且唯一,二元组存在的全部理由
19
- * (同毫秒兄弟行无 tiebreak ⇒ 漏页/重页)在这张表上结构性不存在。游标形仍照 roster 逐字同姿势
20
- * (base64url JSON / 判别式解码 / 400 带**类名** detail)—— 消费端跨端点的游标处理代码因此同形。
21
- */
22
9
  function encodeCursor(c) {
23
10
  return Buffer.from(JSON.stringify(c)).toString("base64url");
24
11
  }
@@ -28,8 +15,6 @@ function decodeCursor(raw) {
28
15
  parsed = JSON.parse(Buffer.from(raw, "base64url").toString());
29
16
  }
30
17
  catch (err) {
31
- // 只带错误的**类名**,不回显 payload 片段(与 agents-roster 的同款纪律:被误粘进查询串的凭据
32
- // 不该经由错误文案再走一圈)。
33
18
  return { ok: false, why: `not decodable base64url JSON (${err instanceof Error ? err.name : "unknown error"})` };
34
19
  }
35
20
  if (typeof parsed !== "object" || parsed === null)
@@ -53,8 +38,7 @@ async function handleRetentionBody(req, res, url, ctx, miss) {
53
38
  miss.fell = true;
54
39
  return;
55
40
  }
56
- // 门序 = operator 面的既有口径(routes/adoption.ts 逐字):身份 → 授权 → 能力 → 验型。
57
- const principal = gatedPrincipal(req, deps.config); // direct-door safe:验证过的身份,不是可伪造的头
41
+ const principal = gatedPrincipal(req, deps.config);
58
42
  if (deps.config.requirePrincipal && !principal) {
59
43
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
60
44
  return;
@@ -65,8 +49,6 @@ async function handleRetentionBody(req, res, url, ctx, miss) {
65
49
  }
66
50
  const store = deps.backend?.retentionLane?.();
67
51
  if (!store) {
68
- // ⚠️ 文案里用**单引号**写 `retention:'none'`:文案冻结门的字面提取器不认带反斜杠转义的双引号串
69
- // (`"([^"\n\\]*)"`),一个 `\"` 会让这条站点对那道门隐形 —— 那不是"过了门",是绕过门。
70
52
  sendError(res, 501, "capability.retention_store_required", "managed retention requires a SQL store backend (DB_BACKEND=mysql|pg) — the local/file backend declares retention:'none' and holds nothing to govern");
71
53
  return;
72
54
  }
@@ -76,12 +58,9 @@ async function handleRetentionBody(req, res, url, ctx, miss) {
76
58
  }
77
59
  await handleAuditRead(req, res, store);
78
60
  }
79
- /** 放置(PUT)/ 解除(DELETE)—— 两支共用一条 upsert(见文件头的唯一纪律)。 */
80
61
  async function handleHold(req, res, ctx, store, rawDomain, principal) {
81
62
  const domain = ctx.helpers.safeDecode(rawDomain);
82
63
  if (domain === null) {
83
- // 复用既有的 `request.id_invalid`(sessions/attachments/adoption 同族),不为一条新路由铸第二个
84
- // 同义码 —— 附录 A 判据②:只有消费端真会分支时才铸新码。
85
64
  sendError(res, 400, "request.id_invalid", "invalid retention domain segment");
86
65
  return;
87
66
  }
@@ -94,7 +73,6 @@ async function handleHold(req, res, ctx, store, rawDomain, principal) {
94
73
  const held = req.method === "PUT";
95
74
  let note;
96
75
  if (held) {
97
- // 体是**可选**的(`PUT …/holds/acme` 不带体是合法的最小放置)。空体走 `{}`。
98
76
  let raw;
99
77
  try {
100
78
  raw = await ctx.helpers.readJson(req);
@@ -115,7 +93,6 @@ async function handleHold(req, res, ctx, store, rawDomain, principal) {
115
93
  return;
116
94
  }
117
95
  if (n.length > NOTE_MAX_CHARS) {
118
- // 铸行之前拒:静默截断会把一条**被改写过的**审计事实写进一张追加式的表。
119
96
  sendError(res, 400, "request.field_invalid", "the retention hold note is longer than this face accepts", {
120
97
  detail: `note is ${n.length} characters; the limit is ${NOTE_MAX_CHARS}`,
121
98
  });
@@ -126,12 +103,7 @@ async function handleHold(req, res, ctx, store, rawDomain, principal) {
126
103
  }
127
104
  }
128
105
  const nowMs = Date.now();
129
- const placedBy = principal ?? "operator"; // 单用户 turnkey 部署没有 principal 这个概念(见 explicitOperatorOk)
130
- // 🔴 状态变更与治理审计行走**同一个事务**(codex 对抗复审 R1-[high],验真后修)。两次独立提交下,
131
- // 第二步失败会留下一次**无审计的状态变更** —— DELETE 那一支尤其重:冻结已解除、sweep 从下一拍起就能
132
- // 删这个域的数据,而账上没有任何 `hold_released` 行说明是谁在什么时候解的。
133
- // `mode`/`policyDays` 记的是**这一刻的部署事实**:一条 hold 行不带策略档,事后没人说得清"当时是在
134
- // 什么留存策略下冻结的"。`fencingToken` 诚实留空 —— 它只属于 sweep 轮次,operator 动作不在任何一轮里。
106
+ const placedBy = principal ?? "operator";
135
107
  await store.setHoldAudited({
136
108
  domain,
137
109
  held,
@@ -151,7 +123,6 @@ async function handleHold(req, res, ctx, store, rawDomain, principal) {
151
123
  });
152
124
  sendJson(res, 200, held ? { domain, held: true, placedBy, placedAtMs: nowMs } : { domain, held: false });
153
125
  }
154
- /** 审计读面(keyset 分页;窗与游标语义见文件头)。 */
155
126
  async function handleAuditRead(req, res, store) {
156
127
  const q = new URL(req.url ?? "", "http://x").searchParams;
157
128
  const limitRaw = q.get("limit");
@@ -163,8 +134,6 @@ async function handleAuditRead(req, res, store) {
163
134
  const beforeRaw = q.get("before");
164
135
  const decoded = beforeRaw === null ? undefined : decodeCursor(beforeRaw);
165
136
  if (decoded !== undefined && !decoded.ok) {
166
- // 文案冻结在 wire 上(api-error-text-freeze 的一行);`detail` 是 additive 的**为什么**,
167
- // 供调用方自诊(它描述的是调用方自己传进来的串,零跨租户信息)。
168
137
  sendError(res, 400, "request.query_invalid", "before must be the opaque `nextBefore` cursor from a previous page of this endpoint", { detail: decoded.why });
169
138
  return;
170
139
  }
@@ -173,7 +142,6 @@ async function handleAuditRead(req, res, store) {
173
142
  sendError(res, 400, "request.query_invalid", `domain must be at most ${DOMAIN_MAX_CHARS} characters`);
174
143
  return;
175
144
  }
176
- // 多取一行用来判「还有没有下一页」——不进结果。`hasMore` 因此是**事实**而不是"limit 满了就猜有"。
177
145
  const scanned = await store.listAudit({
178
146
  ...(domainRaw !== null ? { domain: domainRaw } : {}),
179
147
  limit: Math.min(limit + 1, RETENTION_AUDIT_MAX_LIMIT + 1),
@@ -2,25 +2,15 @@ import { z } from "zod";
2
2
  import { sendJson, sendError } from "../send.js";
3
3
  import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
4
4
  import { createLogger } from "../../observability/logger.js";
5
- // 等待秒数的单一真源在车道:它同时被「放回认领时判这张票还撑不撑得过这段窗」用到(见那侧头注)。
6
- // scope 的 wire 形也在车道(列举列 / 查询参数 / 删除体 / 游标四处同一份表示,见那侧头注)。
7
5
  import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
8
6
  const logger = createLogger();
9
7
  export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
10
8
  export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
11
- /** 撤销面两口共用的字面路径(GET 列举 / DELETE 撤销)。 */
12
9
  export const RULES_PATH = "/v1/rules";
13
- /** 一页的上下限。50 = 一屏治理列表;200 = 一次响应的上界(一条规则行带 `adds[]`,不是一个标量)。
14
- * 与 `GET /v1/sessions` 的 keyset 姿势同族(sessions-list.ts:137 的 `Math.min(100, …)`),数值按本面
15
- * 的行宽另定 —— 抄一个数比抄一条理由更容易漂。 */
16
10
  const RULES_PAGE_DEFAULT = 50;
17
11
  const RULES_PAGE_MAX = 200;
18
- /** 一层 settings 的上限。一份 CC settings 是人手写的 JSON,不是数据集;16 KiB 宽到装得下几百条 allow,
19
- * 窄到一次请求不能拿它当上传通道。层数同理(core v1 只读三层,给 3 就够,多给等于给一个放大面)。 */
20
12
  const MAX_LAYER_CONTENT_CHARS = 16_384;
21
13
  const MAX_LAYERS = 3;
22
- /** 🔴 边界必 schema(宪法 [2704])。`.strict()`:多给一个键 = 400,不是静默忽略 —— 一个把 `content`
23
- * 拼错成 `contents` 的客户端应该当场知道,而不是拿到一份「零候选」的预览去纳闷。 */
24
14
  const CcImportPrepareBodySchema = z
25
15
  .object({
26
16
  layers: z
@@ -28,12 +18,6 @@ const CcImportPrepareBodySchema = z
28
18
  .object({
29
19
  layer: z.enum(["userSettings", "projectSettings", "localSettings"]),
30
20
  path: z.string().min(1).max(1024),
31
- /** 项目层规则的作用域根。**用户自己的**目录标签(它只影响这条规则将来在哪些 cwd 下生效),
32
- * 故不做**真实性**校验——server 上没有那棵树可对照,假装校验过才是谎。
33
- * 🔴 但**非空**是硬的(codex 交叉复审 round5 [high] 一,验真后修):`root: ""` 会被 core 直接
34
- * 变成 `{kind:"project", root:""}`,而空前缀在 `pathWithinRoot` 下**包含每一个 cwd** —— 一条
35
- * 项目内规则就此悄悄升成全局规则;更糟的是本仓的读面对空 root 是整桶拒,于是一次「成功」的
36
- * 导入会让这位租户**之后每一次**规则读写都失败。 */
37
21
  root: z.string().min(1).max(1024),
38
22
  content: z.string().max(MAX_LAYER_CONTENT_CHARS),
39
23
  })
@@ -43,13 +27,6 @@ const CcImportPrepareBodySchema = z
43
27
  })
44
28
  .strict();
45
29
  const CcImportRedeemBodySchema = z.object({ ticket: z.string().min(1).max(190) }).strict();
46
- /** `DELETE /v1/rules` 的体。`.strict()` 同族理由:把 `scope` 拼错成 `scopes` 的客户端应该当场知道,
47
- * 而不是拿到一个「删掉了 global 那条」的意外结果。`principal` = operator 越权域(缺席 = 删自己的)。
48
- * 长度上限与 principal 入口上限同源(`PRINCIPAL_MAX_LENGTH = 190`,security.ts);`rule` 的 1024 覆盖
49
- * core 的 `MAX_RULE_TEXT_CHARS`(512,铸侧真上界)且留足余量。
50
- * 🔴 `scope` 的上限**不再是本文件自己挑的数**(A-054.24):改由铸侧的尺派生 —— 见
51
- * {@link MAX_RULE_SCOPE_CHARS} 顶注(此前独立写死 1088,而铸侧能落到 4096+,于是超限 root 的规则
52
- * 「列得出、删不掉」)。撤销口的收窄若严于铸侧,等于造一条**不可撤的常驻放行规则**,方向是错的一侧。 */
53
30
  const RuleRevokeBodySchema = z
54
31
  .object({
55
32
  rule: z.string().min(1).max(1024),
@@ -63,13 +40,6 @@ const RulesCursorSchema = z
63
40
  function encodeRulesCursor(c) {
64
41
  return Buffer.from(JSON.stringify(c)).toString("base64url");
65
42
  }
66
- /**
67
- * 读不出形 ⇒ `{ok:false, why}`,由调用方拒(400)。两条纪律:
68
- * · **刻意不**「读不出就当第一页」(sessions-list 的旧姿势):那条腿的游标只影响顺序,这条腿的游标
69
- * 一旦被静默丢弃,客户端会以为自己列完了整份清单。
70
- * · 失败**带着原因出来**而不是塌成一个 `undefined`:调用点把 `why` 写进服务端日志。一个只回默认值的
71
- * catch 是本仓在册的静默降级形(#191 门② SHAPE C)——这里不需要那种便利,原因本来就有用。
72
- */
73
43
  function decodeRulesCursor(raw) {
74
44
  try {
75
45
  const parsed = RulesCursorSchema.safeParse(JSON.parse(Buffer.from(raw, "base64url").toString()));
@@ -79,10 +49,6 @@ function decodeRulesCursor(raw) {
79
49
  return { ok: false, why: err instanceof Error ? err.message : String(err) };
80
50
  }
81
51
  }
82
- /** 排序键 —— 与 `RuleConsentLane.listRules` 的 (scope, rule) 二元排序**等价**(两处若各写一份必然漂,
83
- * 而漂的表现是分页静默漏行)。分隔符必须**小于**任何可能出现的字符,否则串比较与元组比较会在
84
- * 「一个 scope 是另一个的前缀」处分岔(`project:/a` vs `project:/ab`)。用空格是错的 ——
85
- * `Bash(echo hi)` 里就有一个;\u0000 是码点最小的字符,而且规则文本与路径里都不可能出现它。 */
86
52
  const rowKey = (r) => `${r.scope}\u0000${r.rule}`;
87
53
  export async function handleRules(req, res, url, ctx) {
88
54
  const miss = { fell: false };
@@ -94,22 +60,14 @@ async function handleRulesBody(req, res, url, ctx, miss) {
94
60
  const { readJson, rateLimited } = ctx.helpers;
95
61
  const isPrepare = req.method === "POST" && url === RULES_CC_IMPORT_PREPARE_PATH;
96
62
  const isRedeem = req.method === "POST" && url === RULES_CC_IMPORT_REDEEM_PATH;
97
- // 🔴 判路写**字面量**而不是引 `RULES_PATH`(与 server.ts 的 `ROUTE_LABEL_LITERALS` 那条注同族理由):
98
- // 路由名册门与文案接线门的枚举器扫的是 `url === "…"` 的**源码文本**,引常量会让这两口对两道门隐形
99
- // ——那不是「过了门」,是绕过门。常量仍导出给测试/SDK 侧当单一真源用。
100
63
  const isList = req.method === "GET" && url === "/v1/rules";
101
64
  const isRevoke = req.method === "DELETE" && url === "/v1/rules";
102
65
  if (!isPrepare && !isRedeem && !isList && !isRevoke) {
103
66
  miss.fell = true;
104
67
  return;
105
68
  }
106
- // 🔴 `prepare` 每次调用**都会**落一条 pending 审批记录 + 一张票 —— 它是本域唯一一条「一次请求 = 一行」
107
- // 的写面,没有限流就是一条行洪口。与 `POST /v1/tasks/:id/asks/:askId/decision` 同姿势(限流在最前)。
108
- // 撤销面两口同罩:`DELETE` 是写(每次铸一个 dot + 一条墓碑),`GET` 是整桶读 —— 两者都不该无限打。
109
69
  if (rateLimited(req, res))
110
70
  return;
111
- // 鉴权**先于**可用性(501-before-auth 先例的反面同理):这四口没有可被探测的部署事实,但
112
- // principal 是本域的全部授权基础,先立住它再谈别的。
113
71
  const principal = gatedPrincipal(req, deps.config);
114
72
  if (principal === undefined) {
115
73
  sendError(res, 401, "auth.unauthorized", "unauthorized");
@@ -120,8 +78,6 @@ async function handleRulesBody(req, res, url, ctx, miss) {
120
78
  sendError(res, 501, "capability.rule_store_required", "this deployment has no permission-rule store wired");
121
79
  return;
122
80
  }
123
- // ── 撤销面两口(#203 §2)──────────────────────────────────────────────────────────────────────
124
- // 放在读体之前:`GET` 没有体,`readJson` 对它是一次白等。
125
81
  if (isList) {
126
82
  await handleRuleList(req, res, principal, lane, deps.config.operatorPrincipals);
127
83
  return;
@@ -146,9 +102,6 @@ async function handleRulesBody(req, res, url, ctx, miss) {
146
102
  }
147
103
  const prepared = await lane.prepareImport(principal, parsed.data.layers);
148
104
  if (!prepared.ok) {
149
- // 413(不是 400):族语义就是「体/条目超上限」,与附件/图片同码同状态(附录 A 已成文)。
150
- // 帽的位置与理由(为什么单层字节帽不够、为什么帽在**铸票之前**)见 `rules-consent.ts` 的
151
- // `MAX_IMPORT_CANDIDATES`。
152
105
  sendError(res, 413, "request.payload_too_large", "too many allow entries across the submitted settings layers");
153
106
  return;
154
107
  }
@@ -163,31 +116,16 @@ async function handleRulesBody(req, res, url, ctx, miss) {
163
116
  const redeemed = await lane.redeemImport(principal, parsed.data.ticket);
164
117
  if (!redeemed.ok) {
165
118
  logger.info("rule import ticket refused", { reason: redeemed.reason, detail: redeemed.detail, retryable: redeemed.retryable === true });
166
- // 🔴 codex 交叉复审 round2 [medium](验真后修):**可重试**那一支不能折进 404。
167
- // 为什么这不破坏零存在性 oracle:走到这一支的调用方**已经赢下了一次 principal 绑定的认领** ——
168
- // 也就是说他已经证明了这张票是他自己的、没过期、没用过。对他说「稍后再试」泄不出任何**别人**的
169
- // 存在性(伪造 / 别人的 / 过期 / 已用过 四类仍然同形 404,一个字都没变)。反过来把它折成 404 才是
170
- // 真损失:round1 修好的「认领已放回、这张票还能用」在 wire 上被抹掉,客户端按常规 404 处置一丢了之。
171
119
  if (redeemed.retryable === true) {
172
- res.setHeader("retry-after", String(RULE_IMPORT_RETRY_AFTER_SEC)); // 体里有等待提示 ⇒ 标准头同值(同 503 族先例)
120
+ res.setHeader("retry-after", String(RULE_IMPORT_RETRY_AFTER_SEC));
173
121
  sendError(res, 503, "state.rule_import_retry", "the rule import could not be completed right now; the ticket is still usable — retry", { retryAfterSec: RULE_IMPORT_RETRY_AFTER_SEC });
174
122
  return;
175
123
  }
176
- // 同形 404(顶注):四类成因**不上 wire**,只进服务端日志。
177
124
  sendError(res, 404, "not_found.rule_ticket", "no such import ticket");
178
125
  return;
179
126
  }
180
127
  sendJson(res, 200, { result: redeemed.result });
181
128
  }
182
- /**
183
- * 解析**目标 principal**(自有域 vs operator 越权域)。
184
- *
185
- * 🔴 判据链是硬的:①没给 = 自己(最常见的路,零授权提问);②给了但等于自己 = 还是自己(一个把
186
- * principal 显式写出来的客户端不该因此被要求当 operator);③给了别人 = **必须** `explicitOperatorOk`。
187
- * 第三条用的是 `explicitOperatorOk` 而不是 `isOperator`:后者在 `OPERATOR_PRINCIPALS` 为空时「人人
188
- * 都是 operator」,那在跨租户读规则这条轴上是一道世界可写的门(principal-gate.ts 的头注逐字)。
189
- * **绝不**从 service token 推断:一个共享的部署凭证不是一个人,而这条口的授权对象是人。
190
- */
191
129
  function resolveTargetPrincipal(requested, caller, operatorPrincipals) {
192
130
  if (requested === undefined || requested === caller)
193
131
  return { ok: true, principal: caller };
@@ -195,19 +133,14 @@ function resolveTargetPrincipal(requested, caller, operatorPrincipals) {
195
133
  return { ok: false };
196
134
  return { ok: true, principal: requested };
197
135
  }
198
- /** `GET /v1/rules` —— 列出一位 principal 名下**活着**的规则。 */
199
136
  async function handleRuleList(req, res, caller, lane, operatorPrincipals) {
200
- // `url` 在派发前已被剥掉 query,查询参数只能从 `req.url` 重新取(与 `/v1/outcomes` 同姿势)。
201
137
  const q = new URL(req.url ?? "/", "http://localhost").searchParams;
202
138
  const target = resolveTargetPrincipal(q.get("principal") ?? undefined, caller, operatorPrincipals);
203
139
  if (!target.ok) {
204
140
  sendError(res, 403, "auth.operator_only", "reading another principal's rules is an operator-only action");
205
141
  return;
206
142
  }
207
- // limit:非数/越界一律夹取(与 sessions-list 同姿势——一个分页参数写错不该让治理面整个打不开)。
208
143
  const limit = Math.min(RULES_PAGE_MAX, Math.max(1, Number(q.get("limit") ?? RULES_PAGE_DEFAULT) || RULES_PAGE_DEFAULT));
209
- // scope 过滤:**给了就必须读得出形**。读不出不当「没过滤」——那会把一次「只看这个项目」的请求
210
- // 悄悄变成一次全量列举,而客户端会照单全收当成该项目的清单。
211
144
  const scopeRaw = q.get("scope");
212
145
  const scopeFilter = scopeRaw === null ? undefined : parseRuleScope(scopeRaw);
213
146
  if (scopeRaw !== null && scopeFilter === undefined) {
@@ -222,15 +155,9 @@ async function handleRuleList(req, res, caller, lane, operatorPrincipals) {
222
155
  return;
223
156
  }
224
157
  const cursor = decoded?.ok === true ? decoded.cursor : undefined;
225
- // 过滤键用**规范形**(`serializeRuleScope` 的产物),不是客户端交上来的原串:行上的 scope 是规范的,
226
- // 拿一个等价但拼法不同的原串去比会静默过滤成空表。空串 = 未过滤(游标里也用这一个表示)。
227
158
  const filterKey = scopeFilter === undefined ? "" : serializeRuleScope(scopeFilter);
228
159
  const listing = await lane.listRules(target.principal);
229
160
  if (cursor !== undefined && (cursor.rev !== listing.rev || cursor.principal !== target.principal || cursor.scope !== filterKey)) {
230
- // 游标绑 (rev, principal, scope)(见 {@link RulesCursor} 头注)。**拒**而不是从头给:静默重置会让
231
- // 客户端把第二页当成「接着上一页」,治理面上于是出现一份既漏行又重行的清单,而没有任何人知道。
232
- // 三项合并成一句文案是刻意的:对客户端来说处置完全相同(丢掉游标从头列),而把「你换了 principal」
233
- // 单独成句会让一个 operator 从别人桶的 rev 上读出信息。
234
161
  sendError(res, 400, "request.query_invalid", "the rules cursor was minted at a different revision — restart the listing", { rev: listing.rev });
235
162
  return;
236
163
  }
@@ -242,7 +169,6 @@ async function handleRuleList(req, res, caller, lane, operatorPrincipals) {
242
169
  : undefined;
243
170
  sendJson(res, 200, { rules: page, rev: listing.rev, ...(nextCursor !== undefined ? { nextCursor } : {}) });
244
171
  }
245
- /** `DELETE /v1/rules` —— 按内容(rule, scope)撤销。 */
246
172
  async function handleRuleRevoke(res, rawBody, caller, lane, operatorPrincipals) {
247
173
  const parsed = RuleRevokeBodySchema.safeParse(rawBody);
248
174
  if (!parsed.success) {
@@ -256,38 +182,20 @@ async function handleRuleRevoke(res, rawBody, caller, lane, operatorPrincipals)
256
182
  }
257
183
  const target = resolveTargetPrincipal(parsed.data.principal, caller, operatorPrincipals);
258
184
  if (!target.ok) {
259
- // 与读那一半**同码同姿势**:方向(收紧)决定了这条越权域存在,身份判据不因方向而放松。
260
185
  sendError(res, 403, "auth.operator_only", "revoking another principal's rules is an operator-only action");
261
186
  return;
262
187
  }
263
188
  const outcome = await lane.removeRule({ principal: target.principal, rule: parsed.data.rule, scope });
264
189
  switch (outcome.status) {
265
190
  case "removed":
266
- // 🔴 `stillLive` **必须上 wire**(design/203 v2 §6 F3)。它为真的意思是「墓碑落了,但这次调用
267
- // 期间又落了一次新的批准,规则按 add-wins 仍然活着」——把它折进 200 不说,客户端会把一次
268
- // **没有完成**的撤销读成完成,而那正是治理面最不能出错的地方。
269
191
  sendJson(res, 200, { status: "removed", rev: outcome.rev, stillLive: outcome.stillLive });
270
192
  return;
271
193
  case "no-op": {
272
- // 幂等:重复删 / 本来就没有,一律 200 `no-op`。**不是 404** —— 一个「这条规则存在过吗」的
273
- // 404 就是一台存在性 oracle(operator 越权域下更是跨租户的那种),而删除对「有没有过」
274
- // 本来就不该作 oracle。客户端要判「我删到东西了吗」,读 `status` 即可。
275
- //
276
- // 🔴 `stillLive` 这一支**也要有**(codex 对抗复审 R1-F2,验真后修)。core 的原语在初始快照里
277
- // 找不到匹配 dot 时**立刻**回 no-op —— 它不做 removed 那支的读回。于是「快照之后、应答之前落了
278
- // 一次批准」的窗口里,规则**是活的**而端点说 no-op,客户端合理地读成「已经没有这条规则了」。
279
- // 那与 F3 修好的 removed+stillLive 是**同一个**危害,只是走了另一支。
280
- // 处置 = server 自己补一次读回,把**观察到的**事实如实上 wire;两支 200 的键集因此同形,消费端
281
- // 一套分支读得通。读回失败不改判(no-op 本身是 core 裁定的)⇒ 保守报 `stillLive:false`?**不**:
282
- // 那是编一个结论。读不出来就让它抛,由本域的全局错误臂报 500 —— 一个说不清结果的撤销不该回 200。
283
- // 让 core 的原语自己报这一位是更根治的形,已作为上游请托登记(server 不改上游语义,[2862])。
284
194
  const stillLive = (await lane.listRules(target.principal)).rows.some((r) => r.rule === parsed.data.rule && r.scope === serializeRuleScope(scope));
285
195
  sendJson(res, 200, { status: "no-op", rev: outcome.rev, stillLive });
286
196
  return;
287
197
  }
288
198
  case "failed":
289
- // core 对 `failed` 的承诺是「读回确认什么都没写」或「读回本身也失败了,结局不定」——两种都是
290
- // **重试可改判**的,所以 503 而不是 500。理由与 `state.rule_import_retry` 同族。
291
199
  logger.info("rule revoke failed", { error: outcome.error });
292
200
  sendError(res, 503, "state.rule_remove_failed", "the rule could not be revoked right now — retry, then reconcile with GET /v1/rules");
293
201
  return;