@sema-agent/server 7.136.0 → 7.137.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 (214) hide show
  1. package/MIGRATION.md +173 -0
  2. package/USAGE.md +84 -16
  3. package/deploy/sema-up/README.md +14 -0
  4. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +7 -1
  5. package/deploy/sema-up/stack/sema-stack-cluster.yaml +7 -1
  6. package/dist/approval-content-kind.d.ts +6 -0
  7. package/dist/approval-content-kind.js +7 -0
  8. package/dist/approval-gate-currency.d.ts +48 -6
  9. package/dist/approval-gate-currency.js +16 -9
  10. package/dist/approval-hmac.d.ts +13 -1
  11. package/dist/approval-hmac.js +1 -1
  12. package/dist/approval.js +3 -3
  13. package/dist/auth-bridge.js +2 -2
  14. package/dist/boot/execution-env.d.ts +24 -1
  15. package/dist/boot/execution-env.js +69 -6
  16. package/dist/boot/reapers.d.ts +3 -0
  17. package/dist/boot/reapers.js +15 -1
  18. package/dist/boot/resolve-spec.d.ts +4 -0
  19. package/dist/boot/resolve-spec.js +16 -17
  20. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  21. package/dist/boot/stage-04-stores.d.ts +1 -1
  22. package/dist/boot/stage-05-execution-env.d.ts +3 -1
  23. package/dist/boot/stage-05-execution-env.js +11 -2
  24. package/dist/boot/stage-06-runners.d.ts +3 -1
  25. package/dist/boot/stage-06-runners.js +5 -1
  26. package/dist/boot/stage-07-capability-layer.d.ts +4 -1
  27. package/dist/boot/stage-07-capability-layer.js +1 -1
  28. package/dist/boot/stage-08-reapers.d.ts +4 -1
  29. package/dist/boot/stage-08-reapers.js +2 -1
  30. package/dist/boot/stage-09-leader.d.ts +4 -1
  31. package/dist/boot/stage-10-http-server.d.ts +4 -1
  32. package/dist/boot/stage-10-http-server.js +3 -2
  33. package/dist/boot/stores.d.ts +1 -1
  34. package/dist/broker/broker.d.ts +89 -0
  35. package/dist/broker/broker.js +287 -0
  36. package/dist/broker/constants.d.ts +39 -0
  37. package/dist/broker/constants.js +15 -0
  38. package/dist/broker/http1.d.ts +111 -0
  39. package/dist/broker/http1.js +256 -0
  40. package/dist/broker/lease-book.d.ts +87 -0
  41. package/dist/broker/lease-book.js +142 -0
  42. package/dist/broker/lease-file.d.ts +60 -0
  43. package/dist/broker/lease-file.js +127 -0
  44. package/dist/broker/main.d.ts +2 -0
  45. package/dist/broker/main.js +20 -0
  46. package/dist/capabilities/composite-members.d.ts +2 -1
  47. package/dist/capabilities/composite-members.js +2 -1
  48. package/dist/capabilities/engine-capabilities.js +1 -0
  49. package/dist/capabilities/scenarios.d.ts +23 -8
  50. package/dist/capabilities/scenarios.js +13 -8
  51. package/dist/capabilities/skills.d.ts +28 -5
  52. package/dist/capabilities/skills.js +8 -0
  53. package/dist/center-credential-renewer.js +5 -1
  54. package/dist/checkpoint-ctx-seal-m28.d.ts +3 -10
  55. package/dist/checkpoint-ctx-seal-m28.js +1 -1
  56. package/dist/checkpoint-identity.d.ts +3 -3
  57. package/dist/config-catalog.js +9 -6
  58. package/dist/config-types.d.ts +9 -0
  59. package/dist/config.d.ts +1 -1
  60. package/dist/config.js +34 -8
  61. package/dist/deployment-governance.d.ts +2 -2
  62. package/dist/deployment-governance.js +7 -9
  63. package/dist/device-ws-protocol.d.ts +1 -0
  64. package/dist/device-ws-protocol.js +2 -1
  65. package/dist/file-history-disclosure.d.ts +4 -2
  66. package/dist/file-history-disclosure.js +2 -0
  67. package/dist/hooks/classic-stations.d.ts +11 -54
  68. package/dist/http/admission.js +10 -7
  69. package/dist/http/approval-sweeps.js +4 -3
  70. package/dist/http/caller-trust.d.ts +2 -1
  71. package/dist/http/caller-trust.js +1 -3
  72. package/dist/http/checkpoint-ctx.d.ts +48 -7
  73. package/dist/http/checkpoint-ctx.js +40 -0
  74. package/dist/http/dispatch.js +15 -10
  75. package/dist/http/durable-run-leg.d.ts +5 -1
  76. package/dist/http/durable-run-leg.js +1 -1
  77. package/dist/http/health-self-report.d.ts +2 -2
  78. package/dist/http/mcp-remote-text.d.ts +5 -0
  79. package/dist/http/mcp-remote-text.js +7 -0
  80. package/dist/http/principal-gate.d.ts +15 -10
  81. package/dist/http/principal-gate.js +48 -9
  82. package/dist/http/resume-legs.d.ts +2 -20
  83. package/dist/http/resume-legs.js +52 -38
  84. package/dist/http/route-ctx.d.ts +8 -2
  85. package/dist/http/routes/approvals-assistant.js +18 -4
  86. package/dist/http/routes/capabilities.js +11 -1
  87. package/dist/http/routes/mcp-reconnect.d.ts +1 -1
  88. package/dist/http/routes/mcp-reconnect.js +5 -7
  89. package/dist/http/routes/runs.js +2 -1
  90. package/dist/http/routes/session-sync.js +42 -6
  91. package/dist/http/routes/sessions.js +2 -1
  92. package/dist/http/routes/tasks.js +8 -6
  93. package/dist/http/server.d.ts +4 -0
  94. package/dist/http/stored-body-replay.d.ts +3 -3
  95. package/dist/http/stored-body-replay.js +6 -1
  96. package/dist/http/wire-types.d.ts +52 -27
  97. package/dist/identity-source.d.ts +65 -0
  98. package/dist/identity-source.js +45 -0
  99. package/dist/memory-store-refusal.d.ts +3 -3
  100. package/dist/model-entry-refusal.d.ts +2 -0
  101. package/dist/model-entry-refusal.js +11 -0
  102. package/dist/model-select.d.ts +6 -17
  103. package/dist/model-select.js +0 -3
  104. package/dist/namespaced-rule-name.d.ts +1 -1
  105. package/dist/observability/fail-open.d.ts +13 -1
  106. package/dist/observability/fail-open.js +13 -1
  107. package/dist/observability/metrics.js +1 -1
  108. package/dist/observability/run-terminal.js +1 -0
  109. package/dist/operator-ask.d.ts +2 -2
  110. package/dist/org-delivery-gate.js +1 -1
  111. package/dist/org-memory-admission.d.ts +2 -2
  112. package/dist/org-memory-admission.js +39 -2
  113. package/dist/parked-decide.js +3 -2
  114. package/dist/peer-directory.js +2 -0
  115. package/dist/permission-rule-list.d.ts +2 -2
  116. package/dist/plugins/checkpoint-store-sql.d.ts +19 -9
  117. package/dist/plugins/checkpoint-store-sql.js +6 -6
  118. package/dist/plugins/file-history-store-sql.js +7 -14
  119. package/dist/plugins/file-run-store.js +8 -2
  120. package/dist/plugins/k8s-exec-protocol.d.ts +69 -13
  121. package/dist/plugins/k8s-exec-protocol.js +116 -28
  122. package/dist/plugins/k8s-pod-delete-failure.d.ts +25 -0
  123. package/dist/plugins/k8s-pod-delete-failure.js +17 -0
  124. package/dist/plugins/k8s-pod-reaper.d.ts +112 -0
  125. package/dist/plugins/k8s-pod-reaper.js +122 -0
  126. package/dist/plugins/local-checkpoint-store.d.ts +3 -6
  127. package/dist/plugins/local-checkpoint-store.js +3 -3
  128. package/dist/plugins/memory-run-store.js +2 -0
  129. package/dist/plugins/one-time-migrations.d.ts +28 -9
  130. package/dist/plugins/one-time-migrations.js +48 -5
  131. package/dist/plugins/permission-rule-store-file.js +2 -2
  132. package/dist/plugins/permission-rule-store-sql.d.ts +17 -17
  133. package/dist/plugins/permission-rule-store-sql.js +16 -17
  134. package/dist/plugins/pg-pool.js +4 -0
  135. package/dist/plugins/pg-session-storage.js +2 -2
  136. package/dist/plugins/remote-env-adb.d.ts +3 -0
  137. package/dist/plugins/remote-env-adb.js +1 -0
  138. package/dist/plugins/remote-env-device.d.ts +3 -0
  139. package/dist/plugins/remote-env-device.js +4 -0
  140. package/dist/plugins/remote-env-e2b.d.ts +3 -0
  141. package/dist/plugins/remote-env-e2b.js +1 -0
  142. package/dist/plugins/remote-env-file-error.d.ts +8 -1
  143. package/dist/plugins/remote-env-file-error.js +7 -1
  144. package/dist/plugins/remote-env-host.d.ts +7 -1
  145. package/dist/plugins/remote-env-host.js +7 -1
  146. package/dist/plugins/remote-env-k8s.d.ts +137 -10
  147. package/dist/plugins/remote-env-k8s.js +276 -14
  148. package/dist/plugins/remote-env-local-docker.d.ts +3 -0
  149. package/dist/plugins/remote-env-local-docker.js +1 -0
  150. package/dist/plugins/remote-env-ssh.d.ts +3 -0
  151. package/dist/plugins/remote-env-ssh.js +1 -0
  152. package/dist/plugins/run-store-sql.js +10 -4
  153. package/dist/plugins/session-ownership.d.ts +17 -3
  154. package/dist/plugins/session-ownership.js +19 -5
  155. package/dist/plugins/sql-errors.d.ts +23 -2
  156. package/dist/plugins/sql-errors.js +15 -0
  157. package/dist/plugins/store-contracts.d.ts +10 -1
  158. package/dist/plugins/store-fault.d.ts +2 -2
  159. package/dist/plugins/store-fault.js +4 -2
  160. package/dist/plugins/tidb-pool.js +7 -0
  161. package/dist/plugins/tidb-session-store.js +2 -2
  162. package/dist/request-key-closure.js +1 -1
  163. package/dist/request-value-closure.d.ts +31 -2
  164. package/dist/request-value-closure.js +7 -1
  165. package/dist/rule-entry-list.d.ts +3 -2
  166. package/dist/rule-entry-list.js +2 -2
  167. package/dist/rules-consent.js +7 -4
  168. package/dist/run-local.d.ts +2 -2
  169. package/dist/runs.js +2 -1
  170. package/dist/runtime-governance.d.ts +5 -5
  171. package/dist/sandbox-git-lease.d.ts +49 -0
  172. package/dist/sandbox-git-lease.js +60 -0
  173. package/dist/sandbox-git-plane.d.ts +84 -0
  174. package/dist/sandbox-git-plane.js +105 -0
  175. package/dist/security.d.ts +16 -4
  176. package/dist/security.js +8 -3
  177. package/dist/server-secret-env.d.ts +6 -0
  178. package/dist/server-secret-env.js +14 -0
  179. package/dist/session-error-wire.d.ts +14 -6
  180. package/dist/session-error-wire.js +2 -1
  181. package/dist/session-policy-wire.d.ts +1 -1
  182. package/dist/session-policy-wire.js +2 -2
  183. package/dist/session-sync.d.ts +1 -1
  184. package/dist/spec-fields.d.ts +43 -13
  185. package/dist/spec-fields.js +33 -12
  186. package/dist/stored-request-prompt-profile-m30.d.ts +49 -0
  187. package/dist/stored-request-prompt-profile-m30.js +25 -0
  188. package/dist/task-mcp.d.ts +1 -1
  189. package/dist/task-settings.d.ts +2 -2
  190. package/dist/tool-approval.d.ts +12 -2
  191. package/dist/tool-approval.js +8 -3
  192. package/dist/tool-rule-names.d.ts +5 -0
  193. package/dist/trace/core-keyset-guard.d.ts +4 -4
  194. package/dist/trace/engine-notice-wire.d.ts +1 -1
  195. package/dist/trace/engine-notice-wire.js +2 -0
  196. package/dist/trace/exit-seat.js +2 -2
  197. package/dist/trace/ledger-sink.d.ts +4 -5
  198. package/dist/trace/ledger-sink.js +3 -7
  199. package/dist/trace/project.d.ts +37 -3
  200. package/dist/trace/project.js +20 -6
  201. package/dist/trace/redact.d.ts +18 -6
  202. package/dist/trace/redact.js +118 -53
  203. package/dist/trace/self-minted-wire.d.ts +377 -3
  204. package/dist/trace/self-minted-wire.js +122 -0
  205. package/dist/trace/wire-projection-faces.d.ts +5 -3
  206. package/dist/trace/wire-projection-faces.js +7 -3
  207. package/dist/web-admin-client/apply-effective.js +0 -1
  208. package/dist/web-admin-client/center-request.d.ts +20 -2
  209. package/dist/web-admin-client/center-request.js +40 -5
  210. package/dist/web-admin-client/http-client.js +6 -6
  211. package/dist/web-admin-client/types.d.ts +5 -5
  212. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +11 -5
  213. package/package.json +2 -2
  214. package/wire-changes/server@7.137.0.json +825 -0
