@sema-agent/server 7.142.0 → 7.144.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 (301) hide show
  1. package/MIGRATION.md +608 -0
  2. package/USAGE.md +63 -36
  3. package/dist/adoption/plan.js +22 -0
  4. package/dist/approval-ask-audit-store.d.ts +7 -5
  5. package/dist/approval-ask-audit-store.js +12 -6
  6. package/dist/approval-ask-machine.d.ts +15 -7
  7. package/dist/approval-ask-machine.js +10 -4
  8. package/dist/approval-card.d.ts +155 -39
  9. package/dist/approval-card.js +111 -31
  10. package/dist/approval-queue-kind.d.ts +27 -0
  11. package/dist/approval-queue-kind.js +6 -0
  12. package/dist/approval-reconciler.d.ts +8 -8
  13. package/dist/approval-reconciler.js +3 -3
  14. package/dist/audit.js +4 -2
  15. package/dist/boot/budget-tracing.d.ts +1 -1
  16. package/dist/boot/execution-env.js +1 -1
  17. package/dist/boot/governance-seams.d.ts +26 -41
  18. package/dist/boot/governance-seams.js +61 -28
  19. package/dist/boot/late-binds.d.ts +7 -4
  20. package/dist/boot/late-binds.js +3 -1
  21. package/dist/boot/leader.js +2 -4
  22. package/dist/boot/memory-consolidation.d.ts +0 -8
  23. package/dist/boot/memory-consolidation.js +3 -3
  24. package/dist/boot/reapers.js +5 -4
  25. package/dist/boot/resolve-spec.d.ts +1 -1
  26. package/dist/boot/resolve-spec.js +8 -8
  27. package/dist/boot/runner-deps.d.ts +14 -6
  28. package/dist/boot/runner-deps.js +6 -17
  29. package/dist/boot/shutdown.d.ts +9 -0
  30. package/dist/boot/shutdown.js +18 -2
  31. package/dist/boot/stage-01-config.js +2 -0
  32. package/dist/boot/stage-02-web-admin-pull.js +3 -0
  33. package/dist/boot/stage-03-store-backend.d.ts +4 -1
  34. package/dist/boot/stage-03-store-backend.js +1 -2
  35. package/dist/boot/stage-04-stores.d.ts +5 -2
  36. package/dist/boot/stage-04-stores.js +3 -5
  37. package/dist/boot/stage-05-execution-env.d.ts +5 -2
  38. package/dist/boot/stage-06-runners.d.ts +5 -2
  39. package/dist/boot/stage-06-runners.js +1 -3
  40. package/dist/boot/stage-07-capability-layer.d.ts +5 -2
  41. package/dist/boot/stage-07-capability-layer.js +6 -2
  42. package/dist/boot/stage-08-reapers.d.ts +5 -2
  43. package/dist/boot/stage-09-leader.d.ts +5 -2
  44. package/dist/boot/stage-10-http-server.d.ts +5 -2
  45. package/dist/boot/stage-10-http-server.js +4 -1
  46. package/dist/boot/stores.d.ts +5 -2
  47. package/dist/boot/stores.js +7 -7
  48. package/dist/boot/web-admin.d.ts +5 -1
  49. package/dist/boot/web-admin.js +31 -19
  50. package/dist/capabilities/hands-lane.d.ts +9 -39
  51. package/dist/capabilities/hands-lane.js +8 -13
  52. package/dist/capabilities/mcp-probe.d.ts +3 -2
  53. package/dist/capabilities/scenarios.d.ts +23 -17
  54. package/dist/capabilities/scenarios.js +2 -2
  55. package/dist/capabilities/skills.d.ts +1 -17
  56. package/dist/capabilities/skills.js +0 -8
  57. package/dist/capability-facts.d.ts +8 -8
  58. package/dist/capability-facts.js +4 -4
  59. package/dist/card-void.d.ts +6 -9
  60. package/dist/config-catalog.js +1 -1
  61. package/dist/config-d-migration.d.ts +2 -0
  62. package/dist/config-dir-trust.js +58 -29
  63. package/dist/config-invariants.d.ts +7 -1
  64. package/dist/config-invariants.js +7 -0
  65. package/dist/config-provider.d.ts +10 -0
  66. package/dist/config-provider.js +15 -4
  67. package/dist/config-root-migration.d.ts +13 -0
  68. package/dist/config-root-migration.js +99 -0
  69. package/dist/config-types.d.ts +5 -5
  70. package/dist/config.d.ts +4 -3
  71. package/dist/config.js +1 -1
  72. package/dist/delegated-session-owner.d.ts +35 -151
  73. package/dist/delegated-session-owner.js +1 -83
  74. package/dist/device-ws-protocol.d.ts +1 -0
  75. package/dist/device-ws-protocol.js +2 -1
  76. package/dist/entry-mode-clamp-notice.d.ts +1 -1
  77. package/dist/env-model-table-gate.d.ts +53 -0
  78. package/dist/env-model-table-gate.js +53 -0
  79. package/dist/execution-placement.d.ts +7 -4
  80. package/dist/fleet/fleet-bus.d.ts +28 -52
  81. package/dist/fleet/fleet-bus.js +35 -100
  82. package/dist/fleet/fleet-reconciler.d.ts +4 -4
  83. package/dist/fleet/fleet-reconciler.js +1 -1
  84. package/dist/fleet/subagent-tail-bus.d.ts +14 -11
  85. package/dist/fleet/subagent-tail-bus.js +15 -28
  86. package/dist/hooks/classic-stations.d.ts +3 -1
  87. package/dist/hooks/classic-stations.js +2 -0
  88. package/dist/hooks/hook-runner.js +28 -7
  89. package/dist/host-decision.d.ts +54 -20
  90. package/dist/host-decision.js +42 -14
  91. package/dist/http/admission.d.ts +1 -1
  92. package/dist/http/admission.js +7 -3
  93. package/dist/http/approval-sweeps.js +11 -8
  94. package/dist/http/caller-trust.d.ts +1 -1
  95. package/dist/http/durable-run-leg.js +2 -2
  96. package/dist/http/engine-refusal.d.ts +16 -0
  97. package/dist/http/engine-refusal.js +17 -0
  98. package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
  99. package/dist/http/memory-compliance-failure-reply.js +4 -6
  100. package/dist/http/owner-access.d.ts +11 -0
  101. package/dist/http/owner-access.js +13 -0
  102. package/dist/http/principal-gate.d.ts +1 -1
  103. package/dist/http/refusal-body.d.ts +7 -3
  104. package/dist/http/refusal-body.js +1 -0
  105. package/dist/http/resume-legs.d.ts +21 -7
  106. package/dist/http/resume-legs.js +82 -53
  107. package/dist/http/resume-restatement.d.ts +39 -0
  108. package/dist/http/resume-restatement.js +25 -0
  109. package/dist/http/route-ctx.d.ts +35 -10
  110. package/dist/http/route-ctx.js +1 -1
  111. package/dist/http/routes/approvals-assistant.d.ts +9 -1
  112. package/dist/http/routes/approvals-assistant.js +143 -31
  113. package/dist/http/routes/capabilities.js +11 -2
  114. package/dist/http/routes/memory-policy.d.ts +2 -2
  115. package/dist/http/routes/memory-policy.js +1 -1
  116. package/dist/http/routes/notify-wake.js +23 -25
  117. package/dist/http/routes/rules.js +48 -13
  118. package/dist/http/routes/runs.js +9 -8
  119. package/dist/http/routes/session-memory-sync.d.ts +34 -6
  120. package/dist/http/routes/session-memory-sync.js +27 -10
  121. package/dist/http/routes/session-sync.js +13 -20
  122. package/dist/http/routes/sessions.d.ts +2 -2
  123. package/dist/http/routes/sessions.js +22 -23
  124. package/dist/http/routes/tasks.js +27 -49
  125. package/dist/http/server.d.ts +13 -7
  126. package/dist/http/server.js +2 -2
  127. package/dist/http/stored-body-replay.js +15 -4
  128. package/dist/http/wire-types.d.ts +17 -7
  129. package/dist/index.d.ts +1 -0
  130. package/dist/index.js +1 -0
  131. package/dist/lane-home-probe.d.ts +15 -0
  132. package/dist/lane-home-probe.js +4 -0
  133. package/dist/leader/leader.js +5 -2
  134. package/dist/leader/wire.d.ts +7 -1
  135. package/dist/leader/wire.js +6 -5
  136. package/dist/leg-facts.d.ts +37 -0
  137. package/dist/leg-facts.js +15 -0
  138. package/dist/memory-bundle-engine.d.ts +1 -4
  139. package/dist/memory-bundle-engine.js +2 -2
  140. package/dist/memory-operator-faces.d.ts +30 -66
  141. package/dist/memory-operator-faces.js +17 -88
  142. package/dist/memory-scope-stores.d.ts +38 -40
  143. package/dist/memory-scope-stores.js +51 -36
  144. package/dist/memory-scope.d.ts +0 -9
  145. package/dist/memory-scope.js +0 -9
  146. package/dist/memory-sync-client.js +14 -3
  147. package/dist/memory-sync.d.ts +20 -4
  148. package/dist/memory-sync.js +23 -4
  149. package/dist/model-plane-face.d.ts +13 -7
  150. package/dist/model-plane-face.js +2 -1
  151. package/dist/observability/corrupt-read-seat.d.ts +5 -9
  152. package/dist/observability/corrupt-read-seat.js +1 -2
  153. package/dist/observability/fail-open.d.ts +30 -10
  154. package/dist/observability/fail-open.js +27 -10
  155. package/dist/observability/run-terminal.d.ts +1 -1
  156. package/dist/observability/run-terminal.js +6 -2
  157. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
  158. package/dist/orchestration/workflow-completion-inbox.js +0 -12
  159. package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
  160. package/dist/orchestration/workflow-park-decision.d.ts +8 -3
  161. package/dist/orchestration/workflow-park-decision.js +5 -3
  162. package/dist/org-delivery-gate.d.ts +11 -0
  163. package/dist/org-delivery-gate.js +19 -1
  164. package/dist/parked-decide.d.ts +13 -9
  165. package/dist/parked-decide.js +7 -7
  166. package/dist/parked-revive-surface.d.ts +6 -7
  167. package/dist/parked-revive-surface.js +1 -1
  168. package/dist/peer-inbound-face.d.ts +122 -0
  169. package/dist/peer-inbound-face.js +123 -0
  170. package/dist/permission-rule-list.d.ts +13 -9
  171. package/dist/permission-rule-list.js +7 -3
  172. package/dist/plugins/approval-ask-store-file.js +18 -6
  173. package/dist/plugins/approval-ask-store-memory.js +8 -6
  174. package/dist/plugins/approval-ask-store-sql.d.ts +28 -13
  175. package/dist/plugins/approval-ask-store-sql.js +30 -11
  176. package/dist/plugins/caching-session-store.d.ts +0 -3
  177. package/dist/plugins/caching-session-store.js +0 -3
  178. package/dist/plugins/checkpoint-store-sql.d.ts +45 -38
  179. package/dist/plugins/checkpoint-store-sql.js +108 -45
  180. package/dist/plugins/file-history-store-sql.d.ts +24 -18
  181. package/dist/plugins/file-history-store-sql.js +24 -19
  182. package/dist/plugins/file-run-store.d.ts +29 -0
  183. package/dist/plugins/file-run-store.js +97 -8
  184. package/dist/plugins/fork-routing-session-store.d.ts +8 -1
  185. package/dist/plugins/fork-routing-session-store.js +6 -0
  186. package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
  187. package/dist/plugins/imported-layer-root-retirement.js +96 -0
  188. package/dist/plugins/local-checkpoint-store.d.ts +6 -8
  189. package/dist/plugins/local-checkpoint-store.js +5 -4
  190. package/dist/plugins/local-session-store.d.ts +69 -30
  191. package/dist/plugins/local-session-store.js +133 -57
  192. package/dist/plugins/local-session-window.d.ts +4 -2
  193. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
  194. package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
  195. package/dist/plugins/mailbox-store-sql.d.ts +12 -8
  196. package/dist/plugins/mailbox-store-sql.js +76 -7
  197. package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
  198. package/dist/plugins/memory-sync-store-pg.js +21 -0
  199. package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
  200. package/dist/plugins/memory-sync-store-tidb.js +13 -1
  201. package/dist/plugins/one-time-migrations.d.ts +44 -14
  202. package/dist/plugins/one-time-migrations.js +118 -15
  203. package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
  204. package/dist/plugins/org-delivery-quota-store.js +2 -12
  205. package/dist/plugins/park-commit-watch.d.ts +1 -1
  206. package/dist/plugins/permission-rule-store-file.d.ts +10 -1
  207. package/dist/plugins/permission-rule-store-file.js +15 -2
  208. package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
  209. package/dist/plugins/permission-rule-store-sql.js +35 -2
  210. package/dist/plugins/pg-session-storage.d.ts +1 -9
  211. package/dist/plugins/pg-session-storage.js +2 -19
  212. package/dist/plugins/remote-env-device.d.ts +8 -11
  213. package/dist/plugins/remote-env-device.js +6 -3
  214. package/dist/plugins/remote-env-host.d.ts +8 -4
  215. package/dist/plugins/remote-env-host.js +7 -9
  216. package/dist/plugins/remote-env-k8s.d.ts +37 -24
  217. package/dist/plugins/remote-env-k8s.js +50 -17
  218. package/dist/plugins/remote-shell.js +12 -2
  219. package/dist/plugins/retention-store-sql.d.ts +2 -2
  220. package/dist/plugins/sandbox-read-floor.d.ts +5 -7
  221. package/dist/plugins/sandbox-read-floor.js +2 -7
  222. package/dist/plugins/schema-consistency.d.ts +5 -1
  223. package/dist/plugins/session-fork.d.ts +1 -1
  224. package/dist/plugins/session-ownership.d.ts +9 -0
  225. package/dist/plugins/session-ownership.js +6 -0
  226. package/dist/plugins/session-store.d.ts +13 -16
  227. package/dist/plugins/session-store.js +9 -10
  228. package/dist/plugins/sql-errors.d.ts +10 -1
  229. package/dist/plugins/sql-errors.js +13 -0
  230. package/dist/plugins/sql-json-column.d.ts +24 -2
  231. package/dist/plugins/sql-json-column.js +38 -3
  232. package/dist/plugins/store-backend.d.ts +23 -28
  233. package/dist/plugins/store-backend.js +12 -3
  234. package/dist/plugins/store-contracts.d.ts +29 -5
  235. package/dist/plugins/store-contracts.js +4 -1
  236. package/dist/plugins/task-attachment-store.d.ts +2 -2
  237. package/dist/plugins/task-attachment-store.js +37 -52
  238. package/dist/plugins/tidb-session-store.d.ts +2 -14
  239. package/dist/plugins/tidb-session-store.js +2 -20
  240. package/dist/plugins/tool-result-store-sql.d.ts +9 -8
  241. package/dist/plugins/tool-result-store-sql.js +23 -2
  242. package/dist/plugins/wiring-section-codec.d.ts +32 -0
  243. package/dist/plugins/wiring-section-codec.js +55 -0
  244. package/dist/plugins/workspace-fill.d.ts +1 -1
  245. package/dist/rule-entry-list.d.ts +3 -2
  246. package/dist/rule-entry-list.js +5 -1
  247. package/dist/rules-consent.d.ts +28 -5
  248. package/dist/rules-consent.js +40 -28
  249. package/dist/run-local.js +11 -6
  250. package/dist/runs.d.ts +12 -8
  251. package/dist/runs.js +10 -7
  252. package/dist/runtime-governance.d.ts +1 -1
  253. package/dist/security.d.ts +0 -4
  254. package/dist/server-secret-env.d.ts +4 -0
  255. package/dist/session-error-wire.d.ts +34 -23
  256. package/dist/session-error-wire.js +8 -3
  257. package/dist/session-incarnation.d.ts +3 -3
  258. package/dist/session-notify-door.d.ts +106 -0
  259. package/dist/session-notify-door.js +97 -0
  260. package/dist/session-sync.d.ts +9 -14
  261. package/dist/session-sync.js +5 -5
  262. package/dist/session-workspace.d.ts +3 -13
  263. package/dist/session-workspace.js +0 -9
  264. package/dist/standing-answer-grant.d.ts +5 -0
  265. package/dist/standing-answer-grant.js +10 -0
  266. package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
  267. package/dist/stores-sql/session-policy-store-sql.js +3 -13
  268. package/dist/task-mcp.d.ts +1 -1
  269. package/dist/task-settings.d.ts +11 -8
  270. package/dist/task-settings.js +31 -11
  271. package/dist/tool-approval.d.ts +55 -35
  272. package/dist/tool-approval.js +112 -74
  273. package/dist/trace/core-keyset-guard.d.ts +12 -9
  274. package/dist/trace/engine-notice-wire.d.ts +1 -1
  275. package/dist/trace/engine-notice-wire.js +4 -2
  276. package/dist/trace/exit-seat.d.ts +7 -1
  277. package/dist/trace/exit-seat.js +1 -0
  278. package/dist/trace/ledger-sink.d.ts +2 -2
  279. package/dist/trace/ledger-sink.js +12 -4
  280. package/dist/trace/model-segment.d.ts +74 -25
  281. package/dist/trace/model-segment.js +141 -32
  282. package/dist/trace/project.d.ts +15 -6
  283. package/dist/trace/project.js +12 -4
  284. package/dist/trace/redact.d.ts +72 -9
  285. package/dist/trace/redact.js +1509 -471
  286. package/dist/trace/self-minted-wire.d.ts +598 -47
  287. package/dist/trace/self-minted-wire.js +190 -20
  288. package/dist/trace/wire-projection-faces.d.ts +1 -1
  289. package/dist/trace/wire-projection-faces.js +1 -1
  290. package/dist/verifier-pause-card.d.ts +33 -5
  291. package/dist/verifier-pause-card.js +15 -10
  292. package/dist/web-admin-client/apply-effective.d.ts +12 -16
  293. package/dist/web-admin-client/apply-effective.js +90 -10
  294. package/dist/workspace-space.d.ts +19 -3
  295. package/dist/workspace-space.js +8 -4
  296. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
  297. package/package.json +3 -2
  298. package/wire-changes/server@7.143.0.json +1167 -0
  299. package/wire-changes/server@7.144.0.json +291 -0
  300. package/dist/session-records-skipped-notice.d.ts +0 -21
  301. package/dist/session-records-skipped-notice.js +0 -27
