@sema-agent/server 7.138.0 → 7.139.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 (215) hide show
  1. package/MIGRATION.md +276 -7
  2. package/README.md +7 -4
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +73 -29
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/lib/lanes.json +3 -2
  7. package/dist/adoption/wire.d.ts +3 -3
  8. package/dist/approval-card.d.ts +20 -20
  9. package/dist/bench/s1/arms.d.ts +1 -1
  10. package/dist/bench/s1/arms.js +1 -3
  11. package/dist/bench/s1/run-firm.js +2 -2
  12. package/dist/boot/device-lane.d.ts +8 -3
  13. package/dist/boot/device-lane.js +7 -2
  14. package/dist/boot/execution-env.d.ts +4 -2
  15. package/dist/boot/execution-env.js +16 -24
  16. package/dist/boot/execution-lane.d.ts +1 -1
  17. package/dist/boot/governance-seams.d.ts +3 -2
  18. package/dist/boot/governance-seams.js +9 -11
  19. package/dist/boot/memory-consolidation.js +2 -2
  20. package/dist/boot/resolve-spec.d.ts +1 -1
  21. package/dist/boot/resolve-spec.js +8 -9
  22. package/dist/boot/retention-lane.d.ts +5 -5
  23. package/dist/boot/retention-lane.js +0 -6
  24. package/dist/boot/runner-deps.d.ts +3 -1
  25. package/dist/boot/runner-deps.js +2 -1
  26. package/dist/boot/session-faces.d.ts +0 -3
  27. package/dist/boot/session-faces.js +15 -8
  28. package/dist/boot/stage-02-web-admin-pull.js +2 -2
  29. package/dist/boot/stage-05-execution-env.js +2 -2
  30. package/dist/boot/stage-06-runners.js +0 -1
  31. package/dist/boot/stage-09-leader.js +1 -2
  32. package/dist/boot/stage-10-http-server.js +2 -2
  33. package/dist/boot/stores.js +4 -4
  34. package/dist/boot/web-admin.js +2 -2
  35. package/dist/capabilities/mcp-probe.js +1 -1
  36. package/dist/capabilities/scenarios.d.ts +1 -1
  37. package/dist/capability-facts.d.ts +15 -7
  38. package/dist/capability-facts.js +8 -3
  39. package/dist/config-catalog.js +7 -7
  40. package/dist/config-invariants.d.ts +23 -3
  41. package/dist/config-invariants.js +42 -0
  42. package/dist/config-provider.d.ts +19 -24
  43. package/dist/config-provider.js +27 -12
  44. package/dist/config-types.d.ts +10 -5
  45. package/dist/config.d.ts +33 -0
  46. package/dist/config.js +44 -15
  47. package/dist/delegated-session-owner.d.ts +2 -2
  48. package/dist/deployment-shape.d.ts +1 -1
  49. package/dist/device-enrollment.d.ts +41 -2
  50. package/dist/device-enrollment.js +27 -12
  51. package/dist/device-store.d.ts +58 -85
  52. package/dist/device-store.js +19 -49
  53. package/dist/device-ws-hub.js +2 -1
  54. package/dist/env-name-allowlist-knobs.d.ts +5 -4
  55. package/dist/exec-cut.d.ts +60 -0
  56. package/dist/exec-cut.js +36 -0
  57. package/dist/execution-lane-caps.d.ts +19 -0
  58. package/dist/execution-lane-caps.js +10 -0
  59. package/dist/execution-placement.d.ts +259 -0
  60. package/dist/execution-placement.js +199 -0
  61. package/dist/field-conflict.d.ts +3 -2
  62. package/dist/field-conflict.js +1 -0
  63. package/dist/hosted-posture.d.ts +2 -2
  64. package/dist/http/admission.js +8 -19
  65. package/dist/http/checkpoint-ctx.d.ts +21 -12
  66. package/dist/http/checkpoint-ctx.js +4 -2
  67. package/dist/http/claimed-run.d.ts +1 -1
  68. package/dist/http/durable-run-leg.js +9 -5
  69. package/dist/http/execution-context.d.ts +218 -0
  70. package/dist/http/execution-context.js +330 -0
  71. package/dist/http/resume-legs.js +50 -20
  72. package/dist/http/route-ctx.d.ts +14 -8
  73. package/dist/http/routes/background.d.ts +29 -1
  74. package/dist/http/routes/background.js +29 -2
  75. package/dist/http/routes/devices.d.ts +13 -19
  76. package/dist/http/routes/devices.js +21 -26
  77. package/dist/http/routes/session-sync.js +110 -15
  78. package/dist/http/routes/sessions.d.ts +6 -0
  79. package/dist/http/routes/sessions.js +115 -11
  80. package/dist/http/routes/side-query.d.ts +12 -4
  81. package/dist/http/routes/side-query.js +4 -5
  82. package/dist/http/routes/tasks.js +37 -31
  83. package/dist/http/server.d.ts +35 -19
  84. package/dist/http/server.js +11 -0
  85. package/dist/http/session-inherit-env.d.ts +3 -4
  86. package/dist/http/session-inherit-env.js +1 -1
  87. package/dist/http/wire-types.d.ts +8 -12
  88. package/dist/index.d.ts +1 -0
  89. package/dist/memory-sync-client.d.ts +53 -14
  90. package/dist/memory-sync-client.js +196 -116
  91. package/dist/memory-sync.d.ts +1 -1
  92. package/dist/memory-sync.js +3 -2
  93. package/dist/observability/cost-taxonomy.d.ts +2 -2
  94. package/dist/observability/fail-open.d.ts +12 -0
  95. package/dist/observability/fail-open.js +12 -0
  96. package/dist/observability/run-terminal.js +0 -1
  97. package/dist/orchestration/workflow-notify-journal.js +4 -4
  98. package/dist/org-memory-admission.d.ts +4 -3
  99. package/dist/org-memory-admission.js +6 -6
  100. package/dist/parked-decide.d.ts +34 -9
  101. package/dist/parked-decide.js +12 -19
  102. package/dist/parked-revive-surface.d.ts +12 -15
  103. package/dist/parked-revive-surface.js +3 -6
  104. package/dist/peer-seat.d.ts +13 -7
  105. package/dist/peer-seat.js +37 -3
  106. package/dist/plugins/approval-ask-store-file.d.ts +7 -0
  107. package/dist/plugins/approval-ask-store-file.js +11 -0
  108. package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
  109. package/dist/plugins/approval-ask-store-memory.js +12 -0
  110. package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
  111. package/dist/plugins/approval-ask-store-sql.js +30 -12
  112. package/dist/plugins/caching-session-store.d.ts +6 -2
  113. package/dist/plugins/caching-session-store.js +9 -3
  114. package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
  115. package/dist/plugins/checkpoint-store-sql.js +5 -7
  116. package/dist/plugins/device-store-sql.d.ts +25 -51
  117. package/dist/plugins/device-store-sql.js +49 -103
  118. package/dist/plugins/execution-placement-sql.d.ts +151 -0
  119. package/dist/plugins/execution-placement-sql.js +223 -0
  120. package/dist/plugins/file-history-store-sql.d.ts +7 -6
  121. package/dist/plugins/file-history-store-sql.js +5 -5
  122. package/dist/plugins/file-run-store.d.ts +6 -1
  123. package/dist/plugins/file-run-store.js +4 -0
  124. package/dist/plugins/fork-routing-session-store.d.ts +6 -0
  125. package/dist/plugins/fork-routing-session-store.js +16 -0
  126. package/dist/plugins/local-session-store.d.ts +149 -50
  127. package/dist/plugins/local-session-store.js +382 -107
  128. package/dist/plugins/memory-key-guards.d.ts +4 -0
  129. package/dist/plugins/memory-key-guards.js +1 -0
  130. package/dist/plugins/memory-run-store.d.ts +1 -1
  131. package/dist/plugins/one-time-migrations.d.ts +111 -3
  132. package/dist/plugins/one-time-migrations.js +380 -10
  133. package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
  134. package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
  135. package/dist/plugins/permission-rule-store-sql.js +13 -16
  136. package/dist/plugins/pg-pool.d.ts +7 -3
  137. package/dist/plugins/pg-session-storage.d.ts +31 -20
  138. package/dist/plugins/pg-session-storage.js +120 -52
  139. package/dist/plugins/remote-env-adb.d.ts +20 -4
  140. package/dist/plugins/remote-env-adb.js +70 -62
  141. package/dist/plugins/remote-env-device.d.ts +30 -12
  142. package/dist/plugins/remote-env-device.js +18 -17
  143. package/dist/plugins/remote-env-e2b.d.ts +18 -4
  144. package/dist/plugins/remote-env-e2b.js +41 -42
  145. package/dist/plugins/remote-env-file-error.d.ts +2 -0
  146. package/dist/plugins/remote-env-file-error.js +1 -1
  147. package/dist/plugins/remote-env-host.d.ts +21 -8
  148. package/dist/plugins/remote-env-host.js +66 -70
  149. package/dist/plugins/remote-env-k8s.d.ts +11 -16
  150. package/dist/plugins/remote-env-k8s.js +61 -54
  151. package/dist/plugins/remote-env-local-docker.d.ts +29 -6
  152. package/dist/plugins/remote-env-local-docker.js +46 -59
  153. package/dist/plugins/remote-env-ssh.d.ts +16 -4
  154. package/dist/plugins/remote-env-ssh.js +72 -56
  155. package/dist/plugins/remote-shell.d.ts +100 -3
  156. package/dist/plugins/remote-shell.js +166 -2
  157. package/dist/plugins/retention-store-sql.d.ts +9 -6
  158. package/dist/plugins/retention-store-sql.js +7 -1
  159. package/dist/plugins/run-store-sql.d.ts +13 -3
  160. package/dist/plugins/run-store-sql.js +5 -0
  161. package/dist/plugins/schema-consistency.d.ts +15 -5
  162. package/dist/plugins/schema-consistency.js +14 -8
  163. package/dist/plugins/schema-drift-sentence.d.ts +27 -0
  164. package/dist/plugins/schema-drift-sentence.js +38 -0
  165. package/dist/plugins/session-fork.d.ts +54 -0
  166. package/dist/plugins/session-fork.js +38 -1
  167. package/dist/plugins/session-row-sql.d.ts +22 -0
  168. package/dist/plugins/session-row-sql.js +81 -0
  169. package/dist/plugins/session-store.d.ts +1 -1
  170. package/dist/plugins/store-backend.d.ts +1 -1
  171. package/dist/plugins/store-backend.js +19 -7
  172. package/dist/plugins/store-contracts.d.ts +25 -5
  173. package/dist/plugins/tidb-pool.d.ts +12 -7
  174. package/dist/plugins/tidb-pool.js +9 -3
  175. package/dist/plugins/tidb-session-store.d.ts +57 -26
  176. package/dist/plugins/tidb-session-store.js +118 -50
  177. package/dist/plugins/tool-result-store-sql.d.ts +3 -2
  178. package/dist/plugins/tool-result-store-sql.js +18 -10
  179. package/dist/project-memory.d.ts +9 -15
  180. package/dist/project-memory.js +5 -4
  181. package/dist/request-key-closure.js +1 -1
  182. package/dist/request-value-closure.d.ts +6 -7
  183. package/dist/request-value-closure.js +1 -3
  184. package/dist/run-local.d.ts +6 -0
  185. package/dist/run-local.js +24 -20
  186. package/dist/runs.d.ts +2 -1
  187. package/dist/runs.js +3 -1
  188. package/dist/security.d.ts +38 -12
  189. package/dist/security.js +8 -2
  190. package/dist/server-secret-env.d.ts +9 -5
  191. package/dist/session-children-retire.d.ts +27 -9
  192. package/dist/session-children-retire.js +9 -5
  193. package/dist/session-incarnation.d.ts +19 -9
  194. package/dist/session-incarnation.js +4 -2
  195. package/dist/session-row.d.ts +114 -5
  196. package/dist/session-row.js +55 -5
  197. package/dist/session-titler.d.ts +2 -2
  198. package/dist/task-a2a.d.ts +1 -1
  199. package/dist/task-cwd.d.ts +6 -1
  200. package/dist/task-mcp.d.ts +3 -3
  201. package/dist/tool-approval.d.ts +6 -0
  202. package/dist/tool-approval.js +8 -1
  203. package/dist/trace/project.d.ts +1 -0
  204. package/dist/trace/project.js +1 -0
  205. package/dist/trace/self-minted-wire.d.ts +111 -2
  206. package/dist/trace/self-minted-wire.js +38 -1
  207. package/dist/url-refusal.d.ts +1 -1
  208. package/dist/web-admin-client/apply-effective.d.ts +17 -3
  209. package/dist/web-admin-client/apply-effective.js +20 -10
  210. package/dist/web-admin-client/skills-mcp.js +3 -2
  211. package/dist/web-admin-client/types.d.ts +6 -0
  212. package/package.json +3 -3
  213. package/wire-changes/server@7.139.0.json +348 -0
  214. package/dist/http/device-placement.d.ts +0 -175
  215. package/dist/http/device-placement.js +0 -145