@@ -8,10 +8,26 @@
8
8
  *
9
9
  * ## 判官
10
10
  * 问题:**这把 gate token 是不是该会话当前待决的审批门?**(应答口的答法:`resumeCheckpoint` 的 stale 臂 —— 回显的 token ≠ 该会话当前待决的审批门 ⇒ `409 approval_stale`。)
11
+ * · {@link sightsCurrentGate} —— 「决断方回显的**门身份**(token / `checkpointId`)是不是当前门的同一面」的**唯一**比较(`approval_stale` 律,两道决断门一条,见下一节);
12
+ * 本仓每一处拿**决断方 / 读口给的那把**门身份比「是不是当前门」都经它(`/decide` 的 token 回显、计划审阅的身份回显、下面的 {@link readGateCurrency});
13
+ * 服务端对**自己选出的那张卡**的复核不是回显比较,不经它(`drivePlanReview` 的卡令牌复核答认领输家 `409 conflict.not_resumable`、唤醒腿的赢家比较);
14
+ * 射程:本判官问的是「**哪扇门**」;`/decide` 的 [ref] 第二源问的是「**哪只调用**」(回显的 `boundCallId` vs 载入行的 `toolCallId`)—— 两问不同,所以是两条比较,
15
+ * 不是「不能并」:它们共用同一个 409 答复,缺值规则各按各的问题定(说不出门身份 ⇒ 判「不是」;行上没有 `toolCallId` ⇒ 那一问不判,交引擎的绑定等值校验 fail-closed 兜)。
16
+ * 过期决断的 409 一族(门身份 / 调用绑定两问怎么排成一族、老行那一形答哪个码)是 7.138.0 的设计票(DEBTS-server,主会话开票);
11
17
  * · {@link currentApprovalGateToken} —— 该会话当前待决的审批门(审批门型优先,没有再退到任一待决行);应答口读它,读口读它,别处不再内联第二份;
12
18
  * · {@link readGateCurrency} —— 一把 token 对着它的读数:`current`(带那一行)/ `absent`(店里没有这一行)/ `settled`(行在,但不再是当前待决门:已结算、已过期、被取代);
13
19
  * · {@link createParkedGateRead} —— 协调器读口的门读席:把读数译成读口要的四种读法({@link ParkedGateReading}),`state` 闭集不加字。
14
20
  *