@@ -314,21 +314,18 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
314
314
  * 🔴 **工厂认设备不认住客**(读口一只 = 会话店执行席 `seat.read(root)`,与解析器 / 换绑门 / 执行放置读面 / 准入链同一只读):按 placement 根读**当前**格,
315
315
  * 与这条腿带来的设备句柄 `honor` 比,不同即 typed 拒。判序(一次行读):
316
316
  * root = ctx.placementRootSessionId // 空 ⇒ config.placement_root_invalid
317
- * honor = ctx.resumeWorkspace?.deviceId
317
+ * honor = ctx.placementTarget?.deviceId // 引擎交来的放置围栏(这条腿必须落在哪台设备上)
318
318
  * cell = seat.read(root).cell // 行不在 / 格缺 / mirror / writer(device, ∅) ⇒ device.not_bound(唯一读法 `boundDeviceOf`)
319
319
  * honor ≠ undefined ∧ 格上设备 ≠ honor ⇒ device.identity_mismatch(句点名两台,零连接)
320
320
  * readDeviceForAdmission(格上设备, 格上属主)
321
321
  * 工厂**不**读 `task_run`、不读子代分区行、不按 `ctx.taskId`、不比化身、不比属主(住客谓词只用于写与 judged 复核,不是工厂的读者)。
