@sema-agent/server 7.151.0 → 7.152.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 (286) hide show
  1. package/MIGRATION.md +266 -0
  2. package/README.md +15 -3
  3. package/README.zh-CN.md +10 -2
  4. package/USAGE.md +93 -64
  5. package/deploy/sema-up/README.md +5 -4
  6. package/deploy/sema-up/cluster-up.sh +6 -6
  7. package/deploy/sema-up/kube-up.sh +5 -5
  8. package/deploy/sema-up/lib/flagshape.sh +36 -8
  9. package/deploy/sema-up/machines.example.yaml +3 -1
  10. package/deploy/sema-up/sema-up.sh +22 -16
  11. package/dist/approval-ask-audit-store.d.ts +1 -1
  12. package/dist/approval-ask-machine.d.ts +4 -10
  13. package/dist/approval-ask-retention.d.ts +1 -1
  14. package/dist/approval-card.d.ts +10 -12
  15. package/dist/approval-gate-currency.d.ts +8 -8
  16. package/dist/approval-gate-currency.js +3 -4
  17. package/dist/approval-hmac.d.ts +17 -1
  18. package/dist/approval-hmac.js +7 -1
  19. package/dist/approval-queue-kind.d.ts +1 -1
  20. package/dist/approval.d.ts +2 -2
  21. package/dist/approval.js +2 -2
  22. package/dist/bind-host.d.ts +1 -3
  23. package/dist/bind-host.js +2 -3
  24. package/dist/boot/coordinators.d.ts +1 -1
  25. package/dist/boot/coordinators.js +9 -10
  26. package/dist/boot/device-lane.js +2 -0
  27. package/dist/boot/execution-env.d.ts +1 -0
  28. package/dist/boot/execution-env.js +31 -5
  29. package/dist/boot/leader.d.ts +31 -30
  30. package/dist/boot/leader.js +38 -28
  31. package/dist/boot/leg-path-adjudication.d.ts +40 -0
  32. package/dist/boot/leg-path-adjudication.js +18 -0
  33. package/dist/boot/parked-revive-gate.js +3 -15
  34. package/dist/boot/reapers.d.ts +1 -1
  35. package/dist/boot/reapers.js +1 -1
  36. package/dist/boot/resolve-spec.js +12 -26
  37. package/dist/boot/runner-deps.d.ts +39 -35
  38. package/dist/boot/runner-deps.js +92 -89
  39. package/dist/boot/shutdown.d.ts +6 -0
  40. package/dist/boot/shutdown.js +10 -4
  41. package/dist/boot/stage-02-web-admin-pull.js +4 -5
  42. package/dist/boot/stage-05-execution-env.js +2 -1
  43. package/dist/boot/stage-06-runners.d.ts +2 -0
  44. package/dist/boot/stage-06-runners.js +10 -4
  45. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  46. package/dist/boot/stage-07-capability-layer.js +1 -1
  47. package/dist/boot/stage-08-reapers.d.ts +1 -0
  48. package/dist/boot/stage-09-leader.d.ts +1 -0
  49. package/dist/boot/stage-09-leader.js +2 -2
  50. package/dist/boot/stage-10-http-server.d.ts +8 -0
  51. package/dist/boot/stage-10-http-server.js +11 -15
  52. package/dist/boot/stores.js +7 -4
  53. package/dist/boot/web-admin.js +2 -7
  54. package/dist/capabilities/approvals-push.d.ts +3 -3
  55. package/dist/capabilities/scenarios.d.ts +28 -13
  56. package/dist/capabilities/scenarios.js +2 -2
  57. package/dist/capability-facts.d.ts +22 -20
  58. package/dist/capability-facts.js +18 -34
  59. package/dist/card-rule-offers.d.ts +32 -0
  60. package/dist/card-rule-offers.js +130 -0
  61. package/dist/config-catalog.js +12 -11
  62. package/dist/config-dir-trust.js +4 -2
  63. package/dist/config-invariants.d.ts +18 -7
  64. package/dist/config-invariants.js +51 -8
  65. package/dist/config-lkg.d.ts +5 -19
  66. package/dist/config-lkg.js +8 -62
  67. package/dist/config-provider.d.ts +1 -1
  68. package/dist/config-root-migration.d.ts +2 -2
  69. package/dist/config-types.d.ts +28 -38
  70. package/dist/config.d.ts +7 -5
  71. package/dist/config.js +55 -16
  72. package/dist/control-socket.d.ts +51 -0
  73. package/dist/control-socket.js +115 -0
  74. package/dist/deployment-governance.d.ts +2 -1
  75. package/dist/device-ws-hub.d.ts +7 -0
  76. package/dist/device-ws-hub.js +7 -1
  77. package/dist/elicitation.js +2 -2
  78. package/dist/entry-problems.d.ts +40 -3
  79. package/dist/entry-problems.js +3 -1
  80. package/dist/execution-env-refusal.d.ts +8 -2
  81. package/dist/execution-env-refusal.js +3 -0
  82. package/dist/fleet/engine-signal.d.ts +12 -29
  83. package/dist/fleet/engine-signal.js +1 -7
  84. package/dist/fleet-client.js +2 -5
  85. package/dist/gate-settlement.d.ts +1 -8
  86. package/dist/gate-settlement.js +0 -4
  87. package/dist/history-anchor.js +1 -1
  88. package/dist/hitl-delivery.d.ts +2 -3
  89. package/dist/hitl-delivery.js +0 -2
  90. package/dist/hooks/hook-llm.js +4 -2
  91. package/dist/hooks/hook-runner.d.ts +3 -2
  92. package/dist/hooks/hook-runner.js +16 -1
  93. package/dist/host-decision.d.ts +11 -11
  94. package/dist/http/active-run-conflict.d.ts +35 -2
  95. package/dist/http/active-run-conflict.js +48 -19
  96. package/dist/http/approval-sweeps.d.ts +1 -1
  97. package/dist/http/approval-sweeps.js +5 -2
  98. package/dist/http/checkpoint-ctx.d.ts +2 -0
  99. package/dist/http/checkpoint-ctx.js +1 -0
  100. package/dist/http/dispatch.d.ts +6 -2
  101. package/dist/http/dispatch.js +25 -5
  102. package/dist/http/durable-run-leg.js +0 -1
  103. package/dist/http/health-reader.d.ts +16 -9
  104. package/dist/http/health-reader.js +2 -1
  105. package/dist/http/health-self-report.js +1 -1
  106. package/dist/http/owner-access.d.ts +2 -3
  107. package/dist/http/owner-access.js +3 -4
  108. package/dist/http/principal-gate.d.ts +49 -21
  109. package/dist/http/principal-gate.js +61 -11
  110. package/dist/http/request-channel.d.ts +23 -0
  111. package/dist/http/request-channel.js +9 -0
  112. package/dist/http/request-guards.js +17 -2
  113. package/dist/http/resume-ctx-read.js +20 -3
  114. package/dist/http/resume-legs.d.ts +2 -2
  115. package/dist/http/resume-legs.js +111 -41
  116. package/dist/http/route-ctx.d.ts +73 -7
  117. package/dist/http/route-ctx.js +17 -0
  118. package/dist/http/route-table.d.ts +4 -0
  119. package/dist/http/route-table.js +4 -0
  120. package/dist/http/routes/admin-config-refresh.js +3 -3
  121. package/dist/http/routes/admin-drain.d.ts +3 -0
  122. package/dist/http/routes/admin-drain.js +7 -7
  123. package/dist/http/routes/admin-memory-optout.js +3 -3
  124. package/dist/http/routes/adoption.js +3 -5
  125. package/dist/http/routes/approvals-assistant.d.ts +2 -1
  126. package/dist/http/routes/approvals-assistant.js +95 -48
  127. package/dist/http/routes/capabilities.d.ts +4 -0
  128. package/dist/http/routes/capabilities.js +30 -5
  129. package/dist/http/routes/config-catalog.js +2 -2
  130. package/dist/http/routes/devices.js +1 -1
  131. package/dist/http/routes/diagnostics.d.ts +3 -3
  132. package/dist/http/routes/diagnostics.js +19 -7
  133. package/dist/http/routes/images.js +10 -10
  134. package/dist/http/routes/memory-bundle.js +3 -3
  135. package/dist/http/routes/memory-compliance.js +3 -3
  136. package/dist/http/routes/memory-consolidation.js +2 -2
  137. package/dist/http/routes/memory-origin.js +3 -3
  138. package/dist/http/routes/memory-policy.js +2 -2
  139. package/dist/http/routes/observability.js +3 -3
  140. package/dist/http/routes/retention-ops.js +3 -3
  141. package/dist/http/routes/rules.js +5 -5
  142. package/dist/http/routes/run-permission-mode.d.ts +2 -1
  143. package/dist/http/routes/run-permission-mode.js +2 -2
  144. package/dist/http/routes/runs.d.ts +4 -9
  145. package/dist/http/routes/runs.js +73 -33
  146. package/dist/http/routes/session-sync.d.ts +18 -6
  147. package/dist/http/routes/session-sync.js +38 -36
  148. package/dist/http/routes/sessions.js +20 -9
  149. package/dist/http/routes/tasks.js +28 -37
  150. package/dist/http/routes/workflows.js +1 -1
  151. package/dist/http/server.d.ts +9 -3
  152. package/dist/http/server.js +15 -3
  153. package/dist/http/submit-ctx.d.ts +1 -1
  154. package/dist/http/submit-ctx.js +2 -0
  155. package/dist/human-facing-args.d.ts +33 -0
  156. package/dist/human-facing-args.js +33 -0
  157. package/dist/index.d.ts +1 -1
  158. package/dist/k8s-api-credentials.d.ts +29 -0
  159. package/dist/k8s-api-credentials.js +25 -0
  160. package/dist/lane-home-probe.d.ts +1 -1
  161. package/dist/lane-home-probe.js +2 -1
  162. package/dist/leader/diffup.d.ts +19 -0
  163. package/dist/leader/diffup.js +7 -0
  164. package/dist/leader/leader.d.ts +114 -10
  165. package/dist/leader/leader.js +69 -5
  166. package/dist/leader/repair-wire.js +2 -2
  167. package/dist/leader/run-entry.d.ts +2 -1
  168. package/dist/leader/run-entry.js +1 -1
  169. package/dist/leader/wire.d.ts +42 -132
  170. package/dist/leader/wire.js +81 -67
  171. package/dist/memory-scope.d.ts +20 -9
  172. package/dist/memory-scope.js +15 -3
  173. package/dist/model-entry-headers.d.ts +9 -10
  174. package/dist/model-entry-headers.js +0 -4
  175. package/dist/model-entry-refusal.d.ts +7 -4
  176. package/dist/model-entry-refusal.js +11 -5
  177. package/dist/model-plane-face.d.ts +1 -1
  178. package/dist/model-plane-face.js +3 -1
  179. package/dist/model-route-endpoint.d.ts +13 -7
  180. package/dist/model-route-endpoint.js +14 -11
  181. package/dist/model-select.d.ts +22 -0
  182. package/dist/model-select.js +9 -0
  183. package/dist/model-unreachable-notice.d.ts +11 -0
  184. package/dist/model-unreachable-notice.js +4 -4
  185. package/dist/object-store-buckets.d.ts +640 -6
  186. package/dist/object-store-buckets.js +166 -0
  187. package/dist/observability/fail-open.d.ts +20 -16
  188. package/dist/observability/fail-open.js +20 -16
  189. package/dist/observability/logger.js +3 -1
  190. package/dist/operator-roster.d.ts +68 -0
  191. package/dist/operator-roster.js +32 -0
  192. package/dist/orchestration/workflow-park-decision.d.ts +1 -1
  193. package/dist/outbound-dispatcher.d.ts +71 -8
  194. package/dist/outbound-dispatcher.js +48 -7
  195. package/dist/owner-faces.d.ts +1 -1
  196. package/dist/parked-decide.d.ts +2 -2
  197. package/dist/permission-mode-admission.d.ts +59 -26
  198. package/dist/permission-mode-admission.js +37 -10
  199. package/dist/plugins/approval-ask-store-file.d.ts +4 -0
  200. package/dist/plugins/approval-ask-store-file.js +3 -0
  201. package/dist/plugins/approval-ask-store-memory.d.ts +4 -0
  202. package/dist/plugins/approval-ask-store-memory.js +7 -1
  203. package/dist/plugins/approval-ask-store-sql.d.ts +26 -2
  204. package/dist/plugins/approval-ask-store-sql.js +12 -1
  205. package/dist/plugins/blob-backend.d.ts +83 -7
  206. package/dist/plugins/blob-backend.js +57 -6
  207. package/dist/plugins/checkpoint-index-columns.d.ts +20 -16
  208. package/dist/plugins/checkpoint-index-columns.js +3 -6
  209. package/dist/plugins/checkpoint-store-face.d.ts +119 -61
  210. package/dist/plugins/checkpoint-store-face.js +134 -55
  211. package/dist/plugins/landed-attendants.d.ts +2 -2
  212. package/dist/plugins/local-checkpoint-store.d.ts +13 -3
  213. package/dist/plugins/local-checkpoint-store.js +38 -49
  214. package/dist/plugins/one-time-migrations.d.ts +51 -21
  215. package/dist/plugins/one-time-migrations.js +67 -45
  216. package/dist/plugins/pg-pool.js +11 -9
  217. package/dist/plugins/remote-env-host.d.ts +29 -10
  218. package/dist/plugins/remote-env-host.js +99 -19
  219. package/dist/plugins/remote-env-k8s.d.ts +68 -2
  220. package/dist/plugins/remote-env-k8s.js +107 -24
  221. package/dist/plugins/replica-registration.d.ts +1 -1
  222. package/dist/plugins/replica-registration.js +1 -1
  223. package/dist/plugins/s3-presign.d.ts +23 -0
  224. package/dist/plugins/s3-presign.js +37 -3
  225. package/dist/plugins/scheduler-support.d.ts +2 -2
  226. package/dist/plugins/scheduler-support.js +0 -3
  227. package/dist/plugins/schema-indexes.js +2 -0
  228. package/dist/plugins/send-file-ledger.d.ts +2 -2
  229. package/dist/plugins/send-file-ledger.js +2 -2
  230. package/dist/plugins/send-user-file.d.ts +3 -4
  231. package/dist/plugins/send-user-file.js +56 -52
  232. package/dist/plugins/session-blob-claim-token.d.ts +15 -0
  233. package/dist/plugins/session-blob-claim-token.js +20 -0
  234. package/dist/plugins/session-blob-namespace.d.ts +46 -0
  235. package/dist/plugins/session-blob-namespace.js +116 -0
  236. package/dist/plugins/snapshot-bucket-presence.d.ts +25 -2
  237. package/dist/plugins/snapshot-bucket-presence.js +8 -2
  238. package/dist/plugins/sql-json-column.d.ts +1 -1
  239. package/dist/plugins/store-backend.d.ts +44 -2
  240. package/dist/plugins/store-backend.js +75 -53
  241. package/dist/plugins/task-attachment-store.d.ts +2 -1
  242. package/dist/plugins/task-attachment-store.js +5 -1
  243. package/dist/plugins/tidb-pool.js +19 -24
  244. package/dist/plugins/workspace-custody.d.ts +4 -3
  245. package/dist/question.d.ts +2 -1
  246. package/dist/question.js +5 -13
  247. package/dist/request-form.d.ts +6 -1
  248. package/dist/request-form.js +4 -1
  249. package/dist/rule-issue-reading.d.ts +1 -1
  250. package/dist/rules-consent.d.ts +1 -1
  251. package/dist/run-local.js +8 -9
  252. package/dist/runs.d.ts +17 -8
  253. package/dist/runs.js +15 -2
  254. package/dist/runtime-governance.d.ts +2 -2
  255. package/dist/sema-config-dir.d.ts +2 -1
  256. package/dist/session-sync.d.ts +27 -11
  257. package/dist/session-sync.js +15 -7
  258. package/dist/stores-sql/blob-index-sql.d.ts +30 -9
  259. package/dist/stores-sql/blob-index-sql.js +40 -21
  260. package/dist/stores-sql/checkpoint-store-sql.d.ts +54 -27
  261. package/dist/stores-sql/checkpoint-store-sql.js +70 -146
  262. package/dist/stores-sql/file-history-store-sql.d.ts +64 -16
  263. package/dist/stores-sql/file-history-store-sql.js +100 -28
  264. package/dist/stores-sql/retention-store-sql.js +3 -9
  265. package/dist/task-settings.d.ts +6 -5
  266. package/dist/task-settings.js +3 -3
  267. package/dist/tool-approval.d.ts +342 -518
  268. package/dist/tool-approval.js +498 -1207
  269. package/dist/trace/core-keyset-guard.d.ts +1 -1
  270. package/dist/trace/ledger-events.d.ts +3 -3
  271. package/dist/trace/project.d.ts +2 -3
  272. package/dist/trace/redact.js +3 -2
  273. package/dist/trace/self-minted-wire.d.ts +287 -157
  274. package/dist/trace/self-minted-wire.js +93 -48
  275. package/dist/web-admin-client/apply-effective.js +2 -2
  276. package/dist/web-admin-client/read-warnings.d.ts +2 -2
  277. package/dist/web-admin-client/read-warnings.js +1 -1
  278. package/dist/web-admin-client/types.d.ts +1 -1
  279. package/dist/workspace-space.d.ts +10 -4
  280. package/dist/workspace-space.js +4 -1
  281. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +1 -1
  282. package/package.json +6 -2
  283. package/scripts/embed-minimal.mjs +3 -3
  284. package/wire-changes/server@7.152.0.json +900 -0
  285. package/dist/config-d-migration.d.ts +0 -37
  286. package/dist/config-d-migration.js +0 -60