21
+ * ## `approval_stale` 律(两道决断门一条;[ref] 把计划审阅收进来)
22
+ * 一句话:**决断体带了「决断方看到的那道门」,而它不是该会话此刻在这道门上待决的那道门 ⇒ `409 approval_stale`,不落决断,拒体带当前门的回显坐标;不带 ⇒ 照旧受理。**
23
+ * 门的身份有两个面,**同一次铸**在同一行上(core 的每个停驻铸点把 `token` 与 `checkpointId` 一起铸;重开翻的是同一行、两面都不变)⇒ 在带身份的行上两面一一对应:
24
+ * · 能力面 `checkpointToken` —— 秘密,永不外发;`/decide` 的老回显键(直连门上拒收);
25
+ * · 身份面 `checkpointId` —— 非秘密(park 结果的 `terminal.checkpointId`、运维队列 / 收件行上的那一个);计划审阅能回显的只有它。
26
+ * 读法只有 `checkpoint-identity.ts` 的信任边界那一只:core 5.42.0 之前铸的老行没有它、没过边界的值不算它 ⇒ 这样的门**说不出**自己的身份面。
27
+ * 判官 {@link sightsCurrentGate} 只做一件事:回显的那一面 = 当前门的同一面。当前门没有那一面(老行 / 不可信值 / 调用方没读行)⇒ 不是(fail-closed:拒,不放)。
28
+ * 「当前门」由每道门按**自己的门型**选(`/decide` = {@link currentApprovalGateToken};计划审阅 = 它的选卡读),判官门型无关。
29
+ * 不把能力面先解成身份面再比:两面在同一行上同生同灭,比能力面就是比这一行;先解一次只会在快路上多一次店读,并在老行上(两边都读不出身份面)把两道不同的门判成同一道。
30
+ *
15
31
  * ## 一个读数只取自一个时刻([ref])
16
32
  * 门身份就是 token:引擎重开一张门(`reopen`:续跑腿在被批准的动作开跑之前失败)翻的是**同一行**(resolved → pending,token 不变)。所以「这张卡此刻在哪」可以在两次店读之间
17
33
  * 来回变(待决 → 被决 → 被重开);判官的分类只用**同一时刻成立**的读数:
@@ -33,8 +49,10 @@
33
49
  * 表明说 `decided` ⇒ 往下读;明说 `void` ⇒ `void`(没有人决过它,答案无处兑现 —— 已过期〔取消 / 收割〕、仍待决却不是当前门〔被取代,应答口答 stale〕、
34
50
  * 结算种类是引擎窗到期 / 宿主 SLA 扫 / 无人可答的失败族);**表外 ⇒ `unknown`**(读不懂的词撑不起「有人决过」,也撑不起「没有人决过它」);
35
51
  * ② 结算赢家的门记录 `resolvedOutcome.gateOutcome` 先过 core 唯一的门记录筛 `screenGateOutcome`(**档案读法**:读回的耐久行,闭集 = 今日成员 ∪ 已退役词;
36
- * 坏形记缺陷、绝不抛;本仓 `trace/project.ts` 投影同一种记录也只经这一只筛),有缺陷 ⇒ `unknown`,过了筛才读它的成员;
37
- * ③ 分类之前记录要**读得全**:有结算记录、时刻(引擎写进赢家的 `settlement.when`)是有限数、赢家绑的是这一行的调用且判词是 allow / deny(与
52
+ * 坏形记缺陷、绝不抛;本仓 `trace/project.ts` 投影同一种记录也只经这一只筛),有缺陷 ⇒ `unknown`,过了筛才读它的成员。core 7.56.0 [ref] 起筛自己判
53
+ * I10(`settlement.when` 是有限数)与 I11(调用方把赢家的 `decision` 一并交给筛:它是赢家拿得出的词,方向与记录的结算种类 / 处置同向)—— 本判官在同一次筛调用里
54
+ * 交 `winnerDecision`,时刻与方向的手判随之退役;
55
+ * ③ 分类之前记录要**读得全**:有结算记录、赢家绑的是这一行的调用且判词是 allow / deny(与
38
56
  * `findDecidedApprovalsForBinding` 同一只 {@link approvalDecisionOfWinner})—— 缺一个都是 `unknown`(**不编一个首决**;读口答 404,应答口对已结算的门答 409 `approval_stale`,两口不同判;
39
57
  * `void` 也要读得全:绑在别的调用上的结算说的不是这张卡);读全了再按**结算种类**表分类:人 / 策略的四个种类 ⇒ `decided`(理由取自赢家的 `reason`),其余十个 ⇒ `void`
40
58
  * (按种类不按当事方:跨版本稳定的是种类词,变过义的是当事方词 —— 此前版本的策略拒记 `policy_refused` + `who.party:"none"`,此后记 `"policy"`,按当事方查会把那一代合法历史记录读成 `void`);
@@ -42,14 +60,38 @@
42
60
  * `unknown` 的这一类(`decision_unreadable`)带上读不出的那几处(缺陷句,非空):读口答 404 `tool_approval.not_pending`,并记一行 error(首句 + 计数)。
43
61
  * 行没了 ⇒ `unknown`(`row_absent`;应答口 PARKED 臂的 404 `not_found.approval` 同判)。
44
62
  *
45
- * ## 盲区(core 的筛不判的两处,如实写下:一处本判官手判有限数,一处本判官也不核)
46
- * · 结算时刻 `settlement.when`:筛不判,本判官手判「有限数」;而本仓另一个读同一种记录的读者(trace 投影)把 `when` 原样放上 wire —— 同一个坏时刻,两个读者两种答法;
47
- * · 方向与配对:赢家的方向(`resolvedOutcome.decision`)与记录自己的种类 / 处置说的方向是否一致,以及种类 × 当事方的配对 —— 筛不判,本判官也不核:读口以赢家的方向为准、读的是种类。
48
- * 只有坏行 / 手改行才走得到这两处。候引擎的筛把时刻、方向与种类 × 当事方配对一并判了,两个读者同时退掉各自的手判(配对今天没有手判可退 —— 它无人判)。
63
+ * ## 盲区(core 的筛仍不判的一处,如实写下)
64
+ * · 种类 × 当事方的配对(结算种类与 `settlement.who` 的当事方是否相配)—— 筛不判(core KL-1265-b),本判官也不核:读的是种类。只有坏行 / 手改行才走得到。
65
+ * (时刻与方向两处自 core 7.56.0 [ref] 起由筛判:I10 / I11。trace 投影读同一种记录、经同一只筛,零改即得 I10 —— 坏时刻在两个读者那里是同一个答法。)
49
66
  */
50
67
  import { type Checkpoint, type CheckpointToken, type SettlementKind } from "@sema-agent/core";
51
68
  import { type PendingRowBinding } from "./plugins/checkpoint-store-sql.js";
52
69
  import { type ParkedGateRead } from "./tool-approval.js";
70
+ /**
71
+ * 决断方回显的「我看到的那道门」—— 门的两个身份面之一(两面的来历与读法见模块头注「`approval_stale` 律」)。
72
+ * `face` 就是回显它的那个体键名:`/decide` 的 `checkpointToken`,计划审阅的 `checkpointId`。
73
+ */
74
+ export type GateSighting = {
75
+ readonly face: "checkpointToken" | "checkpointId";
76
+ readonly value: string;
77
+ };
78
+ /**
79
+ * 当前门的身份面。能力面恒在;身份面在场 ⇔ 行上的 `checkpointId` 过了信任边界 —— 老行 / 没过边界 / 调用方没读行时缺席,
80
+ * 缺席读作「说不出它的身份面」(判官据此对身份面回显答「不是」),**不**读作「没有门」。
81
+ */
82
+ export interface CurrentGateFaces {
83
+ readonly checkpointToken: string;
84
+ readonly checkpointId?: string;
85
+ }
86
+ /** 一道已载入的当前门 → 它的两个身份面(身份面只经 {@link trustedCheckpointId};它含 token 的形在这里就被当成缺席,永不上比较、永不上 wire)。 */
87
+ export declare function currentGateFacesOf(token: string, row: {
88
+ readonly checkpointId?: unknown;
89
+ }): CurrentGateFaces;
90
+ /**
91
+ * `approval_stale` 律的**唯一**判官:回显的那一面是不是当前门的同一面(律全文见模块头注)。没有当前门(`null` / `undefined`)⇒ 不是;
92
+ * 当前门说不出回显的那一面 ⇒ 不是(fail-closed)。调用点:`/decide` 的 token 回显、计划审阅的身份回显、本模块的 {@link readGateCurrency}。
93
+ */
94
+ export declare function sightsCurrentGate(sighting: GateSighting, current: CurrentGateFaces | null | undefined): boolean;
53
95
  /** 判官要的两个店口(三后端同形;窄接口让测试与装配都不必拿整只店)。 */