@@ -1,175 +0,0 @@
1
- /**
2
- * device lane **首绑写协议**(design `device-executor-lane-v2` §4.3.2)—— 两阶段 placement 的
3
- * server 半场:`resolveDevicePlacementIntent`(**只校验、不写库**)+ {@link commitPlacement}
4
- * (**唯一**写入属主)。
5
- *
6
- * ── 两阶段的**分界在哪**(codex R1 两条 finding 判真后的正形)────────────────────────────────────
7
- * 设计同段给了两条等价路(「同一 DB 事务原子提交,**或** commit 失败时以 taskId/rev CAS 终结并释放已建的
8
- * run」)。本仓取**后者**,理由是结构性的:run 行住在 `RunStore`(tidb / pg / **file**),绑定行住在
9
- * `DeviceStore`(tidb / pg)—— 要它们共享一个事务席,就等于要求「凡 device 车道,两只店必须是同一个
10
- * SQL 连接池」。那会把 file / memory 形态的 run 店整个排除在外,而「本地与云无缝、TOC 面 backend 可插」
11
- * 是本仓的铁律。
12
- *
13
- * 🔴 **分界的判据只有一句话:凡需要「这条会话已经归我」才成立的事,排在 claim 之后;其余全排在建任何
14
- * 东西之前。** claim 之后只剩两件,而且是同一条不变量的两条臂 —— **离开 commit 的那一刻,绑定就是准入
15
- * 批准的那一台**:首绑那一次 CAS(写),或对已有绑定的**再断言**(零写;codex 轮 2 [high] 的类修)。
16
- * ⚠️ 「那一刻」是字面义,**不是「此后不变」** —— 论证与残余见 {@link commitPlacement} 顶注。
17
- 首绑那一次 CAS 必须在 claim 之后 —— 否则两条并发提交里输掉 claim 的那个会先把绑定写成**它**要的设备,
18
- * 赢家的 run 于是落到别人挑的机器上。而准入链的三步(绑定解出 / devices active / presence)**全是读**,
19
- * 它们一个字节都不写,所以整段挪到 `resolveDevicePlacementIntent` 里、在 `createRun` **之前**判
20
- *(意图把批准的那台设备**带上**,commit 才有东西可断言 —— 见 `DevicePlacementIntent.admittedDeviceId`)。
21
- * 这条收法同时关掉 codex 轮 1 的两条(都判真):
22
- * · **[medium] 幂等键被常规拒绝烧掉**:`POST /v1/runs` 的自带 `body.taskId` 重放腿读的是 durable run 行
23
- * (`routes/runs.ts` 的 `getRun(clientTaskId)` 臂),行在**哪怕是 failed** 也直接回 `202 {status:"failed"}`
24
- * ⇒ 「设备离线」这种**常规**拒绝若先建了行,网关按同一个 taskId 的正常重试从此永远拿不到一次真准入。
25
- * 读排在前面 ⇒ 常规拒绝的读面代价为零(零 run 行、零 claim、零补偿)。
26
- * · **[high] claim 与 placement 写之间的无界窗**:claim 之后的**每一次**店往返都带
27
- * {@link placementWriteDeadlineMs} 派生的**硬时限**(远小于 `runStaleSec`)⇒ 「挂得比 stale 回收还久、
28
- * 回来以后仍然放行」这条竞态被时限从结构上关掉;超时 = 拒(fail-closed)+ 释放,绝不放行。
29
- * 残余(如实):`createRun` 之后到心跳起来之前**本来**就有若干 await(`putCtx`、SSE 前导),那个窗是
30
- * 存量形,本文件只负责**不加宽它**(本层的写有界),不假装把它关掉了。
31
- *
32
- * ── 为什么 commit 只许有一个属主函数 ───────────────────────────────────────────────────────────────
33
- * 真码有**四个** `runStore.createRun(` 成功点(流式提交 / 同步提交 / durable 提交腿 / wake 续跑腿),
34
- * 而「建了 run 行之后要不要绑设备」这件事对它们是同一句话。四份手抄 = 第五个入口出现时漏接一份,
35
- * 而漏接的症状是「run 跑起来了但没有绑定」⇒ 执行期才 `device.not_bound`,账面上是一条已收费的失败 run。
36
- * 类级机器门钉住这条:`test/device-first-bind.test.ts` 按 AST 找 `src/http/**` 里每一处 `createRun(`,
37
- * 要求同一函数体内可见地走到 {@link commitPlacement}(漏接 = 当场红)。
38
- *
39
- * ── 准入链(§4.3.2 提交期三步,全 fail-closed)─────────────────────────────────────────────────────
40
- * 1. 绑定解出 deviceId —— 解不出 = `device.not_bound`;
41
- * 2. devices 行 `active` —— 行不在 / 不是你的 = `not_found.device`(反枚举同形),吊销 = `device.revoked`;
42
- * 3. presence 在线 —— 否 = `device.offline`(**只在提交期**校验;执行期离线走 §5.4/§8)。
43
- * 三步的属主是 `device-enrollment.ts` 的 `admitSession`,本文件一个判据都不自建,而且**整段在建 run 之前**跑
44
- * (见上一段的分界判据)。任何查表故障 = 拒([ref]:安全轴上没有静默 fail-open 臂)。
45
- *
46
- * ── 便利度不变(clay [ref] 硬约束)────────────────────────────────────────────────────────────────
47
- * 本文件**不产生任何审批**:它只做「这条 session 该落在哪台设备上」的查表与一次 CAS。device 车道上
48
- * `permissionMode: "bypassPermissions"` 的 run 与 host 车道同构造同读数(判据钉在 device-first-bind
49
- * 测试的「零额外审批」格)——「因为是 device 车道所以多问一次」在本层结构上不存在。
50
- */
51
- import type { IncomingMessage } from "node:http";
52
- import { type DeviceOwner } from "../device-store.js";
53
- import { type DeviceEnrollment, type DeviceRejection } from "../device-enrollment.js";
54
- import type { ServiceConfig } from "../config.js";
55
- import type { RunStore } from "../plugins/store-backend.js";
56
- import { type RefusalBody } from "./refusal-body.js";
57
- /**
58
- * 调用方 device 域的**唯一铸造点**。`undefined` = 判不出域(无 principal,或凭据不带 tenant claim)——
59
- * 消费点必须响亮拒,禁静默空视图。
60
- *
61
- * 🔴 位置即契约:`{owner_tenant, owner_subject}` 是 O11 的 device-lane-local 复合身份(全局 principal
62
- * 一个字节不动)。subject = `gatedPrincipal`(verified 链),tenant = `ssoVerifiedScope`(registry
63
- * auth-bridge JWT 的 `scope` claim)—— **两者都是签名真源,不是自报头**。管理面三读三写与首绑写协议
64
- * 全部从这一只函数取域;派生第二份就是把 O11 的判据本体复制成一份会漂的孪生。
65
- */
66
- export declare function deviceOwnerDomainOf(req: IncomingMessage, config: ServiceConfig): DeviceOwner | undefined;
67
- /**
68
- * **已校验、未写库**的 placement 意图(§4.3.2 ①)。
69
- *
70
- * 它的存在本身就是那条设计裁定:`resolveSpec` / `prepareSpec` 那一刻**还不知道**哪个并发请求会赢下
71
- * session 的 active-run claim,所以它不能写绑定行 —— 只能把「如果我赢了,该写什么」算好交出去。
72
- * 而**除了那一次写**,别的都已经在这一刻判完了(见文件头注的分界判据)。
73
- */
74
- export interface DevicePlacementIntent {
75
- /** 绑定键 = **根**会话(后代无条件继承;core 为 subagent/cascade/verify 另铸 sessionId,不另绑)。 */
76
- readonly rootSessionId: string;
77
- /** 验证过的复合身份(§4.3.2:恒取 verified 链,不取自可伪造头)。 */
78
- readonly owner: DeviceOwner;
79
- /**
80
- * **准入链批准的那台设备** —— 意图阶段判完的那一台,也是 {@link commitPlacement} 的**断言对象**。
81
- *
82
- * 🔴 这一位是 codex 轮 2 [high] 的类修(病:已绑路径的意图**丢掉了设备身份**,于是一条明确写了
83
- * `deviceId: A` 的提交在「准入过后、claim 之前」被换绑到 B 时会**静默地在 B 上跑完** —— 执行环境
84
- * 工厂读的是**当下**的绑定,它根本不知道调用方点过名,`plugins/remote-env-device.ts` 的工厂段亲读坐实)。
85
- * 身份留在意图里,commit 才有东西可断言。
86
- */
87
- readonly admittedDeviceId: string;
88
- /**
89
- * `true` = 这条根会话在意图阶段**还没有绑定** ⇒ commit 要做那一次首绑 CAS(写);
90
- * `false` = 已有绑定 ⇒ commit **不写**,只把 {@link admittedDeviceId} 对着活绑定**再断言一次**。
91
- * 两条臂的共同不变量只有一句:**离开 commitPlacement 时,这条会话的绑定就是准入批准的那一台。**
92
- */
93
- readonly firstBind: boolean;
94
- }
95
- /**
96
- * §4.3.2 ① —— 产出**已校验、不写库**的意图,或响亮拒。**准入链整段在这里跑**(它全是读),所以
97
- * 常规拒绝(未绑定 / 离线 / 已吊销 / 不是你的设备)的读面代价是零:没有 run 行、没有会话占用、
98
- * 没有补偿要做,调用方自带的幂等 taskId 一个都不会被烧掉(codex R1-[medium] 的类修)。
99
- *
100
- * 判据(逐条,fail-closed):
101
- * · 车道不是 device 而体携 `deviceId` ⇒ **400**(不静默忽略 —— 安全轴禁 quiet fallback:调用方以为
102
- * 自己指定了执行设备,而那条声明从未存在);
103
- * · 车道是 device 而装配缺件 ⇒ **501**(boot 的 `device-lane-unwired` 不变量本应先拒启,这里是消费点自卫);
104
- * · 判不出调用方域 ⇒ **403 响亮**(O11:准入等值比对没有真源时放行 = 把绑定写成任何人的);
105
- * · 没有根会话 ⇒ **400**(绑定键不存在,绑无可绑);
106
- * · 其余全部交给准入链属主 `admitSession`,**唯一的分叉**是它答 `device.not_bound` 那一支 ——
107
- * 那正是「还没有绑定」,于是本函数看提交体有没有携首绑目标:没携 ⇒ 把 `device.not_bound` 原样交回;
108
- * 携了 ⇒ 形门 + devices 行(active ∧ 是本域的 ∧ **在线**)三条读判完,产出首绑意图。
109
- *
110
- * 返回 `undefined` = 本部署不是 device 车道 ⇒ 整条 placement 面不存在(既有行为逐字节不变)。
111
- */
112
- export declare function resolveDevicePlacementIntent(args: {
113
- config: ServiceConfig;
114
- req: IncomingMessage | undefined;
115
- rootSessionId: string | undefined;
116
- deviceId: unknown;
117
- store: import("../device-store.js").DeviceStore | undefined;
118
- enrollment: DeviceEnrollment | undefined;
119
- }): Promise<DevicePlacementIntent | undefined>;
120
- /** {@link commitPlacement} 的依赖 —— 逐字取自 `ServiceDeps` 的同名键(同一只 deps 对象喂进来,所以
121
- * 「意图在 ⟹ 准入链在」是**同对象不变量**,不是一句祈愿)。 */
122
- export interface PlacementCommitCtx {
123
- deviceEnrollment?: DeviceEnrollment | undefined;
124
- config?: {
125
- runStaleSec?: number;
126
- } | undefined;
127
- logger?: {
128
- warn(msg: string, fields?: Record<string, unknown>): void;
129
- } | undefined;
130
- metrics?: {
131
- inc(name: string, labels?: Record<string, string>): void;
132
- } | undefined;
133
- }
134
- /**
135
- * claim 之后那一次写的**硬时限**(codex R1-[high] 的类修)。
136
- *
137
- * 界从 `runStaleSec` **派生**而不是新铸一个旋钮(operator-knob 纪律:没人要求过的旋钮不铸):它要回答的
138
- * 问题只有一个 —— 「这次写会不会挂到 stale 回收把我的会话占用收走」。取四分之一并夹在 `[1s, 15s]`:
139
- * 上夹是因为一次 CAS 写挂过 15 秒已经不是慢而是坏,下夹是因为运维把 `runStaleSec` 配得极小时,时限
140
- * 不该短到把正常的库往返判成故障。
141
- */
142
- export declare function placementWriteDeadlineMs(runStaleSec: number | undefined): number;
143
- /**
144
- * §4.3.2 ②③ —— placement 的**唯一**提交点(写 + 断言都只在这里发生)。四个 `createRun` 成功点全部经它。
145
- *
146
- * 到这一刻为止,该判的读已经判完(见文件头注的分界判据),而本函数拿到了一样意图阶段拿不到的东西:
147
- * **这条会话的 active-run claim 已经归我们了**。两件事因此才成立:
148
- * · **首绑臂**(`firstBind`)—— 做那一次 INSERT-if-absent CAS(赢 = 成立;输且同设备 = 幂等成功;
149
- * 输且异设备 = `device.claim_conflict`);
150
- * · **已绑臂** —— **不写**,但拿同一只 `admitSession` 对着活绑定把 {@link DevicePlacementIntent.admittedDeviceId}
151
- * **再断言一次**(codex 轮 2 [high] 的类修)。判据属主仍是准入链那一只函数,本文件一个新判据都没造。
152
- *
153
- * 不变量(一句话):**离开本函数的那一刻,这条会话的绑定就是准入批准的那一台;否则本请求刚建的 run
154
- * 已被终结并释放,调用方拿到的是一个 typed 拒绝。**
155
- *
156
- * 🔴 **这条不变量到此为止,它不说「此后不变」——一次如实改口(codex 轮 3 驳回了我方轮 2 的论证)。**
157
- * 我方轮 2 写过「claim 一归我们,换绑动词的『无活跃 run』门就把并发换绑拒掉 ⇒ 断言之后绑定不可能再动」。
158
- * **那句话是错的**,codex 轮 3 给的交错成立、亲读坐标复核无误:换绑腿的那道门是 check→act
159
- * (`http/routes/devices.ts` 的「🔴 成文残留(设计裁定,非疏漏)」那一段逐字写着这个窗),它可以在本次
160
- * `createRun` **之前**读到「无活跃 run」而在本函数**之后**才提交 CAS —— run claim 不推进绑定 `rev`,
161
- * 那条 CAS 照样赢。于是:
162
- * · 本断言**关掉**的是「换绑在本函数读之前已可见」那一半(较大的一半);
163
- * · **剩下**的一半是那条**存量**的 check→act 窗,它的既有处置逐字在那段注里:①赢窗的 run 在换绑提交
164
- * **后**铸 env ⇒ 读到新绑定;②在提交前铸(或已 connect)⇒ 投递门 `expectedDeviceId` 围栏按铸造时
165
- * 设备恒拒,零指令落到任何一台设备。**对本批新增的「体里点了名」这一形,臂 ① 的『合法』口径不再成立**
166
- * (调用方点了 A,却在 B 上跑),这条**登记为未闭环**:要真正关掉它,得把 `admittedDeviceId` 一路钉进
167
- * 执行环境工厂与投递围栏(那是一条跨 core seam 的**新能力**,按本仓复审纪律不在复审环里加),或按
168
- * 稿 §6 的候门段做跨店互斥 claim。两条都是独立的车与设计,不是本层的一个补丁。
169
- *
170
- * 返回 `null` = 放行(含「本部署不是 device 车道」)。
171
- */
172
- export declare function commitPlacement(ctx: PlacementCommitCtx, runStore: RunStore, taskId: string, intent: DevicePlacementIntent | undefined): Promise<DeviceRejection | null>;
173
- /** 拒绝的**唯一** wire 体铸点(四个消费点各自用自己协议的发送姿势,但体的键集只有这一份)。 */
174
- export declare function devicePlacementErrorBody(rej: DeviceRejection): RefusalBody;
175
- //# sourceMappingURL=device-placement.d.ts.map
@@ -1,145 +0,0 @@
1
- import { createCodedHttpError, ssoVerifiedScope } from "../security.js";
2
- import { gatedPrincipal } from "./principal-gate.js";
3
- import { DEVICE_ERROR_CODES, assertDeviceIdShape, } from "../device-store.js";
4
- import { buildDeviceRejection } from "../device-enrollment.js";
5
- import { releaseClaimedRun } from "./claimed-run.js";
6
- import { refusalBody } from "./refusal-body.js";
7
- export function deviceOwnerDomainOf(req, config) {
8
- const subject = gatedPrincipal(req, config);
9
- if (subject === undefined)
10
- return undefined;
11
- const tenant = ssoVerifiedScope(req);
12
- if (tenant === undefined)
13
- return undefined;
14
- return { tenant, subject };
15
- }
16
- function asHttpError(rej) {
17
- return createCodedHttpError(rej.status, rej.code, rej.message);
18
- }
19
- function registryUnavailable() {
20
- return buildDeviceRejection(DEVICE_ERROR_CODES.NOT_BOUND, "the device binding for this session could not be resolved (the device registry is unavailable) — refusing rather than running on an unverified device");
21
- }
22
- export async function resolveDevicePlacementIntent(args) {
23
- const laneIsDevice = args.config.remoteExec?.provider === "device";
24
- const requested = args.deviceId;
25
- if (!laneIsDevice) {
26
- if (requested !== undefined) {
27
- throw createCodedHttpError(400, "request.precondition_unmet", "deviceId is only accepted on a deployment that executes on the user's own device (REMOTE_EXEC=device) — this deployment does not, so binding this session to a device id would be a declaration that never took effect");
28
- }
29
- return undefined;
30
- }
31
- if (!args.store || !args.enrollment) {
32
- throw createCodedHttpError(501, "capability.device_lane_required", "device execution is selected for this deployment but its stores are not wired on this replica — refusing to admit a submit that would execute nowhere");
33
- }
34
- if (args.req === undefined) {
35
- throw createCodedHttpError(403, "auth.forbidden", "device placement needs a verified caller domain and this leg carries no request context");
36
- }
37
- const owner = deviceOwnerDomainOf(args.req, args.config);
38
- if (owner === undefined) {
39
- throw createCodedHttpError(403, "auth.forbidden", "device placement needs a caller domain: this credential carries no verified tenant scope claim (registry auth-bridge JWT `scope`) — connect through the registry auth bridge");
40
- }
41
- if (args.rootSessionId === undefined || args.rootSessionId === "") {
42
- throw createCodedHttpError(400, "request.precondition_unmet", "device execution binds a device to a ROOT SESSION, so this submit needs a session — none was resolved for it");
43
- }
44
- if (requested !== undefined) {
45
- if (typeof requested !== "string") {
46
- throw createCodedHttpError(400, "request.field_invalid", "deviceId must be a string (the device id returned by POST /v1/devices/enroll)");
47
- }
48
- try {
49
- assertDeviceIdShape(requested);
50
- }
51
- catch {
52
- throw createCodedHttpError(400, "request.field_invalid", "deviceId is not a valid device id (byte-exact key: non-empty, no control characters or surrounding whitespace, at most 64 bytes)");
53
- }
54
- }
55
- const requestedDeviceId = requested;
56
- let admitted;
57
- try {
58
- admitted = await args.enrollment.admitSession({
59
- rootSessionId: args.rootSessionId,
60
- requester: owner,
61
- ...(requestedDeviceId === undefined ? {} : { requestedDeviceId }),
62
- });
63
- }
64
- catch {
65
- throw asHttpError(registryUnavailable());
66
- }
67
- if (admitted.ok)
68
- return { rootSessionId: args.rootSessionId, owner, admittedDeviceId: admitted.value.deviceId, firstBind: false };
69
- if (admitted.reject.code !== DEVICE_ERROR_CODES.NOT_BOUND || requestedDeviceId === undefined)
70
- throw asHttpError(admitted.reject);
71
- let view;
72
- try {
73
- view = await args.store.readDeviceForAdmission(requestedDeviceId, owner);
74
- }
75
- catch {
76
- throw asHttpError(registryUnavailable());
77
- }
78
- if (!view)
79
- throw asHttpError(buildDeviceRejection(DEVICE_ERROR_CODES.NOT_FOUND, "device not found"));
80
- if (view.row.status === "revoked")
81
- throw asHttpError(buildDeviceRejection(DEVICE_ERROR_CODES.REVOKED, "device has been revoked"));
82
- if (!view.online)
83
- throw asHttpError(buildDeviceRejection(DEVICE_ERROR_CODES.OFFLINE, "device is not connected"));
84
- return { rootSessionId: args.rootSessionId, owner, admittedDeviceId: requestedDeviceId, firstBind: true };
85
- }
86
- export function placementWriteDeadlineMs(runStaleSec) {
87
- const stale = typeof runStaleSec === "number" && Number.isFinite(runStaleSec) && runStaleSec > 0 ? runStaleSec : 120;
88
- return Math.min(15_000, Math.max(1_000, Math.floor((stale * 1000) / 4)));
89
- }
90
- export async function commitPlacement(ctx, runStore, taskId, intent) {
91
- if (intent === undefined)
92
- return null;
93
- const enrollment = ctx.deviceEnrollment;
94
- const releaseRun = (why) => releaseClaimedRun(runStore, taskId, why, (err, step) => {
95
- ctx.logger?.warn("device_placement_run_release_failed", { taskId, step, err: err instanceof Error ? err.message : String(err) });
96
- ctx.metrics?.inc("device_placement_run_release_failed_total");
97
- });
98
- if (!enrollment) {
99
- await releaseRun("device placement could not be committed: the device lane's admission chain is not wired");
100
- throw new Error("commitPlacement: a device placement intent was minted without a DeviceEnrollment — assembly bug (both are read from the same ServiceDeps)");
101
- }
102
- const deadlineMs = placementWriteDeadlineMs(ctx.config?.runStaleSec);
103
- const withDeadline = async (work) => {
104
- let timer;
105
- try {
106
- return await Promise.race([
107
- work,
108
- new Promise((_resolve, reject) => {
109
- timer = setTimeout(() => reject(new Error(`device placement store call exceeded ${deadlineMs}ms`)), deadlineMs);
110
- timer.unref?.();
111
- }),
112
- ]);
113
- }
114
- finally {
115
- if (timer)
116
- clearTimeout(timer);
117
- }
118
- };
119
- let rejection = null;
120
- try {
121
- if (intent.firstBind) {
122
- const bound = await withDeadline(enrollment.bindPlacement({ rootSessionId: intent.rootSessionId, deviceId: intent.admittedDeviceId, requester: intent.owner }));
123
- if (!bound.ok)
124
- rejection = bound.reject;
125
- }
126
- else {
127
- const readmitted = await withDeadline(enrollment.admitSession({ rootSessionId: intent.rootSessionId, requester: intent.owner, requestedDeviceId: intent.admittedDeviceId }));
128
- if (!readmitted.ok)
129
- rejection = readmitted.reject;
130
- }
131
- }
132
- catch (err) {
133
- ctx.logger?.warn("device_placement_commit_failed", { rootSessionId: intent.rootSessionId, firstBind: intent.firstBind, deadlineMs, err: err instanceof Error ? err.message : String(err) });
134
- rejection = registryUnavailable();
135
- }
136
- if (rejection === null)
137
- return null;
138
- ctx.metrics?.inc("device_typed_rejects_total", { errorCode: rejection.code });
139
- await releaseRun(`device placement rejected: [${rejection.code}] ${rejection.message}`);
140
- return rejection;
141
- }
142
- export function devicePlacementErrorBody(rej) {
143
- return refusalBody({ errorCode: rej.code, message: rej.message });
144
- }
145
- //# sourceMappingURL=device-placement.js.map