@@ -46,7 +46,8 @@ export interface HostEnvConfig extends LaneHomeConfig {
46
46
  * user's OWN directory (e.g. the cwd `run-local` was invoked in), not an adapter-created ephemeral sandbox,
47
47
  * so the agent's file edits PERSIST and are user-visible (like a local coding agent working in your repo).
48
48
  * Takes precedence over `workspaceBase`/`id` (those are ignored). Default unset = ephemeral random + cleanup.
49
- * 唯一的例外由托管表说:它落在一只本服务铸的临时根里({@link custody} 的 `sharedRoot`)⇒ 本环境共持那只根,不是调用方自有目录。
49
+ * 唯一的例外由托管表说:它落在一只本服务铸的临时根里 ⇒ 本环境(也)持有那只根 —— 铸时就判得出的({@link custody} 的 `sharedRoot`)从铸起共持;
50
+ * 铸时判不出、在那只根的最后一只持有者放手那一拍读得出地落在里面的,当场接过删除({@link RemoteHostExecutionEnv.assumeRoot})。
50
51
  */
51
52
  workspaceDir?: string;
52
53
  /**
@@ -72,10 +73,16 @@ export interface HostEnvConfig extends LaneHomeConfig {
72
73
  }
73
74
  /** 工作区托管表交给它铸的环境的那一面(`workspace-custody.ts`;host 车道的读法)。 */
74
75
  export interface HostWorkspaceCustodySeat {
75
- /** `workspaceDir` 落在这只本服务铸的临时根里(委派子代按父的工作目录落根)⇒ 本环境与它的铸者共持这只根:建成后登记、收场时放手、最后放手的持有者删它。 */
76
+ /**
77
+ * 铸时那一判:`workspaceDir` 落在这只本服务铸的临时根里(委派子代按父的工作目录落根)⇒ 本环境从铸起与它的持有者共持这只根。
78
+ * 这是删除责任的**提前交接**,不是删除的闸 —— 删不删只由 {@link passOn} 在放手那一拍答。
79
+ */
76
80
  readonly sharedRoot?: string;
77
- /** 表里除 `self` 之外还有没有成员持有 `root` —— 收场放手时问;有 ⇒ 删交给最后放手的那一只。 */
78
- heldElsewhere(self: RemoteHostExecutionEnv, root: string): boolean;
81
+ /**
82
+ * 删前那一判(本环境放手 `root` 的同一同步段里问;同步、永不抛):「此刻还有谁在 `root` 里」—— 另有持有者 / 有还没离场的环境读得出地落在里面(它当场接过)/
83
+ * 有读不出的(留给退出兜底)⇒ `true`,不删;读得出地没人 ⇒ `false`,本环境在同一同步段里把它移出原路径再删。
84
+ */
85
+ passOn(self: RemoteHostExecutionEnv, root: string): boolean;
79
86
  }
80
87
  export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, SchedulerCapability, BackgroundShellCapability, CustodyMember<never> {
81
88
  /** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。 */
@@ -191,8 +198,10 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
191
198
  */
192
199
  private drainInflightFs;
193
200
  private ensureSpoolRoot;
194
- /** 本环境对它工作区的租期({@link HostWorkspaceTenure}):借用的永不删;持有的收场放手、最后放手的持有者删。唯一写处 = 构造与 {@link removeOwnedTrees}。 */
201
+ /** 本环境对它工作区的租期({@link HostWorkspaceTenure}):借用的永不删;持有的收场放手、放手那一拍判得没人才删。唯一写处 = 构造、{@link assumeRoot} 与 {@link removeOwnedTrees}。 */
195
202
  private tenure;
203
+ /** 离场({@link left}):唯一写处 = {@link removeOwnedTrees} 的放手段。 */
204
+ private hasLeft;
196
205
  /** Memoized lazy `mkdir -p` so concurrent first-use prepares the workspace exactly once. */
197
206
  private preparing?;
198
207
  /**
@@ -208,10 +217,20 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
208
217
  get released(): boolean;
209
218
  /** 本车道收场不欠同一身份下一只环境任何账(删不掉的根留在退出兜底簿上,不交接)。 */
210
219
  readonly leftover: undefined;
211
- /** 收场落定且已放手 ⇒ 出托管表(超时臂里放手挂在在飞写之后:落定了还持有 ⇒ 仍是持有者)。 */
220
+ /**
221
+ * 离场:本环境自己的收场放手那一段({@link removeOwnedTrees},在飞写落地之后)跑过。删前那一判只问还没离场的环境「你在不在这只根里」——
222
+ * 收场开始了而写还没落地的仍在场(它的写还会落进去);离场之后它不再动工作区(残余镜像写只许落在借用目录 / spool 根上)。
223
+ */
224
+ get left(): boolean;
225
+ /** 收场落定且已离场 ⇒ 出托管表(超时臂里放手挂在在飞写之后:落定了还没离场 ⇒ 仍在表里,借用的与持有的同一句)。 */
212
226
  get settled(): boolean;
213
- /** 本环境此刻持有的、本服务铸的临时根(托管表的持有读法读它);借用 / 已放手 ⇒ 缺席。 */
214
- get heldWorkspace(): string | undefined;
227
+ /** 本环境此刻持有的、本服务铸的临时根(托管表的持有读法读它;正常流至多一只);借用 / 已放手 ⇒ 空。 */
228
+ get heldRoots(): readonly string[];
229
+ /**
230
+ * 删前那一判把 `root` 的删除交给本环境。唯一调用方 = 工厂的 {@link passOnRoot}(与放手者的判在同一同步段;前提:本环境还没离场、工作目录读得出地落在 `root` 里):
231
+ * 持有集合加上它(借用的从此持有;已持有别的根的两只都持有),离场时按同一句律逐根再判。`root` 在盘或读不出 ⇒ 当场登记退出兜底簿(本环境的 prepare 可能早已跑过、不会再登记)。
232
+ */
233
+ assumeRoot(root: string): void;
215
234
  workspaceHandle(): WorkspaceHandle;
216
235
  connect(config?: RemoteConnectConfig): Promise<{
217
236
  ok: true;
@@ -507,8 +526,8 @@ export interface HostLegConfig extends Pick<HostEnvConfig, "workspaceDir" | "env
507
526
  /**
508
527
  * host 车道的工厂。**一只身份一只工作区**:经工作区托管表(`workspace-custody.ts`,与 local-docker 车道同一只)铸环境 ——
509
528
  * 同一身份(`ctx.taskId ?? ctx.sessionId`)还有一只没收场的环境 ⇒ 交回它(停驻腿的环境 = 续跑腿的环境:引擎 park-only 契约要的「重连到同一工作区」);
510
- * 正在收场 ⇒ 等它落定再铸。铸的时候问同一张表「腿的目录落在哪只本服务铸的临时根里」:落在里面(委派子代按父的工作目录落根)⇒ 与铸者共持那只根,
511
- * 父收场时子代还持有它就不删,最后放手的持有者删。`legOf` 先于交回 / 铸判(它可以拒)。
529
+ * 正在收场 ⇒ 等它落定再铸。本服务铸的临时根只在它的最后一只持有者放手那一拍、按表里还没离场的环境此刻的工作目录判得没人之后才删({@link passOnRoot});
530
+ * 铸时那一判({@link heldRootContaining})只把落在持有根里的新腿当场记成共持者。`legOf` 先于交回 / 铸判(它可以拒)。
512
531
  * 起服铸**一只**工厂(每次调用另铸一只工厂 = 没有表)。
513
532
  */
514
533
  export declare function hostExecutionEnvFactory(config?: HostEnvConfig, legOf?: (ctx: ExecutionEnvFactoryContext) => HostLegConfig): ExecutionEnvFactory;
@@ -5,7 +5,7 @@ import path, {} from "node:path";
5
5
  import { anchoredPath, anchorUnder, joinedPath } from "./exec-path-law.js";
6
6
  import fs from "node:fs/promises";
7
7
  import { randomBytes } from "node:crypto";
8
- import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdirSync, rmdirSync, mkdtempSync, existsSync, accessSync, constants as FS_CONST } from "node:fs";
8
+ import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdirSync, rmdirSync, mkdtempSync, existsSync, accessSync, renameSync, constants as FS_CONST } from "node:fs";
9
9
  import { StringDecoder } from "node:string_decoder";
10
10
  import { armPipeDestroyGrace, numEnvOr } from "./remote-shell.js";
11
11
  import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
@@ -21,7 +21,8 @@ import { composeHostSubprocessEnv, hostSubprocessEnv, withheldFromHostSubprocess
21
21
  import { nameTooLongCode } from "./remote-env-file-error.js";
22
22
  import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
23
23
  import { createWorkspaceCustody } from "./workspace-custody.js";
24
- import { canonicalPathOf, pathIsWithin } from "../memory-scope.js";
24
+ import { canonicalPathOf, pathIsWithin, presentOnDisk } from "../memory-scope.js";
25
+ import { errorCodeOf } from "../observability/err-text.js";
25
26
  import { OWNER_ONLY_FILE_MODE } from "./owner-only-file-mode.js";
26
27
  import { createConnectSeat, withdrawOnAbort } from "./remote-env-connect-seat.js";
27
28
  const PROVIDER = "host";
@@ -41,6 +42,7 @@ const unsupported = (op) => ({
41
42
  });
42
43
  const BORROWED = Object.freeze({ kind: "borrowed" });
43
44
  const RELEASED = Object.freeze({ kind: "released" });
45
+ const NO_ROOTS = Object.freeze([]);
44
46
  const UNSUPPORTED_SCHEDULER_CAPS = {
45
47
  supported: false,
46
48
  maxScheduledPerScope: 0,
@@ -132,6 +134,7 @@ export class RemoteHostExecutionEnv {
132
134
  return this.spoolRootDir;
133
135
  }
134
136
  tenure;
137
+ hasLeft = false;
135
138
  preparing;
136
139
  seat = createConnectSeat({
137
140
  live: () => (this.prepared && !this.destroyed ? this.handle : undefined),
@@ -143,14 +146,14 @@ export class RemoteHostExecutionEnv {
143
146
  if (config.workspaceDir) {
144
147
  this.workspaceDir = hostSeatPath(config.workspaceDir);
145
148
  const shared = config.custody?.sharedRoot;
146
- this.tenure = shared !== undefined ? { kind: "held", root: hostSeatPath(shared) } : BORROWED;
149
+ this.tenure = shared !== undefined ? { kind: "held", roots: [hostSeatPath(shared)] } : BORROWED;
147
150
  }
148
151
  else {
149
152
  const base = hostSeatPath(config.workspaceBase ?? os.tmpdir());
150
153
  const id = config.id ?? randomBytes(6).toString("hex");
151
154
  const suffix = randomBytes(4).toString("hex");
152
155
  this.workspaceDir = anchorUnder(path, base, `sema-host-${sanitizeId(id)}-${suffix}`);
153
- this.tenure = { kind: "held", root: this.workspaceDir };
156
+ this.tenure = { kind: "held", roots: [this.workspaceDir] };
154
157
  }
155
158
  const inheritEnvProblem = inheritEnvPolicyProblem(config.inheritEnv);
156
159
  if (inheritEnvProblem !== undefined)
@@ -178,11 +181,18 @@ export class RemoteHostExecutionEnv {
178
181
  return this.teardownSettled;
179
182
  }
180
183
  leftover = undefined;
184
+ get left() {
185
+ return this.hasLeft;
186
+ }
181
187
  get settled() {
182
- return this.teardownSettled && this.tenure.kind !== "held";
188
+ return this.teardownSettled && this.hasLeft;
189
+ }
190
+ get heldRoots() {
191
+ return this.tenure.kind === "held" ? this.tenure.roots : NO_ROOTS;
183
192
  }
184
- get heldWorkspace() {
185
- return this.tenure.kind === "held" ? this.tenure.root : undefined;
193
+ assumeRoot(root) {
194
+ this.tenure = { kind: "held", roots: this.tenure.kind === "held" ? [...this.tenure.roots, root] : [root] };
195
+ trackUnlessAbsent(root);
186
196
  }
187
197
  workspaceHandle() {
188
198
  return this.handle;
@@ -243,17 +253,37 @@ export class RemoteHostExecutionEnv {
243
253
  catch { }
244
254
  this.spoolRootDir = undefined;
245
255
  }
256
+ this.hasLeft = true;
246
257
  const tenure = this.tenure;
247
258
  if (tenure.kind !== "held")
248
259
  return;
249
260
  this.tenure = RELEASED;
250
- if (this.cfg.custody?.heldElsewhere(this, tenure.root) === true)
251
- return;
252
- try {
253
- await fs.rm(tenure.root, { recursive: true, force: true });
254
- untrackEphemeralHostWorkspace(tenure.root);
261
+ const tombs = [];
262
+ for (const root of tenure.roots) {
263
+ if (this.cfg.custody?.passOn(this, root) === true)
264
+ continue;
265
+ const tomb = removalTombOf(root);
266
+ try {
267
+ renameSync(root, tomb);
268
+ }
269
+ catch (e) {
270
+ if (e?.code === "ENOENT")
271
+ untrackEphemeralHostWorkspace(root);
272
+ else
273
+ trackUnlessAbsent(root);
274
+ continue;
275
+ }
276
+ trackEphemeralHostWorkspace(tomb);
277
+ untrackEphemeralHostWorkspace(root);
278
+ tombs.push(tomb);
255
279
  }
256
- catch {
280
+ for (const tomb of tombs) {
281
+ try {
282
+ await fs.rm(tomb, { recursive: true, force: true });
283
+ untrackEphemeralHostWorkspace(tomb);
284
+ }
285
+ catch {
286
+ }
257
287
  }
258
288
  }
259
289
  async cleanup() {
@@ -1297,8 +1327,8 @@ export class RemoteHostExecutionEnv {
1297
1327
  .mkdir(this.workspaceDir, { recursive: true })
1298
1328
  .then(() => {
1299
1329
  this.prepared = true;
1300
- if (this.tenure.kind === "held")
1301
- trackEphemeralHostWorkspace(this.tenure.root);
1330
+ for (const root of this.heldRoots)
1331
+ trackUnlessAbsent(root);
1302
1332
  })
1303
1333
  .catch((e) => {
1304
1334
  this.preparing = undefined;
@@ -1437,17 +1467,67 @@ function asError(e) {
1437
1467
  function signalNumber(signal) {
1438
1468
  return os.constants.signals[signal];
1439
1469
  }
1470
+ function heldRootContaining(custody, dir) {
1471
+ const found = [];
1472
+ custody.holder((m) => {
1473
+ const root = m.heldRoots.find((r) => pathIsWithin(r, dir, canonicalPathOf));
1474
+ if (root !== undefined)
1475
+ found.push(root);
1476
+ return root !== undefined;
1477
+ });
1478
+ return found[0];
1479
+ }
1480
+ function passOnRoot(custody, self, root) {
1481
+ if (custody.holder((m) => m !== self && m.heldRoots.includes(root)) !== undefined)
1482
+ return true;
1483
+ const unreadable = [];
1484
+ const occupant = custody.holder((m) => {
1485
+ if (m === self || m.left)
1486
+ return false;
1487
+ try {
1488
+ return pathIsWithin(root, m.cwd, canonicalPathOf);
1489
+ }
1490
+ catch (error) {
1491
+ unreadable.push({ dir: m.cwd, error });
1492
+ return false;
1493
+ }
1494
+ });
1495
+ if (occupant !== undefined) {
1496
+ occupant.assumeRoot(root);
1497
+ return true;
1498
+ }
1499
+ const first = unreadable[0];
1500
+ if (first === undefined)
1501
+ return false;
1502
+ trackUnlessAbsent(root);
1503
+ writeLogText("stderr", `RemoteHostExecutionEnv: kept ${root} instead of removing it — could not read whether the working directory ${first.dir} lies inside it ` +
1504
+ `(${errorCodeOf(first.error) ?? "unreadable"}); nobody takes it over, and the exit-time sweep removes it`);
1505
+ return true;
1506
+ }
1507
+ function trackUnlessAbsent(root) {
1508
+ let absent;
1509
+ try {
1510
+ absent = !presentOnDisk(root);
1511
+ }
1512
+ catch {
1513
+ absent = false;
1514
+ }
1515
+ if (!absent)
1516
+ trackEphemeralHostWorkspace(root);
1517
+ }
1518
+ function removalTombOf(root) {
1519
+ return anchorUnder(path, path.dirname(root), `.${path.basename(root)}.removing-${randomBytes(4).toString("hex")}`);
1520
+ }
1440
1521
  export function hostExecutionEnvFactory(config = {}, legOf = () => ({})) {
1441
1522
  const custody = createWorkspaceCustody();
1442
- const heldElsewhere = (self, root) => custody.holder((m) => m !== self && m.heldWorkspace === root) !== undefined;
1443
- const rootHolding = (dir) => custody.holder((m) => m.heldWorkspace !== undefined && pathIsWithin(m.heldWorkspace, dir, canonicalPathOf))?.heldWorkspace;
1523
+ const passOn = (self, root) => passOnRoot(custody, self, root);
1444
1524
  return (ctx) => {
1445
1525
  const { minted, ...leg } = legOf(ctx);
1446
1526
  const identity = ctx.taskId ?? ctx.sessionId;
1447
1527
  return custody.envOf(identity, () => {
1448
1528
  const dir = leg.workspaceDir ?? config.workspaceDir;
1449
- const sharedRoot = dir === undefined ? undefined : rootHolding(hostSeatPath(dir));
1450
- const env = new RemoteHostExecutionEnv({ ...config, ...leg, id: config.id ?? identity, custody: { heldElsewhere, ...(sharedRoot !== undefined ? { sharedRoot } : {}) } });
1529
+ const sharedRoot = dir === undefined ? undefined : heldRootContaining(custody, hostSeatPath(dir));
1530
+ const env = new RemoteHostExecutionEnv({ ...config, ...leg, id: config.id ?? identity, custody: { passOn, ...(sharedRoot !== undefined ? { sharedRoot } : {}) } });
1451
1531
  minted?.(env);
1452
1532
  return env;
1453
1533
  });
@@ -2,6 +2,7 @@ import { type SpaceReclaimer, type WorkspaceSpacePolicy, type CacheReclaimReason
2
2
  import { type SpaceCommitFence } from "../workspace-space-commit.js";
3
3
  import { type CacheMissMemory, type WorkspaceCacheCapability } from "../workspace-cache.js";
4
4
  import { type SandboxEgressGap, type SandboxIpv6 } from "../sandbox-egress-policy.js";
5
+ import { type K8sApiCredentialFact } from "../k8s-api-credentials.js";
5
6
  import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type ExecResult, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type BoundFileObject, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll, type ExecutionEnvCapabilities } from "@sema-agent/core";
6
7
  import { type SandboxReadFloorKnob } from "../execution-lane-caps.js";
7
8
  import { type SandboxReadFloor } from "./sandbox-read-floor.js";
@@ -10,6 +11,37 @@ import { type LaneHomeConfig } from "../lane-home-probe.js";
10
11
  import { type ConnectResult } from "./remote-env-connect-seat.js";
11
12
  type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
12
13
  export { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, buildKillAllScript, parseBgPollOutput, type ParsedBgPoll, } from "./k8s-bg-scripts.js";
14
+ /**
15
+ * This process's in-cluster identity: the platform's `KUBERNETES_SERVICE_*` and the mounted service-account files. `readMounted` throws when the file is unreadable.
16
+ * (In this file a bare `read(` names a control-plane GET — the RBAC table's source reader classifies every one — so the mount reader carries its own name.)
17
+ */
18
+ export interface InClusterIdentity {
19
+ readonly host: string | undefined;
20
+ readonly port: string | undefined;
21
+ readMounted(file: "token" | "ca.crt"): string;
22
+ }
23
+ /** What the control plane can be reached with — or the ONE missing fact (with the adapter code its request-time refusal carries). */
24
+ export type K8sApiAccess = {
25
+ readonly ok: true;
26
+ readonly apiUrl: string;
27
+ readonly token: string;
28
+ readonly ca: string | undefined;
29
+ } | {
30
+ readonly ok: false;
31
+ readonly fact: K8sApiCredentialFact;
32
+ readonly code: "connect_failed" | "auth_failed";
33
+ };
34
+ /**
35
+ * The ONE judge of "does this lane have cluster API credentials": explicit config (`K8S_API_URL` / `K8S_TOKEN` / `K8S_CA_CERT_B64`, `K8S_INSECURE_TLS`) first, the
36
+ * in-cluster service account second, per piece. The request-time reader ({@link RemoteK8sExecutionEnv}'s `creds()`), the boot gate and the per-run credential
37
+ * reading all read THIS function — what the boot refuses on is exactly what every request would have thrown on. (No kubeconfig: this lane never reads one.)
38
+ */
39
+ export declare function resolveK8sApiAccess(cfg: {
40
+ readonly apiUrl?: string | undefined;
41
+ readonly token?: string | undefined;
42
+ readonly caCert?: string | undefined;
43
+ readonly insecureTls?: boolean | undefined;
44
+ }, inCluster?: InClusterIdentity): K8sApiAccess;
13
45
  export interface K8sEnvConfig extends LaneHomeConfig {
14
46
  /** API server URL. Default: in-cluster (`https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT`). */
15
47
  apiUrl?: string;
@@ -194,6 +226,21 @@ export interface K8sRequestOptions {
194
226
  signal?: AbortSignal;
195
227
  contentType?: string;
196
228
  }
229
+ /**
230
+ * The control plane's reading of this lane's credentials, a closed set: `missing` (the judge's fact — nothing to send), `rejected` (the cluster answered HTTP 401),
231
+ * `accepted` (a 2xx answer: the credentials work), `unproven` (any other answer, or none — see {@link RemoteK8sExecutionEnv.apiCredentialReading}).
232
+ */
233
+ export type ApiCredentialReading = {
234
+ readonly state: "missing";
235
+ readonly fact: K8sApiCredentialFact;
236
+ } | {
237
+ readonly state: "rejected";
238
+ } | {
239
+ readonly state: "accepted";
240
+ } | {
241
+ readonly state: "unproven";
242
+ readonly detail: string;
243
+ };
197
244
  export interface K8sEnvDeps {
198
245
  request?: (method: string, apiPath: string, body?: unknown, opts?: K8sRequestOptions) => Promise<{
199
246
  status: number;
@@ -203,6 +250,8 @@ export interface K8sEnvDeps {
203
250
  connectOnce?: (signal?: AbortSignal) => Promise<ConnectResult>;
204
251
  /** Worker-side object-store requests (the space pointer, listing, deletes). Default: global `fetch`; either way the request goes out through the egress exit (an endpoint that answers with a redirect is refused, never followed). */
205
252
  fetch?: typeof fetch;
253
+ /** This process's in-cluster identity (default: the process env's `KUBERNETES_SERVICE_*` and the mounted service-account files); a test replaces it. */
254
+ inCluster?: () => InClusterIdentity;
206
255
  /** 这只环境手里的读地板模块(缺席 ⇒ `createSandboxReadFloor` 按旋钮选;钉注入守地板的模块)。能力块的 `keepsReadFloor` 读它的状态。 */
207
256
  readFloor?: SandboxReadFloor;
208
257
  }
@@ -369,8 +418,18 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
369
418
  get lifetimeMs(): number;
370
419
  private resolve;
371
420
  workspaceHandle(): WorkspaceHandle;
372
- /** Resolved lazily so an in-cluster pod picks up its service account without explicit config. */
421
+ /** Resolved lazily so an in-cluster pod picks up its service account without explicit config ({@link resolveK8sApiAccess} is the one judge). */
373
422
  private creds;
423
+ /** The missing credential fact of this env's control-plane access, or `undefined` (nothing missing) — {@link resolveK8sApiAccess}, the one judge. */
424
+ missingApiCredential(): K8sApiCredentialFact | undefined;
425
+ /**
426
+ * The control plane's reading of this lane's credentials, asked once per run before its env is minted ({@link readSandboxApiCredentials}): the judge's missing fact
427
+ * ({@link missingApiCredential}), else one SelfSubjectAccessReview (answered for every authenticated caller) — an HTTP 401 is the cluster rejecting the credentials,
428
+ * a 2xx the cluster accepting them. ONLY a positive proof of rejection refuses: any other answer (403, 5xx, a request that threw or timed out) is `unproven` — another
429
+ * fact (a Role, an outage) that the lane's own connect answers loudly and retries within its own bounds; refusing the run on it would turn every API blip into a failed run.
430
+ * The caller counts an `unproven` reading (a registered fail-open arm). Never throws.
431
+ */
432
+ apiCredentialReading(): Promise<ApiCredentialReading>;
374
433
  /** Control-plane REST call (pod create/get/delete), bounded by rpcTimeoutMs. Injectable for tests. */
375
434
  private request;
376
435
  private openExec;
@@ -554,7 +613,7 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
554
613
  * snapshotKey()+s3Presign() pair so the two call sites can't diverge.
555
614
  */
556
615
  private presignSnapshot;
557
- /** The key prefix of every snapshot object this env signs — the ONE reading of `keyPrefix` (unset ⇒ the default); its form is judged in {@link presignSnapshot}. */
616
+ /** The key prefix of every snapshot object this env signs — `workspaceSnapshotPrefixOf`, the ONE reading of `keyPrefix` (unset ⇒ the default; the boot bucket check lists under the same reading); its form is judged in {@link presignSnapshot}. */
558
617
  private snapshotKeyPrefix;
559
618
  /** A lane's partition prefix inside the bucket — the ONE place it is spelled (`presignSnapshot` signs under it; the reclaim reads the listing against it).
560
619
  * The partition segment is validated by `presignSnapshot` before a URL is signed. */
@@ -927,6 +986,13 @@ export declare function createSpaceReclaimer(config: K8sEnvConfig, deps?: K8sEnv
927
986
  * nodes that join after boot are read at the next boot. Never throws (a read failure is an `unreadable` gap).
928
987
  */
929
988
  export declare function checkSandboxEgressPolicy(config: K8sEnvConfig, deps?: K8sEnvDeps): Promise<SandboxEgressGap[]>;
989
+ /**
990
+ * The missing cluster-API credential fact of this k8s config's control-plane access ({@link RemoteK8sExecutionEnv.missingApiCredential}), or `undefined`. The boot
991
+ * gate's judgment: the one judge ({@link resolveK8sApiAccess}) over the config and the process's in-cluster identity. Never throws.
992
+ */
993
+ export declare function sandboxApiCredentialFact(config: K8sEnvConfig, deps?: K8sEnvDeps): K8sApiCredentialFact | undefined;
994
+ /** The control plane's reading of this lane's credentials ({@link RemoteK8sExecutionEnv.apiCredentialReading}; a control-plane env, no pod). Never throws. */
995
+ export declare function readSandboxApiCredentials(config: K8sEnvConfig, deps?: K8sEnvDeps): Promise<ApiCredentialReading>;
930
996
  /** The boot reading of one sandbox Secret seat for this k8s config ({@link RemoteK8sExecutionEnv.secretSeatReading}; a control-plane env, no pod). Never throws. */
931
997
  export declare function readSandboxSecretSeat(config: K8sEnvConfig, seat: SandboxSecretSeatKind, deps?: K8sEnvDeps): Promise<SandboxSecretSeat>;
932
998
  /**
@@ -9,7 +9,7 @@ import { appendFileStaged, buildCwdEnvCommand, createCwdGuardedCall, shellQuote,
9
9
  import { presignS3Url, presignS3ListUrl } from "./s3-presign.js";
10
10
  import { WORKSPACE_SNAPSHOT_BUCKET_DEFAULT } from "../object-store-buckets.js";
11
11
  import { SNAPSHOT_BUCKET_MISSING, isNoSuchBucketAnswer } from "./s3-answer.js";
12
- 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
+ import { K8S_WORKSPACE_MOUNT_DEFAULT, SNAPSHOT_KEY_SEGMENT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, mintSnapshotId, snapshotIdHolds, snapshotKeyPrefixProblem, snapshotNameOwnedBy, workspaceSnapshotPrefixOf } from "../workspace-space.js";
13
13
  import { spaceCommitAlive } from "../workspace-space-commit.js";
14
14
  import { SESSION_INCARNATION_MISSING, incarnationLogPrefix, redactIncarnations } from "../session-incarnation.js";
15
15
  import { WORKSPACE_CACHE_SPACE_LABEL, workspaceCacheClaimOf, workspaceCacheEnv } from "../workspace-cache.js";
@@ -18,6 +18,7 @@ import { PodDeleteRefused, reportPodDeleteFailed } from "./k8s-pod-delete-failur
18
18
  import { buildLivenessCut, commandBoundsOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
19
19
  import { buildCapturedOutput, createCutError } from "../exec-cut.js";
20
20
  import { SANDBOX_IPV6_DEFAULT, sandboxEgressGaps, sandboxPodSysctls } from "../sandbox-egress-policy.js";
21
+ import { describeMissingK8sCredential } from "../k8s-api-credentials.js";
21
22
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
22
23
  import { capabilitiesFor } from "../execution-lane-caps.js";
23
24
  import { DEPLOYMENT_SHAPE_PREDICATES } from "../deployment-shape.js";
@@ -44,6 +45,42 @@ const unsupported = (op) => ({
44
45
  error: new RemoteExecutionError("unsupported", `${op} is not supported on a Kata pod sandbox (no managed memory snapshot — capabilities.suspendable=false; durable suspend degrades to checkpoint + cold rebuild)`),
45
46
  });
46
47
  const SA_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
48
+ const processInCluster = () => ({
49
+ host: process.env.KUBERNETES_SERVICE_HOST,
50
+ port: process.env.KUBERNETES_SERVICE_PORT,
51
+ readMounted: (file) => readFileSync(`${SA_DIR}/${file}`, "utf8"),
52
+ });
53
+ export function resolveK8sApiAccess(cfg, inCluster = processInCluster()) {
54
+ const given = (v) => v !== undefined && v.trim() !== "";
55
+ const apiUrl = given(cfg.apiUrl) ? cfg.apiUrl : given(inCluster.host) ? `https://${inCluster.host}:${inCluster.port ?? "443"}` : undefined;
56
+ if (apiUrl === undefined)
57
+ return { ok: false, fact: "api_url", code: "connect_failed" };
58
+ let token = given(cfg.token) ? cfg.token : undefined;
59
+ if (token === undefined) {
60
+ try {
61
+ const mounted = inCluster.readMounted("token").trim();
62
+ token = mounted === "" ? undefined : mounted;
63
+ }
64
+ catch {
65
+ token = undefined;
66
+ }
67
+ if (token === undefined)
68
+ return { ok: false, fact: "token", code: "auth_failed" };
69
+ }
70
+ let ca = given(cfg.caCert) ? cfg.caCert : undefined;
71
+ if (ca === undefined && !cfg.insecureTls) {
72
+ try {
73
+ const mounted = inCluster.readMounted("ca.crt");
74
+ ca = given(mounted) ? mounted : undefined;
75
+ }
76
+ catch {
77
+ ca = undefined;
78
+ }
79
+ if (ca === undefined)
80
+ return { ok: false, fact: "ca", code: "auth_failed" };
81
+ }
82
+ return { ok: true, apiUrl, token, ca };
83
+ }
47
84
  const EXEC_SUBPROTOCOLS = ["v5.channel.k8s.io", "v4.channel.k8s.io"];
48
85
  const WRITE_CHUNK_BYTES = 64 * 1024;
49
86
  import { applyPodSpecPatch, ADAPTER_VOLUME_NAMES, SANDBOX_POD_LABELS, SANDBOX_RUN_END_HOLD_ANNOTATION, SANDBOX_RUN_LABEL, SANDBOX_SECRET_SEAT_VERBS, isLabelValue } from "./k8s-exec-protocol.js";
@@ -179,6 +216,42 @@ export function boundedStoreCall(op, bound) {
179
216
  });
180
217
  });
181
218
  }
219
+ function snapshotOpOf(req) {
220
+ if (req.method === "LIST")
221
+ return req.lane === "space" ? "k8s-space/prefix:LIST" : "k8s-park/prefix:LIST";
222
+ if ("object" in req)
223
+ return req.lane === "space" ? "k8s-space/object:DELETE" : "k8s-park/object:DELETE";
224
+ if ("pointer" in req) {
225
+ switch (req.method) {
226
+ case "GET":
227
+ return "k8s-space/latest:GET";
228
+ case "PUT":
229
+ return "k8s-space/latest:PUT";
230
+ case "DELETE":
231
+ return "k8s-space/latest:DELETE";
232
+ }
233
+ }
234
+ if (req.lane === "park") {
235
+ switch (req.method) {
236
+ case "GET":
237
+ return "k8s-park/archive:GET";
238
+ case "PUT":
239
+ return "k8s-park/archive:PUT";
240
+ case "HEAD":
241
+ return "k8s-park/archive:HEAD";
242
+ }
243
+ }
244
+ switch (req.method) {
245
+ case "GET":
246
+ return "k8s-space/archive:GET";
247
+ case "PUT":
248
+ return "k8s-space/archive:PUT";
249
+ case "DELETE":
250
+ return "k8s-space/archive:DELETE";
251
+ case "HEAD":
252
+ return "k8s-space/archive:HEAD";
253
+ }
254
+ }
182
255
  import { execSocketTlsOptions, exitCodeFromStatus } from "./k8s-exec-protocol.js";
183
256
  export { execSocketTlsOptions } from "./k8s-exec-protocol.js";
184
257
  export function errorMessageOf(e) {
@@ -323,29 +396,32 @@ export class RemoteK8sExecutionEnv {
323
396
  return { sandboxId: this.podName ?? "(unconnected)", provider: PROVIDER, mountPath: this.cwd, ...(this.handleSnapshotId ? { snapshotId: this.handleSnapshotId } : {}) };
324
397
  }
325
398
  creds() {
326
- const apiUrl = this.cfg.apiUrl ??
327
- (process.env.KUBERNETES_SERVICE_HOST ? `https://${process.env.KUBERNETES_SERVICE_HOST}:${process.env.KUBERNETES_SERVICE_PORT ?? "443"}` : undefined);
328
- if (!apiUrl)
329
- throw new RemoteExecutionError("connect_failed", "no k8s API: set K8S_API_URL or run in-cluster");
330
- let token = this.cfg.token;
331
- let ca = this.cfg.caCert;
332
- if (!token) {
333
- try {
334
- token = readFileSync(`${SA_DIR}/token`, "utf8").trim();
335
- }
336
- catch {
337
- throw new RemoteExecutionError("auth_failed", "no k8s token: set K8S_TOKEN or run in-cluster with a service account");
338
- }
399
+ const access = resolveK8sApiAccess(this.cfg, this.deps.inCluster?.());
400
+ if (!access.ok)
401
+ throw new RemoteExecutionError(access.code, describeMissingK8sCredential(access.fact));
402
+ return { apiUrl: access.apiUrl.replace(/\/+$/, ""), token: access.token, ca: access.ca };
403
+ }
404
+ missingApiCredential() {
405
+ const access = resolveK8sApiAccess(this.cfg, this.deps.inCluster?.());
406
+ return access.ok ? undefined : access.fact;
407
+ }
408
+ async apiCredentialReading() {
409
+ const fact = this.missingApiCredential();
410
+ if (fact !== undefined)
411
+ return { state: "missing", fact };
412
+ try {
413
+ const r = await this.request("POST", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", {
414
+ apiVersion: "authorization.k8s.io/v1",
415
+ kind: "SelfSubjectAccessReview",
416
+ spec: { resourceAttributes: { namespace: this.cfg.namespace, verb: "create", resource: "pods" } },
417
+ });
418
+ if (r.status === 401)
419
+ return { state: "rejected" };
420
+ return r.status >= 200 && r.status < 300 ? { state: "accepted" } : { state: "unproven", detail: `HTTP ${r.status}` };
339
421
  }
340
- if (!ca && !this.cfg.insecureTls) {
341
- try {
342
- ca = readFileSync(`${SA_DIR}/ca.crt`, "utf8");
343
- }
344
- catch {
345
- throw new RemoteExecutionError("auth_failed", "no k8s CA: set K8S_CA_CERT_B64, K8S_INSECURE_TLS=true (lab), or run in-cluster");
346
- }
422
+ catch (e) {
423
+ return { state: "unproven", detail: errorMessageOf(e) };
347
424
  }
348
- return { apiUrl: apiUrl.replace(/\/+$/, ""), token, ca };
349
425
  }
350
426
  async request(method, apiPath, body, opts) {
351
427
  if (this.deps.request)
@@ -1002,12 +1078,13 @@ export class RemoteK8sExecutionEnv {
1002
1078
  if (snapshotKeyPrefixProblem(this.snapshotKeyPrefix()) !== undefined)
1003
1079
  return { ok: false, error: new RemoteExecutionError("connect_failed", "unsafe snapshot key segment (keyPrefix): refusing to build a traversing object key") };
1004
1080
  const common = {
1081
+ op: snapshotOpOf(req),
1005
1082
  endpoint: s3.endpoint,
1006
1083
  bucket: s3.bucket,
1007
1084
  accessKey: s3.accessKey,
1008
1085
  secretKey: s3.secretKey,
1009
1086
  ...(s3.region ? { region: s3.region } : {}),
1010
- expiresSec: Math.min(s3.presignTtlSec ?? 600, 3600),
1087
+ ...(s3.presignTtlSec !== undefined ? { expiresSec: s3.presignTtlSec } : {}),
1011
1088
  };
1012
1089
  const base = this.snapshotBase(req);
1013
1090
  if (req.method === "LIST")
@@ -1016,7 +1093,7 @@ export class RemoteK8sExecutionEnv {
1016
1093
  return { ok: true, value: presignS3Url({ ...common, key: `${base}${object}`, method: req.method }) };
1017
1094
  }
1018
1095
  snapshotKeyPrefix() {
1019
- return this.cfg.s3Snapshot?.keyPrefix ?? WORKSPACE_SNAPSHOT_PREFIX_DEFAULT;
1096
+ return workspaceSnapshotPrefixOf(this.cfg.s3Snapshot);
1020
1097
  }
1021
1098
  snapshotBase(req) {
1022
1099
  const prefix = this.snapshotKeyPrefix();
@@ -2426,6 +2503,12 @@ export function createSpaceReclaimer(config, deps) {
2426
2503
  export async function checkSandboxEgressPolicy(config, deps) {
2427
2504
  return new RemoteK8sExecutionEnv(config, deps).egressPolicyGaps();
2428
2505
  }
2506
+ export function sandboxApiCredentialFact(config, deps) {
2507
+ return new RemoteK8sExecutionEnv(config, deps).missingApiCredential();
2508
+ }
2509
+ export async function readSandboxApiCredentials(config, deps) {
2510
+ return new RemoteK8sExecutionEnv(config, deps).apiCredentialReading();
2511
+ }
2429
2512
  export async function readSandboxSecretSeat(config, seat, deps) {
2430
2513
  return new RemoteK8sExecutionEnv(config, deps).secretSeatReading(seat);
2431
2514
  }
@@ -5,7 +5,7 @@ export declare const REPLICA_REGISTRATION_TABLE = "replica_registration";
5
5
  * 本构建的能力名单(常设、只增):每一条读停-迁-起证据的一次性迁移一个名。删码之后名字留着 —— 名单只增不减(钉:`test/replica-registration.test.ts`)。
6
6
  * 守卫读证据时传的名在型上就要在这里(`ReplicaCapability`):不在名单里的迁移读不了证据,编译期红。
7
7
  */
8
- export declare const REPLICA_CAPABILITIES: readonly ["M40", "M41", "M42", "M43", "M51"];
8
+ export declare const REPLICA_CAPABILITIES: readonly ["M40", "M41", "M42", "M43", "M51", "M54"];
9
9
  export type ReplicaCapability = (typeof REPLICA_CAPABILITIES)[number];
10
10
  /** 留存视界(写者钟):登记早于此的别人的行在下一次登记时删。量级取远大于任一副本的寿命与副本间钟差。 */
11
11
  export declare const REPLICA_REGISTRATION_HORIZON_MS: number;
@@ -2,7 +2,7 @@ import { hostname } from "node:os";
2
2
  import { processInstanceId } from "../process-instance.js";
3
3
  import { serviceVersion } from "../service-version.js";
4
4
  export const REPLICA_REGISTRATION_TABLE = "replica_registration";
5
- export const REPLICA_CAPABILITIES = ["M40", "M41", "M42", "M43", "M51"];
5
+ export const REPLICA_CAPABILITIES = ["M40", "M41", "M42", "M43", "M51", "M54"];
6
6
  export const REPLICA_REGISTRATION_HORIZON_MS = 90 * 24 * 60 * 60 * 1000;
7
7
  export function buildThisReplicaRegistration() {
8
8
  return { instanceId: processInstanceId(), releaseLine: serviceVersion(), capabilities: REPLICA_CAPABILITIES, hostName: hostname(), pid: process.pid };