54
96
  export interface GateCurrencyStore {
55
97
  get(token: CheckpointToken): Promise<Checkpoint | null>;
@@ -1,13 +1,21 @@
1
- import { REOPEN_REASONS, screenGateOutcome } from "@sema-agent/core";
1
+ import { REOPEN_REASONS, checkpointRevOf, screenGateOutcome } from "@sema-agent/core";
2
2
  import { approvalDecisionOfWinner } from "./plugins/checkpoint-store-sql.js";
3
3
  import {} from "./tool-approval.js";
4
4
  import { APPROVAL_GATE_KINDS } from "./approval-content-kind.js";
5
+ import { trustedCheckpointId } from "./checkpoint-identity.js";
6
+ export function currentGateFacesOf(token, row) {
7
+ const checkpointId = trustedCheckpointId(row.checkpointId, token);
8
+ return checkpointId === undefined ? { checkpointToken: token } : { checkpointToken: token, checkpointId };
9
+ }
10
+ export function sightsCurrentGate(sighting, current) {
11
+ if (current === null || current === undefined)
12
+ return false;
13
+ const face = current[sighting.face];
14
+ return face !== undefined && face === sighting.value;
15
+ }
5
16
  export async function currentApprovalGateToken(cs, sessionId) {
6
17
  return (await cs.findPendingTokenBySession(sessionId, undefined, { gateKinds: APPROVAL_GATE_KINDS })) ?? (await cs.findPendingTokenBySession(sessionId));
7
18
  }
8
- function revisionOf(cp) {
9
- return cp.rev ?? 0;
10
- }
11
19
  const GATE_READ_ROUNDS = 3;
12
20
  export async function readGateCurrency(cs, gateToken) {
13
21
  const token = gateToken;
@@ -17,10 +25,11 @@ export async function readGateCurrency(cs, gateToken) {
17
25
  return { kind: "absent" };
18
26
  if (row.status !== "pending")
19
27
  return { kind: "settled", checkpoint: row };
20
- const isCurrent = (await currentApprovalGateToken(cs, row.sessionId)) === gateToken;
28
+ const currentToken = await currentApprovalGateToken(cs, row.sessionId);
29
+ const isCurrent = sightsCurrentGate({ face: "checkpointToken", value: gateToken }, currentToken ? { checkpointToken: currentToken } : null);
21
30
  const again = await cs.get(token);
22
31
  if (again !== null && again.status === "pending") {
23
- if (revisionOf(again) === revisionOf(row))
32
+ if (checkpointRevOf(again) === checkpointRevOf(row))
24
33
  return isCurrent ? { kind: "current", checkpoint: again } : { kind: "settled", checkpoint: again };
25
34
  if (round === GATE_READ_ROUNDS)
26
35
  throw new Error(`the checkpoint row moved between reads in ${GATE_READ_ROUNDS} rounds in a row (decided and reopened each time): there is no single-instant reading of its gate`);
@@ -68,14 +77,12 @@ function decisionReading(checkpoint, gateBoundCallId) {
68
77
  function readWinner(checkpoint, gateBoundCallId) {
69
78
  const winner = checkpoint.resolvedOutcome;
70
79
  const recorded = winner?.gateOutcome;
71
- const [defect, ...more] = screenGateOutcome(recorded, { reading: "archive" });
80
+ const [defect, ...more] = screenGateOutcome(recorded, { reading: "archive", ...(winner !== undefined ? { winnerDecision: winner.decision } : {}) });
72
81
  if (defect !== undefined)
73
82
  return unreadable([defect, ...more]);
74
83
  const settlement = recorded?.settlement;
75
84
  if (settlement === undefined)
76
85
  return unreadable(["the gate record carries no settlement: no decider and no time"]);
77
- if (!Number.isFinite(settlement.when))
78
- return unreadable(["settlement.when is not a finite epoch-ms time"]);
79
86
  const boundCallId = gateBoundCallId ?? (checkpoint.pendingAction.kind === "tool_approval" ? checkpoint.pendingAction.toolCallId : "");
80
87
  const direction = approvalDecisionOfWinner(winner, boundCallId);
81
88
  if (direction === null)
@@ -20,6 +20,8 @@ import type { ApprovalHmacKey } from "./auth-keys.js";
20
20
  * `reason`,以及 [ref] 起的 `permissionModeAfter`)。⇒ 「同一张卡上把 approve 的档偷偷调宽」这件事,
21
21
  * 结构上只有 HMAC 这一把拦得住;往 `cnf.bnd` 里加位既不解决它,还会把一份合法 JWT 绑死在一个它签发时
22
22
  * 根本不知道的值上。**本次([ref] F1)只动 HMAC 这一把,`approvalBnd` 一个字节不动。**
23
+ * [ref] 同一判断:`plan_review` 的门身份 `checkpointId` 进 HMAC 的扩展对象,不进 `cnf.bnd` —— 计划审阅的 `cnf.bnd` 绑的是会话(它的第二位是一次性 nonce),
24
+ * 签发 JWT 的一方不知道此刻是哪道计划门;知道的是作决断的那一方,而 HMAC 正是它签的。
23
25
  *
24
26
  * ── 🔴 字节构造规则(权威定义;跨仓签名器照此实现,docs/ASSISTANT-WIRE-CONTRACT.md §6.2 同源)──────────
25
27
  * 被 HMAC 的是下面这个字符串的 **UTF-8 字节**:
@@ -81,7 +83,7 @@ export declare function questionAnswerFromProjection(p: QuestionAnswerProjection
81
83
  * 下面那两条编译期门当场红。**不许有第三种在场判**:恒 `!== undefined`(不是真值判、也不是 `?? null`
82
84
  * 折叠)—— `null` 与缺席在扩展位上是两个不同的被签值(「签名方明说这次不动」≠「签名方不知道有这个键」)。
83
85
  */
84
- export declare const APPROVAL_ENVELOPE_EXT_KEYS: readonly ["answer", "permissionModeAfter"];
86
+ export declare const APPROVAL_ENVELOPE_EXT_KEYS: readonly ["answer", "checkpointId", "permissionModeAfter"];
85
87
  export type ApprovalEnvelopeExtKey = (typeof APPROVAL_ENVELOPE_EXT_KEYS)[number];
86
88
  /** The canonical envelope both {@link approvalHmacMessage} (the signer's/verifier's shared byte-rule) and
87
89
  * {@link verifyApprovalHmac} take — ONE named type so the two signatures cannot silently drift into two
@@ -105,6 +107,16 @@ export interface ApprovalEnvelope {
105
107
  * 值是 {@link projectQuestionAnswer} 的产物,不是请求体上那个对象(理由见该函数)。
106
108
  */
107
109
  answer?: QuestionAnswerProjection;
110
+ /**
111
+ * 🔴 **[ref](7.137.0)—— `plan_review` 决断方看到的那道门(`checkpointId`)进签名载荷。**
112
+ *
113
+ * 计划审阅的证明只绑会话(`cnf.bnd` = 会话 + 一次性 nonce + 内容哈希),不绑门 ⇒ 决断体上的门身份是这次决断**落在哪道门**的唯一陈述
114
+ * (不是当前门 ⇒ `409 approval_stale`)。不签它,能改写线上字节的一方就能把它摘掉(陈旧保护整个不在)或换掉;拒收它则把这项保护在直连门部署上整个关掉。
115
+ * 判据扩成一句:**凡决定这次决断落在哪道门、续跑能做什么 / 落什么账的决裁字段都在签名信封里**。
116
+ * 与 `permissionModeAfter` 同形:在场才进扩展对象,缺席 ⇒ 字节与不认识它的签名器逐字相同(既有签名器零改动);带了不签 ⇒ `401 approval_mac_invalid`。
117
+ * `/decide` 不送它(那道门的门身份是 `boundCallId` / `boundInputHash`,已在前五位)。
118
+ */
119
+ checkpointId?: string;
108
120
  /**
109
121
  * 🔴 **[ref] 合并复审 F1 [high]**(7.86.0 立,[ref] 把它从裸尾位挪进扩展对象)—— `plan_review` 的
110
122
  * `permissionModeAfter` 选的是**批准之后这条 run 有没有手**(`acceptEdits` = 工作目录内的写不再逐次
@@ -5,7 +5,7 @@ export function projectQuestionAnswer(a) {
5
5
  export function questionAnswerFromProjection(p) {
6
6
  return { answers: p.map(([header, selected, note]) => ({ header, selected: [...selected], ...(note === null ? {} : { note }) })) };
7
7
  }
8
- export const APPROVAL_ENVELOPE_EXT_KEYS = ["answer", "permissionModeAfter"];
8
+ export const APPROVAL_ENVELOPE_EXT_KEYS = ["answer", "checkpointId", "permissionModeAfter"];
9
9
  const APPROVAL_ENVELOPE_HEAD_KEYS = ["sessionId", "boundCallId", "boundInputHash", "decision", "reason"];
10
10
  export function approvalHmacMessage(env) {
11
11
  const head = [env.sessionId, env.boundCallId ?? null, env.boundInputHash ?? null, env.decision, env.reason ?? null];
package/dist/approval.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createApprovalPolicy, isNamespacedCoveringRuleName, namespacedRuleNameCovers } from "@sema-agent/core";
1
+ import { createApprovalPolicy, isCoveringRuleName, ruleNameCovers } from "@sema-agent/core";
2
2
  import { operatorAlwaysHumanAsk, operatorAsk, requestAlwaysHumanAsk } from "./operator-ask.js";
3
3
  import { ASK_USER_QUESTION_TOOL_NAME } from "./approval-content-kind.js";
4
4
  import { isExplicitPosture } from "./posture-source.js";
@@ -116,10 +116,10 @@ export function createDeploymentListExemption(store, sessionId, logger, leg) {
116
116
  }
117
117
  function createNameSetMatcher(names) {
118
118
  const exact = new Set(names);
119
- const covering = names.filter((name) => isNamespacedCoveringRuleName(name, NAME_TABLE_BEHAVIOR));
119
+ const covering = names.filter((name) => isCoveringRuleName(name, NAME_TABLE_BEHAVIOR));
120
120
  if (covering.length === 0)
121
121
  return (toolName) => exact.has(toolName);
122
- return (toolName) => exact.has(toolName) || covering.some((rule) => namespacedRuleNameCovers(rule, toolName));
122
+ return (toolName) => exact.has(toolName) || covering.some((rule) => ruleNameCovers(rule, toolName));
123
123
  }
124
124
  export const AUTO_APPROVE_BUDGET_APPROVER = "auto_approve_budget";
125
125
  function budgetSeatAnswer(req, requireHit, signal) {
@@ -2,6 +2,7 @@ import { createPublicKey, verify as cryptoVerify } from "node:crypto";
2
2
  import { AUTH_BRIDGE_ALGS, DEFAULT_CLOCK_SKEW_SEC, DEFAULT_JWKS_REFRESH_MIN_INTERVAL_SEC, DEFAULT_OFFLINE_GRACE_SEC, WEB_ADMIN_JWKS_PATH, WebAdminJwks, classifyTimeClaims, jwksWithinOfflineGrace, shouldForceJwksRefresh, toVerifiedIdentity, } from "@sema-agent/settings-schema/api/auth-bridge";
3
3
  import { readTokenScope } from "@sema-agent/settings-schema/api/scopes";
4
4
  import { buildDisplaySafeUrl } from "./trace/redact.js";
5
+ import { centerJwksFetch } from "./web-admin-client/center-request.js";
5
6
  export function authBridgeIssuerOf(config) {
6
7
  if (config.authBridgeIssuer !== undefined)
7
8
  return { issuer: config.authBridgeIssuer, source: "explicit" };
@@ -49,7 +50,6 @@ export function createWebAdminJwtVerifier(opts) {
49
50
  const refreshMin = opts.jwksRefreshMinIntervalSec ?? DEFAULT_JWKS_REFRESH_MIN_INTERVAL_SEC;
50
51
  const offlineGraceSec = opts.offlineGraceSec ?? DEFAULT_OFFLINE_GRACE_SEC;
51
52
  const jwksTtlSec = opts.jwksTtlSec ?? DEFAULT_JWKS_TTL_SEC;
52
- const doFetch = opts.fetchImpl ?? fetch;
53
53
  const now = opts.nowMs ?? Date.now;
54
54
  const keys = new Map();
55
55
  let lastFetchAttemptMs;
@@ -66,7 +66,7 @@ export function createWebAdminJwtVerifier(opts) {
66
66
  inflight = (async () => {
67
67
  lastFetchAttemptMs = now();
68
68
  try {
69
- const res = await doFetch(jwksUrl, { redirect: "error", signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS) });
69
+ const res = await centerJwksFetch(jwksUrl, { timeoutMs: JWKS_FETCH_TIMEOUT_MS, fetchImpl: opts.fetchImpl });
70
70
  if (!res.ok)
71
71
  throw new Error(`jwks HTTP ${res.status}`);
72
72
  const cl = Number(res.headers.get("content-length"));
@@ -7,7 +7,9 @@ import { PerTaskImageRegistry } from "../per-task-image.js";
7
7
  import type { DeviceEnrollment } from "../device-enrollment.js";
8
8
  import type { DeviceStore } from "../device-store.js";
9
9
  import type { DeviceWsHub } from "../device-ws-hub.js";
10
- import { k8sExecutionEnvFactory, type K8sEnvConfig, type K8sEnvDeps } from "../plugins/remote-env-k8s.js";
10
+ import { k8sExecutionEnvFactory, type K8sEnvConfig, type K8sEnvDeps, type SandboxSecretSeat } from "../plugins/remote-env-k8s.js";
11
+ import { type SandboxGitPlane, type SandboxGitRunRows } from "../sandbox-git-plane.js";
12
+ import { type SandboxPodReaper } from "../plugins/k8s-pod-reaper.js";
11
13
  import { type SpaceCommitRows } from "../workspace-space-commit.js";
12
14
  import * as facts from "../capability-facts.js";
13
15
  import { type TaskAttachmentStore } from "../plugins/task-attachment-store.js";
@@ -76,6 +78,10 @@ export declare function createSessionScopedRegistries(maxEntries?: number): Sess
76
78
  */
77
79
  export declare function createK8sRunEnvFactory(k8sCfg: K8sEnvConfig, seats: {
78
80
  imageOf(sessionId: string): string | undefined;
81
+ /** The member git-credential broker seat for the pod's run (`runId` = the one reading of the run that owns the pod, the same value its
82
+ * `sema.io/run` label carries; `sandbox-git-plane.ts` `sandboxBrokerSeatOf` over the boot's plane reading and the run row's identity);
83
+ * absent seat / `undefined` answer ⇒ the pod carries no broker. */
84
+ brokerSeatOf?(runId: string | undefined): Promise<K8sEnvConfig["sandboxBroker"]>;
79
85
  /** The session store's by-id head read (`SessionStore.headOf`, picked once by `headReadOf`); the occupant is its live-occupant projection
80
86
  * (`sessionFaceOfHead`). Absent ⇔ the store has none, which `assertWorkspaceSpaceReadable` refuses together with `space` (embeddings that
81
87
  * wire another lane pass none). */
@@ -84,6 +90,17 @@ export declare function createK8sRunEnvFactory(k8sCfg: K8sEnvConfig, seats: {
84
90
  * or without a store backend — the latter `assertWorkspaceSpaceReadable` refuses together with `space`. */
85
91
  spaceCommits(): SpaceCommitRows | undefined;
86
92
  }, deps?: K8sEnvDeps): ReturnType<typeof k8sExecutionEnvFactory>;
93
+ /**
94
+ * k8s 车道沙箱秘密座的**起服前提**(装配后的真门,第 5 段 await)。法只有一条:**秘密座非空(`K8S_SANDBOX_ENV` 点到了本进程环境里有的键)⇒
95
+ * 问一次 SelfSubjectAccessReview(env 座那一行动词 {@link SANDBOX_SECRET_SEAT_VERBS}`.env`,沙箱命名空间)** —— 只问这一座自己发的调用,
96
+ * 不问租约座的 `patch`(那一座由凭据平面的判官在它的前提都成立时另问,见 `readSandboxGitPlane`):
97
+ * · 答了「不许」(任一动词)⇒ 拒启,一行拒句点名缺的动词、命名空间、Role 规则原文与出路(`[sandbox-secret-rbac]`)—— 不等第一个 run 才失败;
98
+ * · 问不到(请求失败 / 审查本身被拒 / 答复没有 `status.allowed`)⇒ 照常起服:F 类兜底口 `server.sandbox-secret.seat-unverified` 计数 + 一行
99
+ * `sandbox_secret_seat_unverified`。放行的最坏后果:RBAC 真缺时第一个要建 Secret 的 run 连接即拒(`auth_failed`,pod 被删)—— 秘密值仍然
100
+ * 只经 Secret,不回落明文;
101
+ * · 秘密座空 ⇒ 一个请求都不发(没有 Secret 要建)。
102
+ */
103
+ export declare function ensureSandboxSecretSeat(k8sCfg: K8sEnvConfig, logger: Pick<Logger, "warn">, deps?: K8sEnvDeps, read?: () => Promise<SandboxSecretSeat>): Promise<void>;
87
104
  /**
88
105
  * k8s 工作区空间的**起服前提**(装配后的真门;同段先例 = `assertDeviceLaneWired`)。法只有一条:
89
106
  * **生效策略 `space`(事实 `workspaceSpace`)∧ {@link createK8sRunEnvFactory} 的两只座缺一 ⇒ 拒启。**
@@ -119,6 +136,9 @@ export interface ExecutionEnvCtx {
119
136
  spaceCommits?: () => SpaceCommitRows | undefined;
120
137
  /** k8s 工作区空间的住客读:会话店的按 id 读(`headReadOf(sessionStore)`,第 5 段判一次、与起服门同一只)。不传 ⇒ 同上,不开、响亮。 */
121
138
  headOf?: HeadRead;
139
+ /** run 店的按 id 读(成员凭据代理座读 run 行上受理时记下的身份元组 `RunRecord.identity`)。取值闭包:run 店第 7 段才绑(`late-binds.ts`),
140
+ * 平面开着时每个 run 的 pod 建起前求值一次。不传 / 答 `undefined`(没有存储后端)⇒ 读不到身份 ⇒ 不挂代理。 */
141
+ runRows?: () => SandboxGitRunRows | undefined;
122
142
  }
123
143
  export declare function createExecutionEnv(ctx: ExecutionEnvCtx): {
124
144
  perTaskImage: PerTaskImageRegistry;
@@ -134,5 +154,8 @@ export declare function createExecutionEnv(ctx: ExecutionEnvCtx): {
134
154
  sendUserFileTaskEnvs: TaskEnvRegistry | undefined;
135
155
  lspManager: import("@sema-agent/core").NodeLspManager | import("../lsp/manager.js").E2bLspManager | undefined;
136
156
  spaceReclaimer: SpaceReclaimer | undefined;
157
+ sandboxSecretSeatGate: (() => Promise<void>) | undefined;
158
+ sandboxPodReaper: SandboxPodReaper | undefined;
159
+ readSandboxGitPlane: () => Promise<SandboxGitPlane>;
137
160
  };
138
161
  //# sourceMappingURL=execution-env.d.ts.map
@@ -11,7 +11,11 @@ import { adbExecutionEnvFactory } from "../plugins/remote-env-adb.js";
11
11
  import { deviceExecutionEnvFactory } from "../plugins/remote-env-device.js";
12
12
  import { e2bExecutionEnvFactory } from "../plugins/remote-env-e2b.js";
13
13
  import { hostExecutionEnvFactory, RemoteHostExecutionEnv } from "../plugins/remote-env-host.js";
14
- import { boundedStoreCall, checkSandboxEgressPolicy, createSpaceReclaimer, K8S_SANDBOX_NAMESPACE_DEFAULT, k8sExecutionEnvFactory, rpcTimeoutMsOf } from "../plugins/remote-env-k8s.js";
14
+ import { boundedStoreCall, checkSandboxEgressPolicy, createSandboxPodApi, createSpaceReclaimer, K8S_SANDBOX_NAMESPACE_DEFAULT, k8sExecutionEnvFactory, readSandboxSecretSeat, rpcTimeoutMsOf } from "../plugins/remote-env-k8s.js";
15
+ import { podSpecPatchBrokerRefusal, SANDBOX_SECRET_SEAT_VERBS } from "../plugins/k8s-exec-protocol.js";
16
+ import { judgeSandboxGitPlane, runIdentitySourceOf, sandboxBrokerSeatOf, sandboxGitPlaneAsksSecretSeat, sandboxGitPlanePremisesOf } from "../sandbox-git-plane.js";
17
+ import { createSandboxPodReaper } from "../plugins/k8s-pod-reaper.js";
18
+ import { recordFailOpen } from "../observability/fail-open.js";
15
19
  import { createRunWorkspaceSnapshotSeat } from "../fleet/workspace-snapshot-residual.js";
16
20
  import { createSpaceCommitFence } from "../workspace-space-commit.js";
17
21
  import { localDockerExecutionEnvFactory } from "../plugins/remote-env-local-docker.js";
@@ -58,7 +62,12 @@ function forwardedSandboxEnvKeys(remoteExec) {
58
62
  }
59
63
  }
60
64
  function buildSandboxLaneEnv(config, explicit) {
61
- const merged = { ...derivePkgSourceEnv(config.sandboxPkgSource, config.sandboxPkgSourceCustom), ...(explicit ?? {}) };
65
+ const secretEnv = { ...(explicit ?? {}) };
66
+ const env = Object.fromEntries(Object.entries(derivePkgSourceEnv(config.sandboxPkgSource, config.sandboxPkgSourceCustom)).filter(([name]) => !Object.hasOwn(secretEnv, name)));
67
+ return { ...(Object.keys(env).length > 0 ? { env } : {}), ...(Object.keys(secretEnv).length > 0 ? { secretEnv } : {}) };
68
+ }
69
+ function sandboxLaneEnvMerged(lane) {
70
+ const merged = { ...(lane.env ?? {}), ...(lane.secretEnv ?? {}) };
62
71
  return Object.keys(merged).length > 0 ? merged : undefined;
63
72
  }
64
73
  const MAX_CWD_SESSIONS = 4096;
@@ -118,6 +127,8 @@ export function createK8sRunEnvFactory(k8sCfg, seats, deps) {
118
127
  const child = ctx.placementRootSessionId !== ctx.sessionId;
119
128
  const s3Snapshot = child && k8sCfg.s3Snapshot !== undefined ? spaceOff(k8sCfg.s3Snapshot) : k8sCfg.s3Snapshot;
120
129
  const spaceOccupant = child ? undefined : await occupantOf(seats, ctx.sessionId, ctx.taskId, rpcTimeoutMsOf(k8sCfg));
130
+ const runId = !child && ctx.taskId !== undefined ? ctx.taskId : undefined;
131
+ const sandboxBroker = await seats.brokerSeatOf?.(runId);
121
132
  const { cache, ...deploymentCfg } = k8sCfg;
122
133
  return k8sExecutionEnvFactory({
123
134
  ...deploymentCfg,
@@ -125,6 +136,8 @@ export function createK8sRunEnvFactory(k8sCfg, seats, deps) {
125
136
  ...(s3Snapshot !== undefined ? { s3Snapshot } : {}),
126
137
  image: seats.imageOf(ctx.sessionId) ?? k8sCfg.image,
127
138
  ...(spaceOccupant !== undefined ? { spaceOccupant } : {}),
139
+ ...(runId !== undefined ? { runId } : {}),
140
+ ...(sandboxBroker !== undefined ? { sandboxBroker } : {}),
128
141
  onWorkspaceSnapshot,
129
142
  }, deps)(ctx);
130
143
  };
@@ -156,6 +169,34 @@ function spaceOff(store) {
156
169
  const { space: _off, ...parkOnly } = store;
157
170
  return parkOnly;
158
171
  }
172
+ export async function ensureSandboxSecretSeat(k8sCfg, logger, deps, read = () => readSandboxSecretSeat(k8sCfg, "env", deps)) {
173
+ const names = Object.keys(k8sCfg.podSecretEnv ?? {});
174
+ if (names.length === 0)
175
+ return;
176
+ const namespace = k8sCfg.namespace ?? K8S_SANDBOX_NAMESPACE_DEFAULT;
177
+ const seat = await read();
178
+ switch (seat.state) {
179
+ case "granted":
180
+ return;
181
+ case "denied":
182
+ throw new Error(`K8S_SANDBOX_ENV forwards ${names.length} variable(s) into the k8s sandbox, but this service account may not ${seat.verbs.join(", ")} secrets ` +
183
+ `in namespace ${namespace} [sandbox-secret-rbac]: the forwarded values reach a sandbox pod only through that pod's own Secret (the pod spec carries ` +
184
+ `references, never the values). Add to the sandbox Role in ${namespace}: \`- apiGroups: [""] resources: [secrets] verbs: [${SANDBOX_SECRET_SEAT_VERBS.env.join(", ")}]\` ` +
185
+ "(or unset K8S_SANDBOX_ENV), and restart");
186
+ case "unverified":
187
+ recordFailOpen("server.sandbox-secret.seat-unverified", `namespace=${namespace} ${seat.detail}`);
188
+ logger.warn("sandbox_secret_seat_unverified", {
189
+ namespace,
190
+ detail: seat.detail,
191
+ effect: `the boot could not confirm the sandbox Secret RBAC (secrets ${SANDBOX_SECRET_SEAT_VERBS.env.join(" / ")}) — the service starts; if the Role lacks them, the first run that forwards K8S_SANDBOX_ENV is refused at connect (auth_failed) and its pod deleted — the values never go into the pod spec`,
192
+ });
193
+ return;
194
+ default: {
195
+ const unreachable = seat;
196
+ throw new Error(`unhandled sandbox secret seat reading: ${String(unreachable.state)}`);
197
+ }
198
+ }
199
+ }
159
200
  export function assertWorkspaceSpaceReadable(space, seats, cache, sessionStoreLabel) {
160
201
  if (space.available && persistNeedsOccupant(space.policy.persist)) {
161
202
  const missing = seats.headOf === undefined ? `the session store (${sessionStoreLabel}) has no by-id occupant read (headOf)`
@@ -207,6 +248,10 @@ export function createExecutionEnv(ctx) {
207
248
  const { perSessionInheritEnv, setSessionInheritEnv } = sessionScoped;
208
249
  let executionEnvFactory;
209
250
  let spaceReclaimer;
251
+ let sandboxSecretSeatGate;
252
+ let sandboxPodReaper;
253
+ const planeCell = {};
254
+ let readSandboxGitPlane = async () => (planeCell.current = judgeSandboxGitPlane(sandboxGitPlanePremisesOf(config, {})));
210
255
  let worktreeReap;
211
256
  if (config.remoteExec?.provider === "e2b") {
212
257
  executionEnvFactory = e2bExecutionEnvFactory({
@@ -217,7 +262,7 @@ export function createExecutionEnv(ctx) {
217
262
  ...(config.remoteExec.livenessMs != null ? { livenessMs: config.remoteExec.livenessMs } : {}),
218
263
  ...(config.remoteExec.allowInternetAccess != null ? { allowInternetAccess: config.remoteExec.allowInternetAccess } : {}),
219
264
  ...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
220
- ...((env) => (env !== undefined ? { sandboxEnv: env } : {}))(buildSandboxLaneEnv(config, config.remoteExec.sandboxEnv)),
265
+ ...((env) => (env !== undefined ? { sandboxEnv: env } : {}))(sandboxLaneEnvMerged(buildSandboxLaneEnv(config, config.remoteExec.sandboxEnv))),
221
266
  logger,
222
267
  metrics,
223
268
  });
@@ -243,11 +288,29 @@ export function createExecutionEnv(ctx) {
243
288
  ...((f) => (f.available ? { cache: f.cache } : {}))(facts.workspaceCache(config)),
244
289
  ...(config.remoteExec.workspaceSnapshotExclude ? { snapshotExclude: config.remoteExec.workspaceSnapshotExclude } : {}),
245
290
  logger,
246
- ...((env) => (env !== undefined ? { podEnv: env } : {}))(buildSandboxLaneEnv(config, config.remoteExec.sandboxEnv)),
291
+ ...((lane) => ({ ...(lane.env !== undefined ? { podEnv: lane.env } : {}), ...(lane.secretEnv !== undefined ? { podSecretEnv: lane.secretEnv } : {}) }))(buildSandboxLaneEnv(config, config.remoteExec.sandboxEnv)),
247
292
  };
248
- executionEnvFactory = createK8sRunEnvFactory(k8sCfg, { imageOf: (sid) => perTaskImage.get(sid), headOf: ctx.headOf, spaceCommits: ctx.spaceCommits ?? (() => undefined) });
293
+ const brokerImage = config.remoteExec.brokerImage;
294
+ executionEnvFactory = createK8sRunEnvFactory(k8sCfg, {
295
+ imageOf: (sid) => perTaskImage.get(sid),
296
+ headOf: ctx.headOf,
297
+ spaceCommits: ctx.spaceCommits ?? (() => undefined),
298
+ brokerSeatOf: async (runId) => {
299
+ const plane = planeCell.current;
300
+ if (plane === undefined || plane.state !== "on")
301
+ return undefined;
302
+ return sandboxBrokerSeatOf(plane, brokerImage, await runIdentitySourceOf(runId, ctx.runRows?.(), logger));
303
+ },
304
+ });
249
305
  if (k8sCfg.cache !== undefined || k8sCfg.s3Snapshot !== undefined)
250
306
  spaceReclaimer = createSpaceReclaimer(k8sCfg);
307
+ sandboxSecretSeatGate = () => ensureSandboxSecretSeat(k8sCfg, logger, undefined, () => readSandboxSecretSeat(k8sCfg, "env"));
308
+ readSandboxGitPlane = async () => {
309
+ const premises = sandboxGitPlanePremisesOf(config, { ...((r) => (r !== undefined ? { patchRefusal: r } : {}))(podSpecPatchBrokerRefusal(k8sCfg.podSpecPatch)) });
310
+ const secretSeat = sandboxGitPlaneAsksSecretSeat(premises) ? await readSandboxSecretSeat(k8sCfg, "lease") : undefined;
311
+ return (planeCell.current = judgeSandboxGitPlane({ ...premises, secretSeat }));
312
+ };
313
+ sandboxPodReaper = createSandboxPodReaper({ api: createSandboxPodApi(k8sCfg), logger, metrics });
251
314
  void checkSandboxEgressPolicy(k8sCfg)
252
315
  .then((gaps) => sayEgressPolicyReading(gaps, logger, k8sCfg.namespace ?? K8S_SANDBOX_NAMESPACE_DEFAULT))
253
316
  .catch((e) => logger.error("sandbox_egress_policy_check_crashed", { error: e instanceof Error ? e.message : String(e), effect: "the sandbox egress self-check did not complete this boot — the lane runs; the egress policy was not read" }));
@@ -446,7 +509,7 @@ export function createExecutionEnv(ctx) {
446
509
  perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, setSessionInheritEnv,
447
510
  fenceSessionOwner: sessionScoped.fenceSessionOwner,
448
511
  dropSessionScoped: sessionScoped.dropSession,
449
- executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, spaceReclaimer,
512
+ executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, spaceReclaimer, sandboxSecretSeatGate, sandboxPodReaper, readSandboxGitPlane,
450
513
  };
451
514
  }
452
515
  //# sourceMappingURL=execution-env.js.map
@@ -25,6 +25,7 @@ import type { FileHistoryErasureFace } from "../file-history-capture.js";
25
25
  import type { ApprovalNonceStore, CheckpointStoreFull, CostQuotaStore, ImageBake, RateLimiterStore, ServiceWorkflowJournalStore, StoreBackend, ToolResultStoreFull } from "../plugins/store-backend.js";
26
26
  import type { ToolApprovalCoordinator } from "../tool-approval.js";
27
27
  import type { ParkCommitWatch } from "../plugins/park-commit-watch.js";
28
+ import { type SandboxPodReaper } from "../plugins/k8s-pod-reaper.js";
28
29
  import { type AuditRotationPort } from "../approval-ask-retention.js";
29
30
  /** [ref] / [ref]§五(sema-comms `audits/failopen-governance-176.md` §7 "Z 类 1 条"):default consecutive-failure
30
31
  * count before a reaper leg's swallowed error gets a `warn` line. The ~15 `.catch(() => undefined)` sites
@@ -113,6 +114,8 @@ export interface ReapersCtx {
113
114
  approvalAskAudit: AuditRotationPort | undefined;
114
115
  /** 晚绑(server 造出来才有)——见文件头「位置即契约」①。 */
115
116
  getRunDenySweep: () => ((now: number) => Promise<void>) | undefined;
117
+ /** k8s 车道的沙箱 Pod reaper(`plugins/k8s-pod-reaper.ts`;第 5 段与 run 工厂同一份 k8s 配置铸)。缺席 = 非 k8s 车道 ⇒ 本腿零调用。 */
118
+ sandboxPodReaper?: SandboxPodReaper;
116
119
  }
117
120
  /** core 5.48 `reapDurableAgents` 的 `deps.sessions` 面(结构形,只列本仓要填的三格)。 */
118
121
  export interface ReapSessionsFacade {
@@ -4,6 +4,7 @@ import { CostQuota } from "../observability/cost-quota.js";
4
4
  import { isAdoptionQuiescing, trackMaintenanceLeg } from "../adoption/quiesce.js";
5
5
  import { createApprovalReconciler } from "../approval-reconciler.js";
6
6
  import { createE2bOrphanSandboxProvider, reclaimOrphanE2bSandboxes, E2B_ORPHAN_GRACE_MS } from "../plugins/e2b-orphan-reclaim.js";
7
+ import { SANDBOX_POD_REAP_PERIOD_MS } from "../plugins/k8s-pod-reaper.js";
7
8
  import { APPROVAL_ASK_RETENTION_PASS_MS, runApprovalAskRetentionPass } from "../approval-ask-retention.js";
8
9
  export const REAPER_FAILURE_WARN_THRESHOLD = 5;
9
10
  export function createThrottledReaperCatch(name, logger, threshold = REAPER_FAILURE_WARN_THRESHOLD) {
@@ -57,7 +58,7 @@ export function createPlacementAwareReapSessions(deps) {
57
58
  };
58
59
  }
59
60
  export function startReapers(ctx) {
60
- const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, parkCommits, rateLimiter, costQuota, toolResultStore, fileHistoryErasure, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, approvalNonceStore, getRunDenySweep, userInputLedger, approvalAskAudit, } = ctx;
61
+ const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, parkCommits, rateLimiter, costQuota, toolResultStore, fileHistoryErasure, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, approvalNonceStore, getRunDenySweep, userInputLedger, approvalAskAudit, sandboxPodReaper, } = ctx;
61
62
  const leg = (p) => {
62
63
  if (p !== undefined)
63
64
  trackMaintenanceLeg(p);
@@ -73,6 +74,8 @@ export function startReapers(ctx) {
73
74
  : undefined;
74
75
  let e2bOrphanLastRunMs = Date.now() - E2B_ORPHAN_GRACE_MS;
75
76
  let e2bOrphanInFlight = false;
77
+ let sandboxPodReapLastRunMs = Date.now() - SANDBOX_POD_REAP_PERIOD_MS;
78
+ let sandboxPodReapInFlight = false;
76
79
  let wfRunReapInFlight = false;
77
80
  let attachmentSweepInFlight = false;
78
81
  let bgAgentReapInFlight = false;
@@ -80,6 +83,7 @@ export function startReapers(ctx) {
80
83
  const probeClockSkewGuard = createThrottledReaperCatch("probe_clock_skew", logger);
81
84
  const runsReapStaleGuard = createThrottledReaperCatch("runs_reap_stale", logger);
82
85
  const e2bOrphanGuard = createThrottledReaperCatch("e2b_orphan_reclaim", logger);
86
+ const sandboxPodReapGuard = createThrottledReaperCatch("sandbox_pod_reap", logger);
83
87
  const checkpointsReapExpiredGuard = createThrottledReaperCatch("checkpoints_reap_expired", logger);
84
88
  const approvalNonceReapExpiredGuard = createThrottledReaperCatch("approval_nonce_reap_expired", logger);
85
89
  const runsFailSuspendedExpiredCheckpointGuard = createThrottledReaperCatch("runs_fail_suspended_expired_checkpoint", logger);
@@ -366,6 +370,16 @@ export function startReapers(ctx) {
366
370
  e2bOrphanInFlight = false;
367
371
  }));
368
372
  }
373
+ if (sandboxPodReaper && Date.now() - sandboxPodReapLastRunMs >= SANDBOX_POD_REAP_PERIOD_MS && !sandboxPodReapInFlight) {
374
+ sandboxPodReapInFlight = true;
375
+ sandboxPodReapLastRunMs = Date.now();
376
+ leg(sandboxPodReaper
377
+ .reapOnce(runStore)
378
+ .then(() => sandboxPodReapGuard.onSuccess(), sandboxPodReapGuard.onError)
379
+ .finally(() => {
380
+ sandboxPodReapInFlight = false;
381
+ }));
382
+ }
369
383
  }, config.reapIntervalSec * 1000);
370
384
  reaper.unref?.();
371
385
  return reaper;
@@ -2,6 +2,7 @@ import { type MemoryBackend } from "@sema-agent/core";
2
2
  import { SessionEnvironmentSelection, selectEnvironmentTool } from "../capabilities/select-environment-tool.js";
3
3
  import { sendUserFileTool } from "../capabilities/send-user-file-tool.js";
4
4
  import { selectScenario } from "../capabilities/scenarios.js";
5
+ import { type LoadedSkill } from "../capabilities/skills.js";
5
6
  import type { ServiceConfig } from "../config.js";
6
7
  import { type LiveQuestionFace } from "../deployment-governance.js";
7
8
  import { FleetEventBus } from "../fleet/fleet-bus.js";
@@ -83,6 +84,9 @@ export interface ResolveSpecCtx {
83
84
  /** [ref] 件C:本部署已校验的锁集(boot/governance-seams.ts 的产物,与 `RunnerDeps.lockedConfig`
84
85
  * 同源一次解析)。空集 = 无锁 = 现行为。本域只用它做**同步拒面**(§4.3),不做第二次裁决。 */
85
86
  lockedKeys: ReadonlySet<import("@sema-agent/core").LockedKey>;
87
+ /** 本部署**装载簿**(stage-07 的出厂技能 + 配置源覆盖之后的终表,场景工厂读的同一份)。本装配器只拿它答「场景交来的这只技能来自哪里」
88
+ * (run 名册 `wiring_manifest.skills[].source`,`buildRunSkills`);名字在装载簿里唯一(装载器同名即抛、配置源按名覆盖)。 */
89
+ loadedSkills: readonly LoadedSkill[];
86
90
  }
87
91
  export declare function createResolveSpec(ctx: ResolveSpecCtx): ServiceDeps["resolveSpec"];
88
92
  export {};