322
- * 续跑腿(core 7.54.0 [ref] r3 / [ref],`ExecutionEnvFactoryContext.resumeWorkspace`):行记下了设备 ⇒ 本工厂**只**在那台设备上铸 —— 根此刻绑的不是它(换绑过)
323
- * ⇒ `device.identity_mismatch`(换绑后赎回的同一个词,零新码);不连另一台、不改道(新设备没有那个工作区)。「绑的就是它但现在不在线」与新跑同形:
324
- * 构造成功、`connect()` 回 typed `device.offline`(从不连别的设备)。行没记设备(早于该字段的行)⇒ 引擎已命名的「行未记设备 ⇒ 普通路由」臂,按根读当前格。
325
- * 句柄(`workspaceHandle()`)恒报连接真到达的设备,不抄 `resumeWorkspace`。
326
- *
327
- * 🔴 **过渡期(条件句)**:新派后代腿的放置句柄(引擎在 `ExecutionEnvFactoryContext` 上的放置工作区字段,`honor` 的第二来源)**只在本仓所基的引擎版本带它时**
328
- * 进判序;本仓今天所基的引擎上下文没有这个字段 ⇒ `honor` 只取续跑腿的 `resumeWorkspace`,新派后代腿按根读当前格。本仓**不**注入任何替代见证、不造任何行
329
- * (修在源头)。敞口(在那个字段到货之前成立):活过父 turn 的后台子代,在根换绑之后新派的腿会落到根当前设备 —— 换绑半边由换绑门的子代活性判关闭(归换绑门那一车),
330
- * 删除半边另有票。字段到货后的形:`honor = ctx.resumeWorkspace?.deviceId ?? <放置句柄>.deviceId`,放置句柄的 provider 不是本车道 ⇒ 装配拒(`config.placement_root_invalid` 同级)。
331
- * 本仓任何文本不得宣称工厂「一律按住客定址」。
322
+ * 围栏(`ExecutionEnvFactoryContext.placementTarget`,引擎每条腿一个答案):派生它的那条腿的环境报过的目标 —— 委派子代 / 工作流 agent / 被唤醒的
323
+ * 保留 agent 带唤醒腿的目标,级联后续阶 / 验证腿带第一条腿的目标,续跑腿带检查点行记下的目标。围栏点名了设备 ⇒ 本工厂**只**在那台设备上铸 ——
324
+ * 根此刻绑的不是它(换绑过)⇒ `device.identity_mismatch`(换绑后赎回的同一个词,零新码);不连另一台、不改道(新设备没有那个工作区)。
325
+ * 「绑的就是它但现在不在线」与新跑同形:构造成功、`connect()` 回 typed `device.offline`(从不连别的设备)。围栏缺席(顶层首腿,或派生它的腿
326
+ * 不是远端环境)或不点名设备 ⇒ 普通路由,按根读当前格。围栏的 `provider` 不进判序(引擎的回执也只比设备)。
327
+ * 句柄(`workspaceHandle()`)恒报连接真到达的设备,不抄围栏;引擎拿这份报告复核每条带围栏的腿(不符 ⇒ 续跑腿 `resume.env_failed`、
328
+ * 其余腿 `config.placement_not_honored`,都在腿跑之前)—— 本工厂在连接之前就拒,那两个词只在报告与围栏真不一致时出现。
332
329
  */
333
330
  export declare function deviceExecutionEnvFactory(deps: DeviceEnvFactoryDeps): ExecutionEnvFactory;
334
331
  /**
@@ -17,6 +17,7 @@ export const DEVICE_OUTCOME_TO_FILE_ERROR = {
17
17
  is_directory: "is_directory",
18
18
  liveness_no_output: "unknown",
19
19
  name_too_long: "name_too_long",
20
+ symlink_loop: "symlink_loop",
20
21
  not_directory: "not_directory",
21
22
  not_found: "not_found",
22
23
  not_supported: "not_supported",
@@ -40,6 +41,7 @@ export const DEVICE_OUTCOME_TO_EXEC_ERROR = {
40
41
  is_directory: "unknown",
41
42
  liveness_no_output: "liveness_no_output",
42
43
  name_too_long: "unknown",
44
+ symlink_loop: "unknown",
43
45
  not_directory: "unknown",
44
46
  not_found: "unknown",
45
47
  not_supported: "shell_unavailable",
@@ -771,6 +773,7 @@ const OUTCOME_TO_REMOTE_ERROR = {
771
773
  is_directory: "unknown",
772
774
  liveness_no_output: "liveness_no_output",
773
775
  name_too_long: "unknown",
776
+ symlink_loop: "unknown",
774
777
  not_directory: "unknown",
775
778
  not_found: "unknown",
776
779
  not_supported: "unsupported",
@@ -806,10 +809,10 @@ export function deviceExecutionEnvFactory(deps) {
806
809
  throw deviceFactoryRefusal(DEVICE_ERROR_CODES.NOT_BOUND, `device lane: no device binding for placement root ${rootSessionId} [${DEVICE_ERROR_CODES.NOT_BOUND}] — ` +
807
810
  "the submit-time admission chain must bind the root session to a device before a run reaches the factory (v2 §4.3.2)");
808
811
  }
809
- const honor = ctx.resumeWorkspace?.deviceId;
812
+ const honor = ctx.placementTarget?.deviceId;
810
813
  if (honor !== undefined && honor !== binding.id) {
811
- throw deviceFactoryRefusal(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, `device lane: this resume rebuilds the workspace on device ${honor}, but placement root ${rootSessionId} is now bound to ${binding.id} [${DEVICE_ERROR_CODES.IDENTITY_MISMATCH}] — ` +
812
- "a parked call does not follow a rebind (the new device has no copy of that workspace); finish or cancel the parked task, then start it again on the device now bound");
814
+ throw deviceFactoryRefusal(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, `device lane: this leg is placed on device ${honor} (the device the work it continues ran on), but placement root ${rootSessionId} is now bound to ${binding.id} [${DEVICE_ERROR_CODES.IDENTITY_MISMATCH}] — ` +
815
+ "work already under way does not follow a rebind (the new device has no copy of that workspace); finish or cancel the running or parked task, then start it again on the device now bound");
813
816
  }
814
817
  const view = await deps.store.readDeviceForAdmission(binding.id, binding.owner);
815
818
  if (!view) {
@@ -71,11 +71,8 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
71
71
  cwd: string;
72
72
  /** core `ExecutionEnv.homeDir`([ref]):本机执行用户的 home —— 主张在场 ⇒ 主张(构造时与子进程拿到的 `HOME` 对证过),否则构造时读到的那个 `HOME`,见 {@link LaneHomeSeat}。 */
73
73
  get homeDir(): string | undefined;
74
- /** 家目录的座(主张在场 ⇒ 构造时对证 · 否则构造时实测)。读数 = {@link childHome}:本类自己组合给子进程的那份 env,不 spawn。 */
74
+ /** 家目录的座(主张在场 ⇒ 构造时对证,证不了 ⇒ 构造抛、环境不铸 · 否则构造时实测)。读数 = {@link childHome}:本类自己组合给子进程的那份 env,不 spawn。 */
75
75
  private readonly homeSeat;
76
- /** 家目录:主张与子进程拿到的 `HOME` 对不上 ⇒ 这只环境的拒句;`shellReady` / `fsReady` / `connect` 三道门原样回它,一条命令、一次文件动作都不做。
77
- * 读数是本类自己组合的 env(纯函数,同一份输入同一个答案)⇒ 构造时判一次、记一辈子。 */
78
- private readonly homeRefusal;
79
76
  /**
80
77
  * core 7.49.0 [ref]:shell 子进程**真拿到的** `CDPATH` 条目(`[]` = 没有)—— 删除守卫按它解裸 `cd <dir>`。本车道的命令就是本机子进程,
81
78
  * env 是本类自己组合的(继承姿态 + `cfg.env`),所以说得出:读同一只组合({@link childEnv} 的**读面**,不披露、不钉 shell),切法与 core
@@ -84,6 +81,13 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
84
81
  * {@link hostShell}(win32 上未落定同步抛 —— 修前每条 Bash 的策略链都因此被拒)。
85
82
  */
86
83
  get cdPath(): readonly string[];
84
+ /**
85
+ * core 7.62.0 [ref]([ref]):shell 子进程**收到的**环境变量名(从不带值)—— 引擎判「git 会不会读一只文本没绑的 `GIT_*`」的唯一来源(声明且没有 `GIT_*`
86
+ * ⇒ `git show HEAD:<path>` 的对象路径只在仓库顶判;缺席 ⇒ 每层祖先照判)。与 {@link cdPath} 同一只组合的读面(继承姿态 + `cfg.env`,不 spawn、不记剥除、不碰 shell);
87
+ * 逐命令的 `options.env` 由引擎自己交,它知道(core 残余:逐命令 env 加一个名)。win32 spawn 层钉的 `SHELL` 不在这一读里(不是 `GIT_*`,不改这一判)。
88
+ * 只有本车道声明:远端车道的 shell 跑在对方自己构建的环境里,适配器说不出(缺席)。
89
+ */
90
+ get receivedEnvNames(): readonly string[];
87
91
  /**
88
92
  * core 7.56.0 [ref]:shell 子进程继承的环境里**收不到**的条目(名 + 规则,从不带值)—— 与 {@link cdPath} 同一只组合的读面
89
93
  * (`server-secret-env.ts` 的 `withheldFromHostSubprocess`:继承组合扣下的发现,减去 `cfg.env` 这条显式授予重供的键)。逐命令的 `options.env`
@@ -13,7 +13,7 @@ import { IS_WIN32, resolveHostShell, hostShell, pinHostShell, spawnGroupOptions,
13
13
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
14
14
  import { FileError, NodeExecutionEnv, inheritEnvPolicyProblem, ExecutionError, RemoteExecutionError, RollingTailBuffer, walkSymlinkChain, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
15
15
  import { capabilitiesFor } from "../execution-lane-caps.js";
16
- import { LaneHomeSeat } from "../lane-home-probe.js";
16
+ import { LaneHomeSeat, laneHomeRefusal } from "../lane-home-probe.js";
17
17
  import { composeHostSubprocessEnv, hostSubprocessEnv, withheldFromHostSubprocess } from "../server-secret-env.js";
18
18
  import { nameTooLongCode } from "./remote-env-file-error.js";
19
19
  import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
@@ -64,11 +64,13 @@ export class RemoteHostExecutionEnv {
64
64
  return this.homeSeat.home;
65
65
  }
66
66
  homeSeat;
67
- homeRefusal;
68
67
  get cdPath() {
69
68
  const value = this.childEnv(undefined, undefined).CDPATH;
70
69
  return value === undefined || value === "" ? [] : value.split(":");
71
70
  }
71
+ get receivedEnvNames() {
72
+ return Object.entries(this.childEnv(undefined, undefined)).flatMap(([name, value]) => (value === undefined ? [] : [name]));
73
+ }
72
74
  get withheldEnv() {
73
75
  return withheldFromHostSubprocess({ policy: this.cfg.inheritEnv, allow: [] }, this.cfg.env);
74
76
  }
@@ -148,7 +150,9 @@ export class RemoteHostExecutionEnv {
148
150
  this.cwd = this.workspaceDir;
149
151
  this.handle = { sandboxId: this.workspaceDir, provider: PROVIDER, mountPath: this.workspaceDir };
150
152
  this.homeSeat = new LaneHomeSeat(PROVIDER, config, IS_WIN32 ? "win32" : "posix");
151
- this.homeRefusal = this.homeSeat.settle(this.childHome())?.refusal;
153
+ const homeVerdict = this.homeSeat.settle(this.childHome());
154
+ if (homeVerdict !== undefined)
155
+ throw laneHomeRefusal(homeVerdict);
152
156
  }
153
157
  workspaceHandle() {
154
158
  return this.handle;
@@ -158,8 +162,6 @@ export class RemoteHostExecutionEnv {
158
162
  return unsupported("connect(snapshotId)");
159
163
  if (this.destroyed)
160
164
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
161
- if (this.homeRefusal !== undefined)
162
- return { ok: false, error: new RemoteExecutionError("connect_failed", this.homeRefusal) };
163
165
  if (config?.abortSignal?.aborted)
164
166
  return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
165
167
  try {
@@ -1262,8 +1264,6 @@ export class RemoteHostExecutionEnv {
1262
1264
  async shellReady() {
1263
1265
  if (this.destroyed)
1264
1266
  return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
1265
- if (this.homeRefusal !== undefined)
1266
- return { ok: false, error: new ExecutionError("shell_unavailable", this.homeRefusal) };
1267
1267
  try {
1268
1268
  try {
1269
1269
  await resolveHostShell();
@@ -1295,8 +1295,6 @@ export class RemoteHostExecutionEnv {
1295
1295
  return (this.spoolRootDir !== undefined && inside(this.spoolRootDir)) || (this.persistent && inside(this.workspaceDir));
1296
1296
  }
1297
1297
  async fsReady(p, opts) {
1298
- if (this.homeRefusal !== undefined)
1299
- return { ok: false, error: new FileError("target_unavailable", this.homeRefusal, this.resolve(p)) };
1300
1298
  if (this.destroyed) {
1301
1299
  const lingering = opts?.lingeringBgWrite === true && this._bgManager?.hasShells() === true && this.isLingeringMirrorTarget(p);
1302
1300
  if (!lingering)
@@ -386,6 +386,7 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
386
386
  * loud `workspace_latest_dangling`, the pointer deleted, an empty workspace); absent (404 — the key, with the bucket there) ⇒ a fresh pod; a read that
387
387
  * FAILS ⇒ a loud `connect_failed` — a 404 `NoSuchBucket` included ({@link refuseMissingBucket}) — never a silent fresh workspace (a run that believes it resumed while it did not is
388
388
  * the worst form). Once the pod holds the workspace, this env arms its fence seat (its destroy will commit).
389
+ * The same walk ({@link establish}) is the restore point of a parked run's resumed leg whose park packed nothing ({@link resumeVM}).
389
390
  * Persistence off / no store ⇒ a fresh pod, exactly as before. Persistence on but no such session (the occupant absent) ⇒ a
390
391
  * fresh pod and one `workspace_space_unavailable` line (no restore by any other key). The occupant read FAULTED ⇒ the same
391
392
  * loud `connect_failed` as a failed `latest` read, no pod ({@link provision} refuses it — every entry that would create one).
@@ -397,14 +398,16 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
397
398
  * as it stands to every later connect on every lane — the previous commit is not waited on again, `latest` is not read again, no pod is created.
398
399
  */
399
400
  connect(config?: RemoteConnectConfig): Promise<ConnectResult>;
400
- /** The one walk of a connect ({@link connect} runs it once at a time). */
401
+ /** The one walk of a connect ({@link connect} runs it once at a time) — also the resumed leg's walk when its park packed nothing ({@link restorePointOf},
402
+ * under the resume's own hold of the same gate). */
401
403
  private establish;
402
404
  /**
403
405
  * The restore point's fill (`workspace-fill.ts`): a pod whose workspace this env just minted or restored is handed over only once the fill seat placed
404
406
  * its content — through {@link fillChannel} (this adapter's own exec, never the public face: inside a connect the public face waits on that very connect).
405
407
  * A fill that does not complete releases the pod (no tool and no run-end snapshot may see a half-filled workspace); a refusal that would answer the same
406
408
  * again (`standing`) is held for the env ({@link holdRefusal}). No seat ⇒ the pod as it came. `origin` tells the seat whether the workspace was minted
407
- * (a new pod with nothing restored: the space off / unavailable / without `latest`, or `latest` dangling) or restored (the space's `latest`, or a park snapshot whose archive came back; a park snapshot with no archive — a plan-only park — is minted).
409
+ * (a new pod with nothing restored: the space off / unavailable / without `latest`, or a dangling restore point — `latest` or a park snapshot whose archive is gone)
410
+ * or restored (the space's `latest`, or a park snapshot whose archive came back). A park that packed nothing resumes through a new connect, so it reads as that connect does.
408
411
  */
409
412
  private settle;
410
413
  /** The restore point's channel: a fixed command through the one start seam (start form `adapter`, the engine's commands' identity) and a write in
@@ -447,9 +450,11 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
447
450
  * never its text. Not a knob: there is no honest way to run a space we could not read — a retry of the run reads it again.
448
451
  */
449
452
  private refuseUnreadableOccupant;
450
- /** Every space-lane operator line: the space is named by its incarnation's log prefix only, and free text (a curl
451
- * stderr, a fetch error) passes {@link redactIncarnations} — the full token never reaches a log body. */
453
+ /** Every space-lane operator line: the space is named by its incarnation's log prefix only ({@link snapshotLine} does the rest). */
452
454
  private spaceLine;
455
+ /** Every snapshot operator line, both lanes — the ONE redaction seat: each string field (a key, a curl stderr, a fetch error, an effect) passes
456
+ * {@link redactIncarnations}, so a full incarnation-shaped token never reaches a log body, whatever text carried it there. */
457
+ private snapshotLine;
453
458
  /** A fresh pod (create → Running → workspace root), bounded retry — THE seat every pod of this env is created through
454
459
  * ({@link connect} and, via {@link restoreSnapshot}, {@link resumeVM}). A lane keyed by an occupant read that faulted ⇒ refused
455
460
  * here, before any request ({@link refuseUnreadableOccupant}): never retried (the reading is this env's, fixed at construction). */
@@ -651,11 +656,14 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
651
656
  */
652
657
  private uploadSnapshot;
653
658
  /**
654
- * Resume a workspace snapshot onto a FRESH pod (resume = executionEnvFactory rebuild + resumeVM):
655
- * provision a pod, then download + untar the snapshot into the workspace ({@link restoreSnapshot}, lane `park`). A
656
- * missing object (a plan-only park: no pod was ever minted, so nothing was uploaded) resumes as a clean workspace whose origin is `minted` —
657
- * no restorable tree — so the restore point seeds it; the resume walks the park lane and never reads the space's `latest` (which a fresh connect with the space on
658
- * would restore); a snapshot that came back is `restored` and never seeded.
659
+ * Resume a parked run's workspace onto a FRESH pod (resume = executionEnvFactory rebuild + resumeVM). Where the leg's workspace comes from is
660
+ * read off the park's snapshot id — it says what the park packed ({@link snapshotIdHolds}), never guessed from what the store answers:
661
+ * - an archive ⇒ provision a pod, then download + untar it into the workspace ({@link restoreSnapshot}, lane `park`): it came back ⇒ `restored`
662
+ * (never seeded); it is not there ⇒ a dangling restore point, the same rule on both lanes ({@link dropDangling}: one line, an empty workspace);
663
+ * - nothing (the env held no pod when it parked — the run parked before anything touched its hand) ⇒ there is no workspace of the park to
664
+ * restore, so the leg's restore point is the one a new connect has ({@link establish}): space open ⇒ the previous commit waited out and
665
+ * `latest` restored (none ⇒ a fresh workspace), space off ⇒ a fresh workspace — never a fresh workspace beside a `latest` that the leg's
666
+ * run-end commit would then overwrite. No archive is asked for under such an id.
659
667
  *
660
668
  * [ref]①(priorHandle 根保真)——k8s 走**诚实臂**而非绑定臂:pod 的 workspace volume 挂在**当前**
661
669
  * 配置根(cfg.mountPath),往 priorHandle.mountPath 绑=往容器 rootfs untar 大工作区,ephemeral-storage
@@ -666,15 +674,14 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
666
674
  resumeVM(snapshotId: SnapshotId, options?: VmLifecycleOptions): Promise<ConnectResult>;
667
675
  /** The one walk of a resume ({@link resumeVM} runs it under the connect gate). */
668
676
  private resumeWalk;
669
- /**
670
- * The ONE tar download (both lanes): provision a FRESH pod, download + untar the lane's archive into the workspace.
671
- * A missing archive (404) starts from the clean workspace on both lanes, and on both it is the same fact — no restorable tree ⇒ origin
672
- * `minted` (the restore point seeds it, exactly as on a fresh connect). What differs is only what it means:
673
- * - park: a plan-only suspend has no object (the park came before anything touched the hand) — nothing to say;
674
- * - space: `latest` dangles (its pointer is only written after a successful upload, so the archive was removed later —
675
- * by the bucket's lifecycle rule or a hand): the data is gone by an operator rule ⇒ loud and continuable —
676
- * {@link dropDanglingLatest} (one `workspace_latest_dangling` line, the pointer deleted); the run's own run-end
677
- * snapshot then becomes the new `latest`.
677
+ /** The resumed leg's restore point, chosen by what the park's id names ({@link resumeVM}) — a closed set, one arm each. */
678
+ private restorePointOf;
679
+ /**
680
+ * The ONE tar download (both lanes): provision a FRESH pod, download + untar the lane's archive into the workspace. Only an id that names an
681
+ * archive reaches it ({@link snapshotIdHolds}: a park that packed nothing resumes through {@link establish}; `latest` names only an object
682
+ * that was written). So a missing archive (404) is one fact on both lanes — the restore point names data that is gone (removed later, by the
683
+ * bucket's lifecycle rule or a hand) ⇒ loud and continuable, ONE rule: {@link dropDangling} (one `workspace_latest_dangling` line), and the
684
+ * run continues on the clean workspace, origin `minted` (no restorable tree ⇒ the restore point seeds it, exactly as on a fresh connect).
678
685
  * An attempt that does not complete releases the pod it provisioned: no tool and no run-end snapshot may ever see a torn
679
686
  * workspace. A transient class ({@link restoreFailureIsTransient}) is tried again on a fresh pod after the `retry.backoffMs`
680
687
  * pause, up to the lane's {@link RESTORE_MAX_ATTEMPTS}; a non-transient one ends the restore at once. The lane then says what
@@ -691,12 +698,18 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
691
698
  /** The pause before a restore's next attempt (`retry.backoffMs`, the adapter's one retry pause). `false` ⇒ aborted (now or during it). */
692
699
  private restorePause;
693
700
  /**
694
- * `latest` names an archive that is not there ([ref]): one `workspace_latest_dangling{space, key, deleted}` line and the
695
- * pointer is deleted (the next run must not trip on it again); the run continues on the empty workspace. A pointer
696
- * delete that fails rides on the same line (`deleted: false` + the error) — the next run then says it once more.
697
- * This env is the space's only writer here (the commit fence admits one per space), so the delete is a plain one.
698
- */
699
- private dropDanglingLatest;
701
+ * A restore point names an archive that is not there — the data is gone by an operator rule (a bucket lifecycle rule, a hand): loud and
702
+ * continuable, ONE rule on both lanes — one `workspace_latest_dangling{lane, key, …}` line, and the run continues on the empty workspace
703
+ * {@link restoreSnapshot} hands over. `key` = the object the id names, spelled by {@link snapshotBase} (the one spelling of both partitions).
704
+ * What each lane adds is only what it holds:
705
+ * - space: its `latest` pointer is deleted (the next run must not trip on it again); the line carries `space` and `deleted` — a delete that
706
+ * fails rides on the same line (`deleted: false` + the error) and the next run says it once more. This env is the space's only writer here
707
+ * (the commit fence admits one per space), so the delete is a plain one. The run's own run-end snapshot then becomes the new `latest`;
708
+ * - park: there is no pointer to drop (the id lives in the engine's checkpoint); the line carries the session id. With the space open, the
709
+ * resumed leg's run-end snapshot then becomes the space's new `latest`.
710
+ * Both lines pass the one redaction seat ({@link snapshotLine}).
711
+ */
712
+ private dropDangling;
700
713
  /**
701
714
  * Honest post-resume consistency ([ref] §5 council #8): the workspace FILES are restored byte-for-byte
702
715
  * from the snapshot, so read-before-edit hashing already lines up — there's nothing to re-fetch. In-VM
@@ -8,7 +8,7 @@ import { appendFileStaged, buildCwdEnvCommand, createCwdGuardedCall, shellQuote,
8
8
  import { presignS3Url, presignS3ListUrl } from "./s3-presign.js";
9
9
  import { WORKSPACE_SNAPSHOT_BUCKET_DEFAULT } from "../object-store-buckets.js";
10
10
  import { SNAPSHOT_BUCKET_MISSING, isNoSuchBucketAnswer } from "./s3-answer.js";
11
- import { K8S_WORKSPACE_MOUNT_DEFAULT, SNAPSHOT_KEY_SEGMENT, WORKSPACE_SNAPSHOT_PREFIX_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, mintSnapshotId, snapshotKeyPrefixProblem, snapshotNameOwnedBy } from "../workspace-space.js";
11
+ import { K8S_WORKSPACE_MOUNT_DEFAULT, SNAPSHOT_KEY_SEGMENT, WORKSPACE_SNAPSHOT_PREFIX_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, mintSnapshotId, snapshotIdHolds, snapshotKeyPrefixProblem, snapshotNameOwnedBy } from "../workspace-space.js";
12
12
  import { spaceCommitAlive } from "../workspace-space-commit.js";
13
13
  import { incarnationLogPrefix, redactIncarnations } from "../session-incarnation.js";
14
14
  import { WORKSPACE_CACHE_SPACE_LABEL, workspaceCacheClaimOf, workspaceCacheEnv } from "../workspace-cache.js";
@@ -509,8 +509,10 @@ export class RemoteK8sExecutionEnv {
509
509
  return spaceReadFailed("occupant", ` (${code}) — refusing to start this run on a workspace that could be neither restored nor saved; retry the run once the session store answers, or set ${ways}`);
510
510
  }
511
511
  spaceLine(event, space, fields) {
512
- const safe = Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, typeof v === "string" ? redactIncarnations(v) : v]));
513
- this.logger?.warn(event, { space: incarnationLogPrefix(space.incarnation), ...safe });
512
+ this.snapshotLine(event, { space: incarnationLogPrefix(space.incarnation), ...fields });
513
+ }
514
+ snapshotLine(event, fields) {
515
+ this.logger?.warn(event, Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, typeof v === "string" ? redactIncarnations(v) : v])));
514
516
  }
515
517
  provision(signal) {
516
518
  const space = this.spaceLane;
@@ -1224,12 +1226,12 @@ export class RemoteK8sExecutionEnv {
1224
1226
  async uploadSnapshot(target, boundMs = this.cfg.dataTimeoutMs) {
1225
1227
  if (target.lane === "park" && this.inFlight.size > 0)
1226
1228
  return { ok: false, error: new RemoteExecutionError("command_in_flight", "cannot suspend while a command is in flight (would snapshot a torn workspace)") };
1227
- const snapshotId = mintSnapshotId(this.occupantIncarnation);
1228
1229
  if (!this.podName) {
1229
1230
  return target.lane === "space"
1230
1231
  ? { ok: false, error: new RemoteExecutionError("connect_failed", "the sandbox pod was gone when the run-end snapshot reached the upload — nothing was saved (the workspace could not be read), and `latest` keeps naming the previous snapshot") }
1231
- : ok(snapshotId);
1232
+ : ok(mintSnapshotId(this.occupantIncarnation, "nothing"));
1232
1233
  }
1234
+ const snapshotId = mintSnapshotId(this.occupantIncarnation, "archive");
1233
1235
  const presigned = this.presignSnapshot({ ...target, method: "PUT", snapshotId });
1234
1236
  if (!presigned.ok)
1235
1237
  return presigned;
@@ -1273,13 +1275,26 @@ export class RemoteK8sExecutionEnv {
1273
1275
  this.logger?.warn("sandbox_pod_released_before_restore", { pod: this.podName, ...(this.cfg.sessionId ? { sessionId: this.cfg.sessionId } : {}), effect: "this env held a pod minted before the park snapshot's restore — it is deleted; the restore provisions the leg's one pod" });
1274
1276
  await this.releasePod();
1275
1277
  }
1276
- const r = await this.settle(await this.restoreSnapshot({ lane: "park" }, snapshotId, options?.abortSignal));
1278
+ const r = await this.restorePointOf(snapshotId, options?.abortSignal);
1277
1279
  if (!r.ok)
1278
1280
  return r;
1279
1281
  this.handleSnapshotId = snapshotId;
1280
1282
  this.armSpace();
1281
1283
  return ok(this.workspaceHandle());
1282
1284
  }
1285
+ async restorePointOf(snapshotId, abortSignal) {
1286
+ const holds = snapshotIdHolds(snapshotId);
1287
+ switch (holds) {
1288
+ case "archive":
1289
+ return this.settle(await this.restoreSnapshot({ lane: "park" }, snapshotId, abortSignal));
1290
+ case "nothing":
1291
+ return this.establish(abortSignal ? { abortSignal } : undefined);
1292
+ default: {
1293
+ const unreachable = holds;
1294
+ throw new Error(`unhandled snapshot id family: ${String(unreachable)}`);
1295
+ }
1296
+ }
1297
+ }
1283
1298
  async restoreSnapshot(target, snapshotId, abortSignal) {
1284
1299
  const presigned = this.presignSnapshot({ ...target, method: "GET", snapshotId });
1285
1300
  if (!presigned.ok)
@@ -1303,8 +1318,7 @@ export class RemoteK8sExecutionEnv {
1303
1318
  : undefined;
1304
1319
  if (!failure) {
1305
1320
  if (r.ok && r.value.stdout.includes("EMPTY_SNAPSHOT")) {
1306
- if (target.lane === "space")
1307
- await this.dropDanglingLatest(target.space, snapshotId);
1321
+ await this.dropDangling(target, snapshotId);
1308
1322
  return { ok: true, value: this.workspaceHandle(), origin: "minted" };
1309
1323
  }
1310
1324
  return { ok: true, value: this.workspaceHandle(), origin: "restored" };
@@ -1347,15 +1361,34 @@ export class RemoteK8sExecutionEnv {
1347
1361
  signal?.addEventListener("abort", onAbort, { once: true });
1348
1362
  });
1349
1363
  }
1350
- async dropDanglingLatest(space, snapshotId) {
1351
- const prefix = this.snapshotKeyPrefix();
1352
- const dropped = await this.writePointer(space, "latest", null);
1353
- this.spaceLine("workspace_latest_dangling", space, {
1354
- key: `${prefix}/${space.incarnation}/space/${snapshotId}.tar.gz`,
1355
- deleted: dropped.ok,
1356
- ...(dropped.ok ? {} : { error: dropped.error.message }),
1357
- effect: "the space's latest snapshot is gone (removed by a bucket lifecycle rule or by hand) — this run starts from an empty /workspace; its run-end snapshot becomes the new latest",
1358
- });
1364
+ async dropDangling(target, snapshotId) {
1365
+ const key = `${this.snapshotBase(target)}${snapshotId}.tar.gz`;
1366
+ switch (target.lane) {
1367
+ case "space": {
1368
+ const dropped = await this.writePointer(target.space, "latest", null);
1369
+ this.spaceLine("workspace_latest_dangling", target.space, {
1370
+ lane: "space",
1371
+ key,
1372
+ deleted: dropped.ok,
1373
+ ...(dropped.ok ? {} : { error: dropped.error.message }),
1374
+ effect: "the space's latest snapshot is gone (removed by a bucket lifecycle rule or by hand) — this run starts from an empty /workspace; its run-end snapshot becomes the new latest",
1375
+ });
1376
+ return;
1377
+ }
1378
+ case "park":
1379
+ this.snapshotLine("workspace_latest_dangling", {
1380
+ lane: "park",
1381
+ key,
1382
+ ...(this.cfg.sessionId ? { sessionId: this.cfg.sessionId } : {}),
1383
+ effect: "the parked run's snapshot is gone (removed by a bucket lifecycle rule or by hand) — the resumed leg starts from an empty /workspace" +
1384
+ (this.spaceLane.state === "open" ? "; its run-end snapshot becomes the space's new latest" : ""),
1385
+ });
1386
+ return;
1387
+ default: {
1388
+ const unreachable = target;
1389
+ throw new Error(`unhandled snapshot lane: ${String(unreachable.lane)}`);
1390
+ }
1391
+ }
1359
1392
  }
1360
1393
  async postResumeInit() {
1361
1394
  if (!this.cfg.s3Snapshot)
@@ -157,7 +157,7 @@ export async function runSymlinkChain(absPath, run) {
157
157
  const STAGED_FILE = "data";
158
158
  const STAGE_DIR_REFUSED_EXIT = 3;
159
159
  const STAGE_DIR_UNSEALED_EXIT = 4;
160
- const STAGE_DIR_UNSEALED_MESSAGE = "sema: the private staging directory could not be made private (chmod 700 failed or did not take effect, or the mode or owner read back differently — the filesystem ignores permission bits or the parent directory carries a default ACL); nothing was staged";
160
+ const STAGE_DIR_UNSEALED_MESSAGE = "sema: the private staging directory could not be made private (chmod 700 failed or did not take effect, the mode or owner read back differently, or an inherited ACL entry could not be removed or read on this platform — the filesystem ignores permission bits or the parent directory carries a default or inheritable ACL); nothing was staged";
161
161
  export const STAGE_DIR_LEFT_MARK = "sema: staging directory left behind:";
162
162
  export function buildStagingPaths(abs, nonce = randomBytes(6).toString("hex")) {
163
163
  const cut = abs.lastIndexOf("/");
@@ -174,7 +174,17 @@ function privateMkdir(dir) {
174
174
  }
175
175
  function hardenPrivate(dir) {
176
176
  const statOf = (gnu, bsd) => `$(stat -c ${gnu} -- ${dir} 2>/dev/null || stat -f ${bsd} -- ${dir} 2>/dev/null)`;
177
- return `( chmod -- 700 ${dir} 2>/dev/null && m=${statOf("%a", "%Lp")} && u=${statOf("%u", "%u")} && case "$m" in 700|[1-7]700) [ "$u" = "$(id -u)" ];; *) false;; esac )`;
177
+ return `( chmod -- 700 ${dir} 2>/dev/null && m=${statOf("%a", "%Lp")} && u=${statOf("%u", "%u")} && case "$m" in 700|[1-7]700) [ "$u" = "$(id -u)" ];; *) false;; esac && ${aclCleared(dir)} )`;
178
+ }
179
+ function aclCleared(dir) {
180
+ const marker = `ls_out=$(cd ${dir} && ls -ld . 2>&1) && [ -z "$(printf '%s\\n' "$ls_out" | sed -n 2p)" ] && mode_word=\${ls_out%% *} && ` +
181
+ `case "$mode_word" in d?????????*+*) echo +;; d?????????*) ;; *) false;; esac`;
182
+ return (`plat=$(uname -s 2>/dev/null) && case "$plat" in ` +
183
+ `Linux) acl_read() { :; }; acl_strip() { false; };; ` +
184
+ `Darwin) acl_read() { ls_out=$(cd ${dir} && ls -led . 2>&1) && case "$ls_out" in d?????????*) ;; *) false;; esac && printf '%s\\n' "$ls_out" | sed 1d; }; acl_strip() { chmod -N ${dir}; };; ` +
185
+ `FreeBSD) acl_read() { ${marker}; }; acl_strip() { setfacl -b ${dir}; };; ` +
186
+ `SunOS) acl_read() { ${marker}; }; acl_strip() { chmod A- ${dir}; };; ` +
187
+ `*) false;; esac && acl=$(acl_read 2>/dev/null) && { [ -z "$acl" ] || { acl_strip 2>/dev/null && acl=$(acl_read 2>/dev/null) && [ -z "$acl" ]; }; }`);
178
188
  }
179
189
  function emptyPrivate(dir) {
180
190
  return `[ ! -e ${dir}/${STAGED_FILE} ] && [ ! -L ${dir}/${STAGED_FILE} ]`;
@@ -387,7 +387,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
387
387
  * 它的域完全靠「属主 sessionId 的墓碑」推。而会话 id 是**调用方可自选**的 ⇒ 两个租户先后用过同一个 id
388
388
  * 完全合法;两边都删过之后,同一个 row_key 上会有**两个域**的墓碑,这时任何一边的 sweep 拿自己的墓碑
389
389
  * 去认领这些字节都只是猜。判据因此是:**存在第二个域的同名墓碑 ⇒ 这批行不可归属 ⇒ 一根不删**,
390
- * 实体清理退回既有的 TTL 腿(`reapOlderThan`)。少删一次是可接受的;删掉另一个租户的字节、还跳过它的
390
+ * 实体清理退回既有的 TTL 腿(core `ToolResultStore.reap`)。少删一次是可接受的;删掉另一个租户的字节、还跳过它的
391
391
  * legal hold 与审计域,不可接受。
392
392
  */
393
393
  protected orphanToolResultCandidates(exec: {
@@ -475,7 +475,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
475
475
  * 也只计 `skipped` 不删。会话本体的清理归 {@link SqlRetentionStore.deleteExpiredSessions}(它会连着
476
476
  * 这行一起删),两条腿因此不重叠也不互相抢行。
477
477
  * · **一根不碰的那一支**:`owner_session_id IS NULL` 的**无出处**行。没有出处就没有域,按任何域去删它
478
- * 都是猜;实体清理归既有的 TTL 腿(`reapOlderThan`,`boot/reapers.ts` 驱动)。这与 core 的
478
+ * 都是猜;实体清理归既有的 TTL 腿(core `ToolResultStore.reap`,`boot/reapers.ts` 驱动)。这与 core 的
479
479
  * 「an UNOWNED entry is never deleted」是同一条纪律的两处执行点。
480
480
  *
481
481
  * 事务形与另外两只方法逐字同款(hold 哨兵行锁读 → 删除+墓碑 → 审计行,同 commit 同 rollback)。
@@ -5,11 +5,9 @@ export declare const READ_FLOOR_UNAVAILABLE_REASONS: readonly ["arch", "user_not
5
5
  export type ReadFloorUnavailableReason = (typeof READ_FLOOR_UNAVAILABLE_REASONS)[number];
6
6
  /** F2:附着不过的环境上,带地板起动的拒句(纯字符串 ⇒ `build*`)。首句点名原因词;点名表的首行与旋钮。 */
7
7
  export declare function buildReadFloorUnavailableMessage(reason: ReadFloorUnavailableReason, detail: string | undefined, floor: ReadFloor | undefined): string;
8
- /** F6:守地板的环境上,不带地板的引擎起动的拒句。 */
9
- export declare function buildReadFloorMissingMessage(lane: SandboxFloorLane): string;
10
8
  /**
11
9
  * 一次起动是什么(闭集,调用方必须表态 —— 没有缺省臂):
12
- * · `engine` —— 经执行环境公共起动口要起的命令(引擎的,或替引擎干活的服务端调用方的);带不带地板是它交来的事实。`hold` = 适配器之后还会以
10
+ * · `engine` —— 经执行环境公共起动口要起的命令(引擎的,或替引擎干活的服务端调用方的);它带的地板就是它的表,缺席 = 表无行。`hold` = 适配器之后还会以
13
11
  * 同一起动号再起 `run`(k8s 后台作业的轮询 / 停 / 收尾读的是这次起动的产物,按这次起动的地板跑),登记留到 {@link SandboxReadFloor.release};
14
12
  * · `under` —— 以一次已登记(`hold`)起动的号再起一只 `run`(上面那类轮询);
15
13
  * · `adapter` —— 适配器自己的固定命令(文件面助手、探针、数据搬运、全杀);附着成功之后经监督者的 `plain` 起;
@@ -29,7 +27,7 @@ export type FloorStart = {
29
27
  };
30
28
  export declare const ADAPTER_START: FloorStart;
31
29
  export declare const BOOTSTRAP_START: FloorStart;
32
- /** 引擎起动:`readFloor` 原样交(`undefined` = 这次起动不带地板)。 */
30
+ /** 引擎起动:`readFloor` 原样交(`undefined` = 这次起动的表一行都没有)。 */
33
31
  export declare function engineStart(readFloor: ReadFloor | undefined, hold?: boolean): FloorStart;
34
32
  /** 起动身份:`root` = 车道以 root 起(e2b `user:"root"`、local-docker `-u 0`);`exec-user` = 车道自己的执行身份(改前的形)。 */
35
33
  export type FloorLaunchIdentity = "root" | "exec-user";
@@ -125,8 +123,8 @@ export interface RelayAnswer {
125
123
  export declare const UNJUDGED_VERDICT = "read floor: no verdict for this start";
126
124
  /** 判官:适配器进程里唯一回答「这条路径拒不拒」的东西。live object ⇒ `create*`。 */
127
125
  export interface FloorJudge {
128
- /** 登记一次带地板的起动,回它的起动号(随机、不可猜)。`held` ⇒ 之后还会以这个号再起 `run`(`under`),直到 {@link release};否则这一次就是它唯一的起动。 */
129
- register(floor: ReadFloor, held: boolean): string;
126
+ /** 登记一次引擎起动(带它的表;`undefined` = 表无行),回它的起动号(随机、不可猜)。`held` ⇒ 之后还会以这个号再起 `run`(`under`),直到 {@link release};否则这一次就是它唯一的起动。 */
127
+ register(floor: ReadFloor | undefined, held: boolean): string;
130
128
  /** 以这个号再起一次 `run`(`under`)之前问:在册且适配器没说过「不再起」⇒ `true`(记下又有一只监督者将连上);否则 `false`(起动拒)。 */
131
129
  launch(startId: string): boolean;
132
130
  /** 适配器不会再以这个号起 `run` 了。登记要两件都到才撤(协议 §5.5):这一句 ∧ 此后的 `gone` —— `gone` 已在 ⇒ 当即撤;否则等它(还连着的监督者照旧有判官答)。 */
@@ -202,7 +200,7 @@ export interface SandboxReadFloor {
202
200
  readonly state: SandboxReadFloorState;
203
201
  /** 这只模块钉住的监督者二进制(守地板 ⇒ 在;裸模块 ⇒ 缺席)。k8s 的 pod 座({@link buildFloorPodSeat})只读它 —— 放进 pod 的与自检核的是同一份钉。 */
204
202
  readonly binaries: FloorBinaries | undefined;
205
- /** 唯一接缝:一次起动 → 答案,或 `spawn_error`(F2 / F6 / 时机不对的引导起动 / 已撤的号)。 */
203
+ /** 唯一接缝:一次起动 → 答案,或 `spawn_error`(F2 / 时机不对的引导起动 / 已撤的号)。 */
206
204
  wrap(start: FloorStart): Result<FloorLaunch, ExecutionError>;
207
205
  /** 适配器不会再以这个号起 `run`(`hold` 起动的收尾)。登记在其后的 `gone` 到时撤(还连着的监督者照旧有判官答)。 */
208
206
  release(startId: string | undefined): void;
@@ -35,11 +35,6 @@ export function buildReadFloorUnavailableMessage(reason, detail, floor) {
35
35
  `This command carries a read-deny table${first !== undefined ? ` (first row "${first}")` : ""} this sandbox cannot keep, so it is refused rather than run with fewer rows. ` +
36
36
  `To run this lane without the byte floor, set ${SANDBOX_READ_FLOOR_ENV}=off (the lane then declares keepsReadFloor=false: each run with read-deny rows is announced, or refused when READ_DENY_FLOOR_REQUIRED=true).`);
37
37
  }
38
- export function buildReadFloorMissingMessage(lane) {
39
- return (`read floor: this ${lane} sandbox keeps the read-deny byte floor, and this command start carries no floor — it is refused rather than run a process the floor does not judge ` +
40
- `(every start on a lane that keeps the floor must carry the run's read-deny table). ` +
41
- `To run this lane without the byte floor, set ${SANDBOX_READ_FLOOR_ENV}=off.`);
42
- }
43
38
  export const ADAPTER_START = Object.freeze({ kind: "adapter" });
44
39
  export const BOOTSTRAP_START = Object.freeze({ kind: "bootstrap" });
45
40
  export function engineStart(readFloor, hold = false) {
@@ -121,6 +116,8 @@ export function parseRelayFrame(line) {
121
116
  }
122
117
  export const UNJUDGED_VERDICT = "read floor: no verdict for this start";
123
118
  function judgeOne(floor, path) {
119
+ if (floor === undefined)
120
+ return null;
124
121
  let v;
125
122
  try {
126
123
  v = floor.denies(path);
@@ -433,8 +430,6 @@ export function createKeptSandboxReadFloor(opts) {
433
430
  }
434
431
  case "engine": {
435
432
  const floor = start.readFloor;
436
- if (floor === undefined)
437
- return spawnError(buildReadFloorMissingMessage(lane));
438
433
  if (phase.kind === "unavailable")
439
434
  return spawnError(buildReadFloorUnavailableMessage(phase.reason, phase.detail, floor));
440
435
  if (phase.kind !== "ready" || bin === undefined)
@@ -73,7 +73,11 @@ export interface ParsedTable {
73
73
  export declare function parseCreateTable(stmt: string): ParsedTable | undefined;
74
74
  /** 启动时真会建出来的**全部**表形(录制 → 解析)。同名表二次出现以**先到**为准(`IF NOT EXISTS` 的真语义)。 */
75
75
  export declare function expectedTableShapes(dialect: SqlDialect): Promise<Map<string, ParsedTable>>;
76
- /** 拒启码(机读)。附录 A / 拒启码表的登记名。 */
76
+ /**
77
+ * 拒启码(机读)。附录 A / 拒启码表的登记名。含义 = 「本构建读不出的持久形,开始服务之前判」,一个码三只臂:本文件的列册对账与退役 schema 两道(SQL),
78
+ * 以及 local 数据根的严格边档对账(`local-session-store.ts` 的 `LocalDataRootSchemaMismatchError`,`LocalBackend.ensureSchema()` 调;拒句同形:
79
+ * 码开头的表头 · 逐坐标一段 · 「NEWER than this build」方向句 · 闭集出路)。
80
+ */
77
81
  export declare const SCHEMA_MISMATCH_ERROR_CODE = "config.schema_mismatch";
78
82
  /** 一张表的对账结论。 */
79
83
  export interface TableMismatch {
@@ -28,7 +28,7 @@ export interface ForkLogRow {
28
28
  readonly ts: unknown;
29
29
  }
30
30
  /**
31
- * 两只 SQL 孪生的行怎么交给引擎的 `selectForkRows`(按答案挑行 + 新会话的叶;分叉点不在日志里 ⇒ `invalid_fork_target`、路径中途悬空父 / 父链成环 ⇒ `invalid_session`,
31
+ * 两只 SQL 孪生的行怎么交给引擎的 `selectForkRows`(按答案挑行 + 新会话的叶;分叉点不在日志里 ⇒ `invalid_fork_target`、路径中途悬空父 / 父链成环 ⇒ `corrupt_session`〔core 7.61.x 及以前 `invalid_session`〕,
32
32
  * 引擎的原词原句)。本仓不再自写父链走法 —— 那曾是与引擎 `getPathToRoot` 同义的第二份。
33
33
  */
34
34
  export declare const FORK_LOG_ROW_SHAPE: ForkRowShape<ForkLogRow>;
@@ -22,6 +22,15 @@ export declare function forkOwnerOf(requested: string | null | undefined, source
22
22
  * 「首写」由调用方判(已记下属主的会话上的调用照旧静默不改,不经这里)。判官 = {@link isSessionOwnerValue}(core 7.56.0 [ref] `principalNameOf`)。
23
23
  */
24
24
  export declare function assertFirstOwnerWritable(sessionId: string, owner: unknown): asserts owner is string | null;
25
+ /**
26
+ * placed 行属主格的唯一读法(core 7.62.0 [ref] 盒义务 7;三只持久会话店的 placing write 同调):记下的属主 = 引擎交来的 `placement.owner` **原样** ——
27
+ * 委派树的属主由引擎判(没人派生的 run 读自己那一行、按化身绑一次,原样带给每一层子代;`null` = 根匿名),店只记、不判、不推断,也不进元组
28
+ * (`SessionPlacementRecord` 没有属主)。值不是主体名也不是 `null`(空串、空白、缺席 —— 绕过类型的调用)⇒ 响亮拒(`SessionError("storage")`,一个字节不写):
29
+ * 一只谁也不指的属主会让行要么谁都读得到、要么谁都读不到。判官 = {@link isSessionOwnerValue},与 `register` 首写同一只。
30
+ */
31
+ export declare function placedSessionOwnerOf(sessionId: string, placement: {
32
+ readonly owner?: unknown;
33
+ }): string | null;
25
34
  /**
26
35
  * 导入者的属主 —— 换装的写腿(公开 `importEntries` / `replaceEntries` 与暂存提交)把调用方给的属主重戳进会话行之前的唯一读法:
27
36
  * 主体名或 `null`(匿名)⇒ 原样交回;别的一切(空串、空白串、带控制 / 分隔 / 非连接符格式字符的串、非串)⇒ `TypeError` 响亮拒 ——
@@ -22,6 +22,12 @@ export function assertFirstOwnerWritable(sessionId, owner) {
22
22
  throw new SessionError("storage", `register(${sessionId}): a first ownership write must carry a principal name or null (anonymous), got ${ownerValueFault(owner)} — ` +
23
23
  "a value that names nobody would leave the session claimable by anyone or ownable by no one; refusing, nothing written");
24
24
  }
25
+ export function placedSessionOwnerOf(sessionId, placement) {
26
+ const owner = placement.owner;
27
+ if (isSessionOwnerValue(owner))
28
+ return owner;
29
+ throw new SessionError("storage", `acquire(${sessionId}): a placed session's owner (placement.owner, the delegation tree's owner the engine judged) must be a principal name or null (an anonymous tree), got ${ownerValueFault(owner)} — refusing, nothing written`);
30
+ }
25
31
  export function importerOwnerOf(owner) {
26
32
  if (isSessionOwnerValue(owner))
27
33
  return owner;