@paperclipai/server 2026.817.0 → 2026.818.0-beta.1

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 (548) hide show
  1. package/dist/adapters/hermes-gateway-doc.d.ts +1 -1
  2. package/dist/adapters/hermes-gateway-doc.d.ts.map +1 -1
  3. package/dist/adapters/hermes-gateway-doc.js +1 -1
  4. package/dist/adapters/plugin-loader.d.ts.map +1 -1
  5. package/dist/adapters/plugin-loader.js +3 -2
  6. package/dist/adapters/plugin-loader.js.map +1 -1
  7. package/dist/agent-auth-jwt.d.ts.map +1 -1
  8. package/dist/agent-auth-jwt.js +8 -2
  9. package/dist/agent-auth-jwt.js.map +1 -1
  10. package/dist/app.d.ts +5 -1
  11. package/dist/app.d.ts.map +1 -1
  12. package/dist/app.js +202 -15
  13. package/dist/app.js.map +1 -1
  14. package/dist/built-ins/agents/summarizer/AGENTS.md +2 -2
  15. package/dist/built-ins/agents/summarizer/routines/refresh-stale-summaries.md +1 -0
  16. package/dist/db-errors.d.ts +9 -0
  17. package/dist/db-errors.d.ts.map +1 -1
  18. package/dist/db-errors.js +19 -0
  19. package/dist/db-errors.js.map +1 -1
  20. package/dist/errors.d.ts +1 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +2 -2
  23. package/dist/errors.js.map +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +162 -38
  26. package/dist/index.js.map +1 -1
  27. package/dist/lib/request-promise-memo.d.ts +6 -0
  28. package/dist/lib/request-promise-memo.d.ts.map +1 -0
  29. package/dist/lib/request-promise-memo.js +25 -0
  30. package/dist/lib/request-promise-memo.js.map +1 -0
  31. package/dist/lib/request-promise-memo.test.d.ts +2 -0
  32. package/dist/lib/request-promise-memo.test.d.ts.map +1 -0
  33. package/dist/lib/request-promise-memo.test.js +74 -0
  34. package/dist/lib/request-promise-memo.test.js.map +1 -0
  35. package/dist/middleware/auth.d.ts +13 -1
  36. package/dist/middleware/auth.d.ts.map +1 -1
  37. package/dist/middleware/auth.js +92 -41
  38. package/dist/middleware/auth.js.map +1 -1
  39. package/dist/middleware/cloud-tenant-actor.test.js +26 -1
  40. package/dist/middleware/cloud-tenant-actor.test.js.map +1 -1
  41. package/dist/middleware/error-handler.d.ts.map +1 -1
  42. package/dist/middleware/error-handler.js +11 -2
  43. package/dist/middleware/error-handler.js.map +1 -1
  44. package/dist/middleware/logger.d.ts.map +1 -1
  45. package/dist/middleware/logger.js +7 -33
  46. package/dist/middleware/logger.js.map +1 -1
  47. package/dist/middleware/private-hostname-guard.d.ts.map +1 -1
  48. package/dist/middleware/private-hostname-guard.js +11 -6
  49. package/dist/middleware/private-hostname-guard.js.map +1 -1
  50. package/dist/middleware/private-json-etag.d.ts +3 -0
  51. package/dist/middleware/private-json-etag.d.ts.map +1 -0
  52. package/dist/middleware/private-json-etag.js +37 -0
  53. package/dist/middleware/private-json-etag.js.map +1 -0
  54. package/dist/middleware/redact-sensitive.d.ts.map +1 -1
  55. package/dist/middleware/redact-sensitive.js +11 -0
  56. package/dist/middleware/redact-sensitive.js.map +1 -1
  57. package/dist/realtime/live-events-ws.d.ts +12 -0
  58. package/dist/realtime/live-events-ws.d.ts.map +1 -1
  59. package/dist/realtime/live-events-ws.js +20 -0
  60. package/dist/realtime/live-events-ws.js.map +1 -1
  61. package/dist/redaction.js +1 -1
  62. package/dist/redaction.js.map +1 -1
  63. package/dist/routes/access.d.ts +2 -0
  64. package/dist/routes/access.d.ts.map +1 -1
  65. package/dist/routes/access.js +21 -10
  66. package/dist/routes/access.js.map +1 -1
  67. package/dist/routes/agents.d.ts +42 -0
  68. package/dist/routes/agents.d.ts.map +1 -1
  69. package/dist/routes/agents.js +875 -8
  70. package/dist/routes/agents.js.map +1 -1
  71. package/dist/routes/companies.d.ts +5 -1
  72. package/dist/routes/companies.d.ts.map +1 -1
  73. package/dist/routes/companies.js +467 -30
  74. package/dist/routes/companies.js.map +1 -1
  75. package/dist/routes/environments.d.ts +12 -3
  76. package/dist/routes/environments.d.ts.map +1 -1
  77. package/dist/routes/environments.js +139 -23
  78. package/dist/routes/environments.js.map +1 -1
  79. package/dist/routes/execution-workspaces.d.ts.map +1 -1
  80. package/dist/routes/execution-workspaces.js +240 -70
  81. package/dist/routes/execution-workspaces.js.map +1 -1
  82. package/dist/routes/file-resources.d.ts +14 -1
  83. package/dist/routes/file-resources.d.ts.map +1 -1
  84. package/dist/routes/file-resources.js +182 -3
  85. package/dist/routes/file-resources.js.map +1 -1
  86. package/dist/routes/index.d.ts +2 -1
  87. package/dist/routes/index.d.ts.map +1 -1
  88. package/dist/routes/index.js +2 -1
  89. package/dist/routes/index.js.map +1 -1
  90. package/dist/routes/instance-settings.d.ts.map +1 -1
  91. package/dist/routes/instance-settings.js +20 -1
  92. package/dist/routes/instance-settings.js.map +1 -1
  93. package/dist/routes/issues.d.ts.map +1 -1
  94. package/dist/routes/issues.js +750 -235
  95. package/dist/routes/issues.js.map +1 -1
  96. package/dist/routes/onboarding-seed.d.ts +27 -0
  97. package/dist/routes/onboarding-seed.d.ts.map +1 -0
  98. package/dist/routes/onboarding-seed.js +56 -0
  99. package/dist/routes/onboarding-seed.js.map +1 -0
  100. package/dist/routes/openapi.d.ts.map +1 -1
  101. package/dist/routes/openapi.js +316 -2
  102. package/dist/routes/openapi.js.map +1 -1
  103. package/dist/routes/secrets.d.ts.map +1 -1
  104. package/dist/routes/secrets.js +11 -1
  105. package/dist/routes/secrets.js.map +1 -1
  106. package/dist/routes/setup-token-route.test.d.ts +2 -0
  107. package/dist/routes/setup-token-route.test.d.ts.map +1 -0
  108. package/dist/routes/setup-token-route.test.js +896 -0
  109. package/dist/routes/setup-token-route.test.js.map +1 -0
  110. package/dist/routes/tool-access.d.ts.map +1 -1
  111. package/dist/routes/tool-access.js +87 -52
  112. package/dist/routes/tool-access.js.map +1 -1
  113. package/dist/routes/workspace-runtime-service-authz.d.ts +11 -1
  114. package/dist/routes/workspace-runtime-service-authz.d.ts.map +1 -1
  115. package/dist/routes/workspace-runtime-service-authz.js +18 -16
  116. package/dist/routes/workspace-runtime-service-authz.js.map +1 -1
  117. package/dist/services/access.d.ts.map +1 -1
  118. package/dist/services/activity.d.ts +1 -1
  119. package/dist/services/agents.d.ts +21 -0
  120. package/dist/services/agents.d.ts.map +1 -1
  121. package/dist/services/agents.js +121 -1
  122. package/dist/services/agents.js.map +1 -1
  123. package/dist/services/attention-resolver-audience.test.d.ts +2 -0
  124. package/dist/services/attention-resolver-audience.test.d.ts.map +1 -0
  125. package/dist/services/attention-resolver-audience.test.js +81 -0
  126. package/dist/services/attention-resolver-audience.test.js.map +1 -0
  127. package/dist/services/attention.d.ts +20 -1
  128. package/dist/services/attention.d.ts.map +1 -1
  129. package/dist/services/attention.js +45 -1
  130. package/dist/services/attention.js.map +1 -1
  131. package/dist/services/authorization.d.ts +2 -2
  132. package/dist/services/authorization.d.ts.map +1 -1
  133. package/dist/services/authorization.js +89 -46
  134. package/dist/services/authorization.js.map +1 -1
  135. package/dist/services/board-auth.d.ts.map +1 -1
  136. package/dist/services/board-auth.js +40 -1
  137. package/dist/services/board-auth.js.map +1 -1
  138. package/dist/services/board-auth.test.d.ts +2 -0
  139. package/dist/services/board-auth.test.d.ts.map +1 -0
  140. package/dist/services/board-auth.test.js +34 -0
  141. package/dist/services/board-auth.test.js.map +1 -0
  142. package/dist/services/built-in-agents.d.ts.map +1 -1
  143. package/dist/services/built-in-agents.js +20 -4
  144. package/dist/services/built-in-agents.js.map +1 -1
  145. package/dist/services/bundled-plugins.d.ts +11 -1
  146. package/dist/services/bundled-plugins.d.ts.map +1 -1
  147. package/dist/services/bundled-plugins.js +46 -1
  148. package/dist/services/bundled-plugins.js.map +1 -1
  149. package/dist/services/codex-device-login-reaper.d.ts +61 -0
  150. package/dist/services/codex-device-login-reaper.d.ts.map +1 -0
  151. package/dist/services/codex-device-login-reaper.js +219 -0
  152. package/dist/services/codex-device-login-reaper.js.map +1 -0
  153. package/dist/services/codex-device-login-service.d.ts +355 -0
  154. package/dist/services/codex-device-login-service.d.ts.map +1 -0
  155. package/dist/services/codex-device-login-service.js +775 -0
  156. package/dist/services/codex-device-login-service.js.map +1 -0
  157. package/dist/services/companies.d.ts.map +1 -1
  158. package/dist/services/companies.js +7 -0
  159. package/dist/services/companies.js.map +1 -1
  160. package/dist/services/company-export-readme.js +1 -1
  161. package/dist/services/company-export-readme.js.map +1 -1
  162. package/dist/services/company-import-transfers.d.ts +55 -0
  163. package/dist/services/company-import-transfers.d.ts.map +1 -0
  164. package/dist/services/company-import-transfers.js +151 -0
  165. package/dist/services/company-import-transfers.js.map +1 -0
  166. package/dist/services/company-portability.d.ts.map +1 -1
  167. package/dist/services/company-portability.js +128 -3
  168. package/dist/services/company-portability.js.map +1 -1
  169. package/dist/services/company-transfer-runs.d.ts +136 -0
  170. package/dist/services/company-transfer-runs.d.ts.map +1 -0
  171. package/dist/services/company-transfer-runs.js +243 -0
  172. package/dist/services/company-transfer-runs.js.map +1 -0
  173. package/dist/services/cross-issue-influence-limit.d.ts +7 -1
  174. package/dist/services/cross-issue-influence-limit.d.ts.map +1 -1
  175. package/dist/services/cross-issue-influence-limit.js.map +1 -1
  176. package/dist/services/environment-config.d.ts +15 -0
  177. package/dist/services/environment-config.d.ts.map +1 -1
  178. package/dist/services/environment-config.js +56 -10
  179. package/dist/services/environment-config.js.map +1 -1
  180. package/dist/services/environment-execution-target.d.ts.map +1 -1
  181. package/dist/services/environment-execution-target.js +61 -11
  182. package/dist/services/environment-execution-target.js.map +1 -1
  183. package/dist/services/environment-run-orchestrator.d.ts +16 -1
  184. package/dist/services/environment-run-orchestrator.d.ts.map +1 -1
  185. package/dist/services/environment-run-orchestrator.js +1 -1
  186. package/dist/services/environment-run-orchestrator.js.map +1 -1
  187. package/dist/services/environment-runtime.d.ts +178 -2
  188. package/dist/services/environment-runtime.d.ts.map +1 -1
  189. package/dist/services/environment-runtime.js +1035 -96
  190. package/dist/services/environment-runtime.js.map +1 -1
  191. package/dist/services/environments.d.ts +75 -0
  192. package/dist/services/environments.d.ts.map +1 -1
  193. package/dist/services/environments.js +199 -8
  194. package/dist/services/environments.js.map +1 -1
  195. package/dist/services/execution-allowlist.d.ts +16 -4
  196. package/dist/services/execution-allowlist.d.ts.map +1 -1
  197. package/dist/services/execution-allowlist.js +35 -16
  198. package/dist/services/execution-allowlist.js.map +1 -1
  199. package/dist/services/execution-allowlist.test.js +33 -0
  200. package/dist/services/execution-allowlist.test.js.map +1 -1
  201. package/dist/services/execution-workspace-policy.d.ts +14 -1
  202. package/dist/services/execution-workspace-policy.d.ts.map +1 -1
  203. package/dist/services/execution-workspace-policy.js +29 -12
  204. package/dist/services/execution-workspace-policy.js.map +1 -1
  205. package/dist/services/execution-workspaces.d.ts +89 -0
  206. package/dist/services/execution-workspaces.d.ts.map +1 -1
  207. package/dist/services/execution-workspaces.js +997 -125
  208. package/dist/services/execution-workspaces.js.map +1 -1
  209. package/dist/services/heartbeat-run-summary.d.ts +1 -0
  210. package/dist/services/heartbeat-run-summary.d.ts.map +1 -1
  211. package/dist/services/heartbeat-run-summary.js +19 -3
  212. package/dist/services/heartbeat-run-summary.js.map +1 -1
  213. package/dist/services/heartbeat.d.ts +18 -2
  214. package/dist/services/heartbeat.d.ts.map +1 -1
  215. package/dist/services/heartbeat.js +369 -16
  216. package/dist/services/heartbeat.js.map +1 -1
  217. package/dist/services/import-write-types.d.ts +12 -0
  218. package/dist/services/import-write-types.d.ts.map +1 -1
  219. package/dist/services/index.d.ts +2 -0
  220. package/dist/services/index.d.ts.map +1 -1
  221. package/dist/services/index.js +2 -0
  222. package/dist/services/index.js.map +1 -1
  223. package/dist/services/instance-settings.d.ts +10 -1
  224. package/dist/services/instance-settings.d.ts.map +1 -1
  225. package/dist/services/instance-settings.js +16 -9
  226. package/dist/services/instance-settings.js.map +1 -1
  227. package/dist/services/issue-review-policy.d.ts +4 -0
  228. package/dist/services/issue-review-policy.d.ts.map +1 -1
  229. package/dist/services/issue-review-policy.js +18 -7
  230. package/dist/services/issue-review-policy.js.map +1 -1
  231. package/dist/services/issue-thread-interaction-resolution.d.ts +92 -0
  232. package/dist/services/issue-thread-interaction-resolution.d.ts.map +1 -0
  233. package/dist/services/issue-thread-interaction-resolution.js +198 -0
  234. package/dist/services/issue-thread-interaction-resolution.js.map +1 -0
  235. package/dist/services/issue-thread-interaction-resolution.test.d.ts +2 -0
  236. package/dist/services/issue-thread-interaction-resolution.test.d.ts.map +1 -0
  237. package/dist/services/issue-thread-interaction-resolution.test.js +152 -0
  238. package/dist/services/issue-thread-interaction-resolution.test.js.map +1 -0
  239. package/dist/services/issue-thread-interactions.d.ts +24 -4
  240. package/dist/services/issue-thread-interactions.d.ts.map +1 -1
  241. package/dist/services/issue-thread-interactions.js +360 -145
  242. package/dist/services/issue-thread-interactions.js.map +1 -1
  243. package/dist/services/issue-thread-interactions.test.js +18 -11
  244. package/dist/services/issue-thread-interactions.test.js.map +1 -1
  245. package/dist/services/issues.d.ts +3 -0
  246. package/dist/services/issues.d.ts.map +1 -1
  247. package/dist/services/issues.js +157 -33
  248. package/dist/services/issues.js.map +1 -1
  249. package/dist/services/local-service-supervisor.d.ts +2 -0
  250. package/dist/services/local-service-supervisor.d.ts.map +1 -1
  251. package/dist/services/local-service-supervisor.js +53 -3
  252. package/dist/services/local-service-supervisor.js.map +1 -1
  253. package/dist/services/managed-environments.d.ts +4 -1
  254. package/dist/services/managed-environments.d.ts.map +1 -1
  255. package/dist/services/managed-environments.js +86 -2
  256. package/dist/services/managed-environments.js.map +1 -1
  257. package/dist/services/managed-environments.test.js +164 -0
  258. package/dist/services/managed-environments.test.js.map +1 -1
  259. package/dist/services/onboarding-greeting.d.ts +7 -0
  260. package/dist/services/onboarding-greeting.d.ts.map +1 -0
  261. package/dist/services/onboarding-greeting.js +27 -0
  262. package/dist/services/onboarding-greeting.js.map +1 -0
  263. package/dist/services/onboarding-greeting.test.d.ts +2 -0
  264. package/dist/services/onboarding-greeting.test.d.ts.map +1 -0
  265. package/dist/services/onboarding-greeting.test.js +32 -0
  266. package/dist/services/onboarding-greeting.test.js.map +1 -0
  267. package/dist/services/onboarding-seed.d.ts +57 -0
  268. package/dist/services/onboarding-seed.d.ts.map +1 -0
  269. package/dist/services/onboarding-seed.js +344 -0
  270. package/dist/services/onboarding-seed.js.map +1 -0
  271. package/dist/services/plan-review-context.d.ts +9 -1
  272. package/dist/services/plan-review-context.d.ts.map +1 -1
  273. package/dist/services/plan-review-context.js +192 -0
  274. package/dist/services/plan-review-context.js.map +1 -1
  275. package/dist/services/plugin-environment-driver.d.ts +46 -0
  276. package/dist/services/plugin-environment-driver.d.ts.map +1 -1
  277. package/dist/services/plugin-environment-driver.js +50 -0
  278. package/dist/services/plugin-environment-driver.js.map +1 -1
  279. package/dist/services/plugin-host-services.d.ts +1 -0
  280. package/dist/services/plugin-host-services.d.ts.map +1 -1
  281. package/dist/services/plugin-host-services.js +51 -28
  282. package/dist/services/plugin-host-services.js.map +1 -1
  283. package/dist/services/plugin-loader.d.ts +10 -1
  284. package/dist/services/plugin-loader.d.ts.map +1 -1
  285. package/dist/services/plugin-loader.js +43 -2
  286. package/dist/services/plugin-loader.js.map +1 -1
  287. package/dist/services/plugin-worker-manager.d.ts +61 -0
  288. package/dist/services/plugin-worker-manager.d.ts.map +1 -1
  289. package/dist/services/plugin-worker-manager.js +248 -1
  290. package/dist/services/plugin-worker-manager.js.map +1 -1
  291. package/dist/services/productivity-review.d.ts.map +1 -1
  292. package/dist/services/productivity-review.js +5 -0
  293. package/dist/services/productivity-review.js.map +1 -1
  294. package/dist/services/recovery/service.d.ts.map +1 -1
  295. package/dist/services/recovery/service.js +5 -1
  296. package/dist/services/recovery/service.js.map +1 -1
  297. package/dist/services/recovery/successful-run-handoff.d.ts +16 -1
  298. package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -1
  299. package/dist/services/recovery/successful-run-handoff.js +32 -0
  300. package/dist/services/recovery/successful-run-handoff.js.map +1 -1
  301. package/dist/services/recovery/successful-run-handoff.test.js +51 -1
  302. package/dist/services/recovery/successful-run-handoff.test.js.map +1 -1
  303. package/dist/services/routines.d.ts.map +1 -1
  304. package/dist/services/routines.js +48 -4
  305. package/dist/services/routines.js.map +1 -1
  306. package/dist/services/runtime-exposure/broker-client.d.ts +88 -0
  307. package/dist/services/runtime-exposure/broker-client.d.ts.map +1 -0
  308. package/dist/services/runtime-exposure/broker-client.js +217 -0
  309. package/dist/services/runtime-exposure/broker-client.js.map +1 -0
  310. package/dist/services/runtime-exposure/broker-client.test.d.ts +2 -0
  311. package/dist/services/runtime-exposure/broker-client.test.d.ts.map +1 -0
  312. package/dist/services/runtime-exposure/broker-client.test.js +185 -0
  313. package/dist/services/runtime-exposure/broker-client.test.js.map +1 -0
  314. package/dist/services/runtime-exposure/exposure-manager.d.ts +135 -0
  315. package/dist/services/runtime-exposure/exposure-manager.d.ts.map +1 -0
  316. package/dist/services/runtime-exposure/exposure-manager.js +247 -0
  317. package/dist/services/runtime-exposure/exposure-manager.js.map +1 -0
  318. package/dist/services/runtime-exposure/exposure-manager.test.d.ts +2 -0
  319. package/dist/services/runtime-exposure/exposure-manager.test.d.ts.map +1 -0
  320. package/dist/services/runtime-exposure/exposure-manager.test.js +384 -0
  321. package/dist/services/runtime-exposure/exposure-manager.test.js.map +1 -0
  322. package/dist/services/runtime-exposure/loopback-listener.d.ts +33 -0
  323. package/dist/services/runtime-exposure/loopback-listener.d.ts.map +1 -0
  324. package/dist/services/runtime-exposure/loopback-listener.js +146 -0
  325. package/dist/services/runtime-exposure/loopback-listener.js.map +1 -0
  326. package/dist/services/runtime-exposure/loopback-listener.test.d.ts +2 -0
  327. package/dist/services/runtime-exposure/loopback-listener.test.d.ts.map +1 -0
  328. package/dist/services/runtime-exposure/loopback-listener.test.js +116 -0
  329. package/dist/services/runtime-exposure/loopback-listener.test.js.map +1 -0
  330. package/dist/services/runtime-exposure/port-pair.d.ts +47 -0
  331. package/dist/services/runtime-exposure/port-pair.d.ts.map +1 -0
  332. package/dist/services/runtime-exposure/port-pair.js +57 -0
  333. package/dist/services/runtime-exposure/port-pair.js.map +1 -0
  334. package/dist/services/runtime-exposure/port-pair.test.d.ts +2 -0
  335. package/dist/services/runtime-exposure/port-pair.test.d.ts.map +1 -0
  336. package/dist/services/runtime-exposure/port-pair.test.js +92 -0
  337. package/dist/services/runtime-exposure/port-pair.test.js.map +1 -0
  338. package/dist/services/runtime-exposure/port-reservation.d.ts +211 -0
  339. package/dist/services/runtime-exposure/port-reservation.d.ts.map +1 -0
  340. package/dist/services/runtime-exposure/port-reservation.js +380 -0
  341. package/dist/services/runtime-exposure/port-reservation.js.map +1 -0
  342. package/dist/services/runtime-exposure/port-reservation.test.d.ts +2 -0
  343. package/dist/services/runtime-exposure/port-reservation.test.d.ts.map +1 -0
  344. package/dist/services/runtime-exposure/port-reservation.test.js +456 -0
  345. package/dist/services/runtime-exposure/port-reservation.test.js.map +1 -0
  346. package/dist/services/runtime-exposure/tailscale-hostname.d.ts +11 -0
  347. package/dist/services/runtime-exposure/tailscale-hostname.d.ts.map +1 -0
  348. package/dist/services/runtime-exposure/tailscale-hostname.js +36 -0
  349. package/dist/services/runtime-exposure/tailscale-hostname.js.map +1 -0
  350. package/dist/services/runtime-exposure/tailscale-hostname.test.d.ts +2 -0
  351. package/dist/services/runtime-exposure/tailscale-hostname.test.d.ts.map +1 -0
  352. package/dist/services/runtime-exposure/tailscale-hostname.test.js +14 -0
  353. package/dist/services/runtime-exposure/tailscale-hostname.test.js.map +1 -0
  354. package/dist/services/sandbox-orphan-cleanup-spool.d.ts +64 -0
  355. package/dist/services/sandbox-orphan-cleanup-spool.d.ts.map +1 -0
  356. package/dist/services/sandbox-orphan-cleanup-spool.js +134 -0
  357. package/dist/services/sandbox-orphan-cleanup-spool.js.map +1 -0
  358. package/dist/services/sandbox-provider-runtime.d.ts +15 -0
  359. package/dist/services/sandbox-provider-runtime.d.ts.map +1 -1
  360. package/dist/services/sandbox-provider-runtime.js +1 -0
  361. package/dist/services/sandbox-provider-runtime.js.map +1 -1
  362. package/dist/services/secrets.d.ts +103 -0
  363. package/dist/services/secrets.d.ts.map +1 -1
  364. package/dist/services/secrets.js +432 -10
  365. package/dist/services/secrets.js.map +1 -1
  366. package/dist/services/setup-token-session.d.ts +630 -0
  367. package/dist/services/setup-token-session.d.ts.map +1 -0
  368. package/dist/services/setup-token-session.js +1005 -0
  369. package/dist/services/setup-token-session.js.map +1 -0
  370. package/dist/services/setup-token-session.test.d.ts +2 -0
  371. package/dist/services/setup-token-session.test.d.ts.map +1 -0
  372. package/dist/services/setup-token-session.test.js +1143 -0
  373. package/dist/services/setup-token-session.test.js.map +1 -0
  374. package/dist/services/setup-token-transport-binding.d.ts +202 -0
  375. package/dist/services/setup-token-transport-binding.d.ts.map +1 -0
  376. package/dist/services/setup-token-transport-binding.js +386 -0
  377. package/dist/services/setup-token-transport-binding.js.map +1 -0
  378. package/dist/services/setup-token-transport-binding.test.d.ts +2 -0
  379. package/dist/services/setup-token-transport-binding.test.d.ts.map +1 -0
  380. package/dist/services/setup-token-transport-binding.test.js +761 -0
  381. package/dist/services/setup-token-transport-binding.test.js.map +1 -0
  382. package/dist/services/summary-slots.d.ts.map +1 -1
  383. package/dist/services/summary-slots.js +40 -6
  384. package/dist/services/summary-slots.js.map +1 -1
  385. package/dist/services/task-watchdogs.js +2 -2
  386. package/dist/services/task-watchdogs.js.map +1 -1
  387. package/dist/services/tool-access-policy.d.ts +1 -1
  388. package/dist/services/tool-access.d.ts.map +1 -1
  389. package/dist/services/tool-access.js +29 -11
  390. package/dist/services/tool-access.js.map +1 -1
  391. package/dist/services/tool-gateway.d.ts +1 -1
  392. package/dist/services/tool-gateway.d.ts.map +1 -1
  393. package/dist/services/tool-gateway.js +56 -5
  394. package/dist/services/tool-gateway.js.map +1 -1
  395. package/dist/services/workspace-file-resources.d.ts +10 -1
  396. package/dist/services/workspace-file-resources.d.ts.map +1 -1
  397. package/dist/services/workspace-file-resources.js +258 -6
  398. package/dist/services/workspace-file-resources.js.map +1 -1
  399. package/dist/services/workspace-git-operation-scheduler.d.ts +135 -0
  400. package/dist/services/workspace-git-operation-scheduler.d.ts.map +1 -0
  401. package/dist/services/workspace-git-operation-scheduler.js +655 -0
  402. package/dist/services/workspace-git-operation-scheduler.js.map +1 -0
  403. package/dist/services/workspace-git-operation-scheduler.test.d.ts +2 -0
  404. package/dist/services/workspace-git-operation-scheduler.test.d.ts.map +1 -0
  405. package/dist/services/workspace-git-operation-scheduler.test.js +367 -0
  406. package/dist/services/workspace-git-operation-scheduler.test.js.map +1 -0
  407. package/dist/services/workspace-operations.d.ts +60 -0
  408. package/dist/services/workspace-operations.d.ts.map +1 -1
  409. package/dist/services/workspace-operations.js +384 -19
  410. package/dist/services/workspace-operations.js.map +1 -1
  411. package/dist/services/workspace-runtime-exposure-backfill.test.d.ts +2 -0
  412. package/dist/services/workspace-runtime-exposure-backfill.test.d.ts.map +1 -0
  413. package/dist/services/workspace-runtime-exposure-backfill.test.js +149 -0
  414. package/dist/services/workspace-runtime-exposure-backfill.test.js.map +1 -0
  415. package/dist/services/workspace-runtime-exposure.test.d.ts +2 -0
  416. package/dist/services/workspace-runtime-exposure.test.d.ts.map +1 -0
  417. package/dist/services/workspace-runtime-exposure.test.js +517 -0
  418. package/dist/services/workspace-runtime-exposure.test.js.map +1 -0
  419. package/dist/services/workspace-runtime-leases.d.ts +90 -0
  420. package/dist/services/workspace-runtime-leases.d.ts.map +1 -0
  421. package/dist/services/workspace-runtime-leases.js +252 -0
  422. package/dist/services/workspace-runtime-leases.js.map +1 -0
  423. package/dist/services/workspace-runtime-read-model.d.ts +12 -0
  424. package/dist/services/workspace-runtime-read-model.d.ts.map +1 -1
  425. package/dist/services/workspace-runtime-read-model.test.js +3 -0
  426. package/dist/services/workspace-runtime-read-model.test.js.map +1 -1
  427. package/dist/services/workspace-runtime-ready-comment.test.js +1 -0
  428. package/dist/services/workspace-runtime-ready-comment.test.js.map +1 -1
  429. package/dist/services/workspace-runtime.d.ts +160 -2
  430. package/dist/services/workspace-runtime.d.ts.map +1 -1
  431. package/dist/services/workspace-runtime.js +1693 -141
  432. package/dist/services/workspace-runtime.js.map +1 -1
  433. package/dist/shutdown.d.ts +24 -0
  434. package/dist/shutdown.d.ts.map +1 -1
  435. package/dist/shutdown.js +37 -0
  436. package/dist/shutdown.js.map +1 -1
  437. package/dist/shutdown.test.js +113 -1
  438. package/dist/shutdown.test.js.map +1 -1
  439. package/dist/startup-banner.js +1 -1
  440. package/dist/startup-banner.js.map +1 -1
  441. package/dist/url-utils.d.ts +26 -0
  442. package/dist/url-utils.d.ts.map +1 -0
  443. package/dist/url-utils.js +59 -0
  444. package/dist/url-utils.js.map +1 -0
  445. package/dist/url-utils.test.d.ts +2 -0
  446. package/dist/url-utils.test.d.ts.map +1 -0
  447. package/dist/url-utils.test.js +55 -0
  448. package/dist/url-utils.test.js.map +1 -0
  449. package/dist/worktree-config.d.ts.map +1 -1
  450. package/dist/worktree-config.js +3 -17
  451. package/dist/worktree-config.js.map +1 -1
  452. package/package.json +20 -20
  453. package/skills/paperclip/SKILL.md +16 -11
  454. package/skills/paperclip/references/api-reference.md +8 -5
  455. package/skills/paperclip-board/SKILL.md +1 -1
  456. package/ui-dist/assets/{abnfDiagram-VRR7QNED-wvSnvD6-.js → abnfDiagram-N423BO3Z-BvgoV-u5.js} +1 -1
  457. package/ui-dist/assets/{arc-Bx0V1JsN.js → arc-BInnUBUU.js} +1 -1
  458. package/ui-dist/assets/architectureDiagram-T3A2C74G-Dg4MziOm.js +36 -0
  459. package/ui-dist/assets/{blockDiagram-677ZJIJ3-BROcLaUk.js → blockDiagram-VBNYF7ZC-D9kzAkLN.js} +1 -1
  460. package/ui-dist/assets/{c4Diagram-LMCZKHZV-D-V3kgju.js → c4Diagram-5PPSVZJV-BwzsgV_0.js} +1 -1
  461. package/ui-dist/assets/channel-qoSYbCn0.js +1 -0
  462. package/ui-dist/assets/{chunk-32BRIVSS-qillH3H6.js → chunk-2GRJ4B5K-D2EQ9vrl.js} +1 -1
  463. package/ui-dist/assets/{chunk-2Q5K7J3B-D19cVnii.js → chunk-2Q5K7J3B-DOA_8fSd.js} +1 -1
  464. package/ui-dist/assets/{chunk-EX3LRPZG-CLUswrZg.js → chunk-5RXB4S5H-CT5ukRiT.js} +1 -1
  465. package/ui-dist/assets/{chunk-5VM5RSS4-Cwlp02Lx.js → chunk-5VM5RSS4-D2gOlkyB.js} +1 -1
  466. package/ui-dist/assets/{chunk-MOJQB5TN-yKciyq6E.js → chunk-6Q2QTUOP-DUb6aG8c.js} +1 -1
  467. package/ui-dist/assets/{chunk-V7JOEXUC-DjJ75g0_.js → chunk-GF5L2VYU-BPabG3Cx.js} +1 -1
  468. package/ui-dist/assets/{chunk-JWPE2WC7-BjepyzEb.js → chunk-JWPE2WC7-scNfhlZz.js} +1 -1
  469. package/ui-dist/assets/{chunk-VR4S4FIN-CiEmy8-R.js → chunk-KBJHAD2P-BcRAzRpq.js} +1 -1
  470. package/ui-dist/assets/{chunk-RYQCIY6F-ed7mQM9j.js → chunk-RYQCIY6F-BiZ0-EGh.js} +1 -1
  471. package/ui-dist/assets/{chunk-XXDRQBXY-BiI83zoO.js → chunk-XXDRQBXY-lJULxFIE.js} +1 -1
  472. package/ui-dist/assets/classDiagram-JCYQIIEL-B24u1REs.js +1 -0
  473. package/ui-dist/assets/classDiagram-v2-OCEON4UE-B24u1REs.js +1 -0
  474. package/ui-dist/assets/{cose-bilkent-JH36ORCC-DIdQf5ta.js → cose-bilkent-JH36ORCC-Do8pe8JB.js} +1 -1
  475. package/ui-dist/assets/{cynefin-VYW2F7L2-B2m9xxh8.js → cynefin-VYW2F7L2-B34jJY1x.js} +1 -1
  476. package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-CGVdB0kX.js → cynefinDiagram-MW4NZA55-DQ8QK8JK.js} +1 -1
  477. package/ui-dist/assets/cytoscape.esm-DShqj__B.js +321 -0
  478. package/ui-dist/assets/{dagre-VKFMJZFB-CskKiZwJ.js → dagre-VZM6K2ZE-B7lSKgxH.js} +1 -1
  479. package/ui-dist/assets/{diagram-OA4YK3LP-D2cn5eIN.js → diagram-7IWD3JNH-B3hLAFGE.js} +1 -1
  480. package/ui-dist/assets/{diagram-FQU43EPY-oa5Zu0XG.js → diagram-B4RE2ZJO-CH9Ro9e0.js} +1 -1
  481. package/ui-dist/assets/{diagram-NH7WQ7WH-fymti5Js.js → diagram-LBJQPF4R-DdPVMii7.js} +1 -1
  482. package/ui-dist/assets/{diagram-G47NLZAW-BWF2ZCsw.js → diagram-Q27KOJAE-f5VXswjc.js} +1 -1
  483. package/ui-dist/assets/diagram-UB23O5K3-DGqxixsR.js +41 -0
  484. package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-CJ1d3D5q.js → ebnfDiagram-BXEA7PRR-C-1klg_w.js} +1 -1
  485. package/ui-dist/assets/{erDiagram-Q63AITRT-BJCPa7kK.js → erDiagram-JOGREHBK-Dt4qK4gv.js} +1 -1
  486. package/ui-dist/assets/{flowDiagram-23GEKE2U-BWjWuz6r.js → flowDiagram-UKHOOZJN-9ZlrTuET.js} +5 -5
  487. package/ui-dist/assets/{ganttDiagram-NO4QXBWP-Dv1KKjoM.js → ganttDiagram-PKOTCBZU-CE3lyvFq.js} +1 -1
  488. package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-9X7vgL0A.js → gitGraphDiagram-DS77QQ5N-HH2fSgad.js} +1 -1
  489. package/ui-dist/assets/{index-CpaK48ok.js → index-B61ORkIb.js} +1 -1
  490. package/ui-dist/assets/index-BHiUiToL.css +1 -0
  491. package/ui-dist/assets/{index-D2YCrA-D.js → index-BY5ITKmG.js} +1 -1
  492. package/ui-dist/assets/{index-9JMDmXc5.js → index-BbAhzCq8.js} +1 -1
  493. package/ui-dist/assets/{index-C5qHatpp.js → index-Bc9g6q63.js} +1 -1
  494. package/ui-dist/assets/{index-DuI6e5mP.js → index-Bf7tyGxE.js} +1 -1
  495. package/ui-dist/assets/{index-BahRsj0s.js → index-BvFx-4KW.js} +1 -1
  496. package/ui-dist/assets/{index-DKlsEDV7.js → index-C1mpWCcn.js} +1 -1
  497. package/ui-dist/assets/{index-DIXaXZrW.js → index-CHXG4GPL.js} +1 -1
  498. package/ui-dist/assets/{index-DcfO6NoY.js → index-CLu3Tfpv.js} +1 -1
  499. package/ui-dist/assets/{index-Cfz6P0Qq.js → index-CQhaM6qd.js} +1 -1
  500. package/ui-dist/assets/{index-D9d3sl6K.js → index-CVTL3aAL.js} +1 -1
  501. package/ui-dist/assets/{index-BexA3NAl.js → index-CcRTJ93J.js} +1 -1
  502. package/ui-dist/assets/{index-CnBrDwGT.js → index-Cm6iEYPt.js} +247 -223
  503. package/ui-dist/assets/{index-ByGcu-MB.js → index-DQIrrn2L.js} +1 -1
  504. package/ui-dist/assets/{index-DKEzHO7c.js → index-DU4U1fuv.js} +1 -1
  505. package/ui-dist/assets/{index-pqJO8FsV.js → index-DV2FOgyD.js} +1 -1
  506. package/ui-dist/assets/{index-tDuieU3u.js → index-DceuCrr8.js} +1 -1
  507. package/ui-dist/assets/{index-CjN2lm_w.js → index-DlVBBF8o.js} +1 -1
  508. package/ui-dist/assets/{index-0yJtUsFh.js → index-DsdVneqe.js} +1 -1
  509. package/ui-dist/assets/{index-C2a4_IKU.js → index-DvFUXI-Q.js} +1 -1
  510. package/ui-dist/assets/{index-BTMiDMYv.js → index-FHzrJ7iQ.js} +1 -1
  511. package/ui-dist/assets/{index-cUQXJtLK.js → index-diFs0CNC.js} +1 -1
  512. package/ui-dist/assets/{index-C96LHV1A.js → index-hkanj4t6.js} +1 -1
  513. package/ui-dist/assets/{index-rDI1Q4r9.js → index-yRJnh4vx.js} +1 -1
  514. package/ui-dist/assets/{infoDiagram-FWYZ7A6U-CHIhutSb.js → infoDiagram-6WML65LV-2jai2fw-.js} +1 -1
  515. package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-ChaD7-5_.js → ishikawaDiagram-WSZJBQD7-CUmGhmEb.js} +1 -1
  516. package/ui-dist/assets/{journeyDiagram-5HDEW3XC-M5T2sMgn.js → journeyDiagram-NVQOT4AX-CGQuPDO1.js} +1 -1
  517. package/ui-dist/assets/{kanban-definition-HUTT4EX6-B8H6R_Wc.js → kanban-definition-27J2QSJJ-zgEPcbHl.js} +1 -1
  518. package/ui-dist/assets/{linear-cpPKlVCq.js → linear-BVf5VtLS.js} +1 -1
  519. package/ui-dist/assets/mermaid.core-D2TFkGfI.js +308 -0
  520. package/ui-dist/assets/{mindmap-definition-LN4V7U3C-Bf6POzvm.js → mindmap-definition-FAOFIHXS-xoAQ0Lb_.js} +1 -1
  521. package/ui-dist/assets/{pegDiagram-2B236MQR-BJbGktAV.js → pegDiagram-VL7TDLO6-CBR1PWLr.js} +1 -1
  522. package/ui-dist/assets/{pieDiagram-ENE6RG2P-BmSN9Bio.js → pieDiagram-7S7Q4E2Y-BMPnzK_e.js} +1 -1
  523. package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-JUpqQCx_.js → quadrantDiagram-CIZ2JOQS-CPtVUPkR.js} +1 -1
  524. package/ui-dist/assets/{railroadDiagram-RFXS5EU6-B8k8T4-2.js → railroadDiagram-AXF67PYL-DC0lsZV1.js} +1 -1
  525. package/ui-dist/assets/{requirementDiagram-TGXJPOKE-b32heq7Q.js → requirementDiagram-LRYGKXZP-BF6k05SB.js} +1 -1
  526. package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-DC3s-xgf.js → sankeyDiagram-W5VNT64P-CRN0ibiP.js} +1 -1
  527. package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-BsCFyv_E.js → sequenceDiagram-SI44F4Z6-CQtTiPWm.js} +1 -1
  528. package/ui-dist/assets/{sizeCapture-X5ZJPWSS-S7xeCud3.js → sizeCapture-X5ZJPWSS-BdjuGZd1.js} +1 -1
  529. package/ui-dist/assets/{stateDiagram-2N3HPSRC-58O3ofoS.js → stateDiagram-OKZ733FA-B99onxyh.js} +1 -1
  530. package/ui-dist/assets/stateDiagram-v2-UEYNNEHI-bzFlYBZ1.js +1 -0
  531. package/ui-dist/assets/{swimlanes-5IMT3BWC-DFCVG0iF.js → swimlanes-SLNWSIFB-BIByPbrL.js} +2 -2
  532. package/ui-dist/assets/swimlanesDiagram-ULZ7WXOC-DgdkBnuK.js +8 -0
  533. package/ui-dist/assets/{timeline-definition-FHXFAJF6-C0c0SkQu.js → timeline-definition-Z64GVDOM-I1NgOcn9.js} +1 -1
  534. package/ui-dist/assets/{vennDiagram-L72KCM5P-B1cEx8GQ.js → vennDiagram-T6HMQDX7-D6t2Njcl.js} +1 -1
  535. package/ui-dist/assets/{wardleyDiagram-EHGQE667-BQhDkOx9.js → wardleyDiagram-T6FBY63Y-BiYKm9gh.js} +1 -1
  536. package/ui-dist/assets/{xychartDiagram-FW5EYKEG-YSQp1N0i.js → xychartDiagram-ELKLHX3M-BhwXKWIa.js} +4 -4
  537. package/ui-dist/index.html +3 -3
  538. package/ui-dist/sw.js +8 -3
  539. package/ui-dist/assets/architectureDiagram-ZJ3FMSHR-BcJt8Ia1.js +0 -36
  540. package/ui-dist/assets/channel-ZRpjyNiZ.js +0 -1
  541. package/ui-dist/assets/classDiagram-OUVF2IWQ-CCzzzCgk.js +0 -1
  542. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CCzzzCgk.js +0 -1
  543. package/ui-dist/assets/cytoscape.esm-D8joxN9f.js +0 -321
  544. package/ui-dist/assets/diagram-WEI45ONY-M3WOC2tK.js +0 -41
  545. package/ui-dist/assets/index-BqfqfoQX.css +0 -1
  546. package/ui-dist/assets/mermaid.core-MCazc_od.js +0 -308
  547. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-_VY5dpDG.js +0 -1
  548. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-BJhUjfns.js +0 -8
@@ -1,16 +1,187 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { and, eq, inArray } from "drizzle-orm";
3
3
  import { companySecrets, companySecretVersions, environmentLeases } from "@paperclipai/db";
4
+ import { resolveDeclaredSandboxCapabilities } from "@paperclipai/shared";
4
5
  import { ensureSshWorkspaceReady } from "@paperclipai/adapter-utils/ssh";
5
6
  import { getActiveStepContext, runWithRuntimeParent, } from "@paperclipai/adapter-utils/acpx-engine/startup-timing";
6
7
  import { environmentService } from "./environments.js";
7
- import { collectEnvironmentSecretRefs, parseEnvironmentDriverConfig, resolveEnvironmentDriverConfigForRuntime, stripSandboxProviderEnvelope, } from "./environment-config.js";
8
+ import { collectEnvironmentSecretRefs, parseEnvironmentDriverConfig, resolveEnvironmentDriverConfigForRuntime, resolveSandboxCleanupConfigSecrets, stripSandboxProviderEnvelope, } from "./environment-config.js";
8
9
  import { createEffectiveRunConfigFingerprints, } from "./effective-run-config-fingerprints.js";
9
10
  import { acquireSandboxProviderLease, destroySandboxProviderLease, findReusableSandboxProviderLeaseId, getSandboxProvider as getBuiltinSandboxProvider, isBuiltinSandboxProvider, releaseSandboxProviderLease, sandboxConfigFromLeaseMetadata, sandboxConfigFromLeaseMetadataLoose, } from "./sandbox-provider-runtime.js";
10
11
  import { pluginRegistryService } from "./plugin-registry.js";
11
- import { destroyPluginEnvironmentLease, executePluginEnvironmentCommand, realizePluginEnvironmentWorkspace, resolvePluginSandboxProviderDriverByKey, resolvePluginExecuteRpcTimeoutMs, resumePluginEnvironmentLease, } from "./plugin-environment-driver.js";
12
+ import { REUSABLE_LEASE_WORKER_METHODS, destroyPluginEnvironmentLease, executePluginEnvironmentCommand, realizePluginEnvironmentWorkspace, resolvePluginSandboxProviderDriverByKey, resolvePluginSandboxProviderDriverById, resolvePluginExecuteRpcTimeoutMs, resumePluginEnvironmentLease, } from "./plugin-environment-driver.js";
12
13
  import { collectSecretRefPaths } from "./json-schema-secret-refs.js";
13
14
  import { buildWorkspaceRealizationRecordFromDriverInput } from "./workspace-realization.js";
15
+ import { createSandboxOrphanCleanupSpool, } from "./sandbox-orphan-cleanup-spool.js";
16
+ import { logger } from "../middleware/logger.js";
17
+ // The constant error kind for the durable orphan-cleanup-write-failed log. The
18
+ // log never reads the caught exception, because the exception can carry a
19
+ // credential in its name, code, message, cause, or stack. So the log records
20
+ // this constant and the plain provider identifiers only.
21
+ const SANDBOX_ORPHAN_CLEANUP_WRITE_ERROR_KIND = "sandbox_orphan_cleanup_write_failed";
22
+ // ---------------------------------------------------------------------------
23
+ // Sandbox capability contract — one normalizer for both branches
24
+ // ---------------------------------------------------------------------------
25
+ export const SANDBOX_CAPABILITY_KEYS = [
26
+ "reusableLeases",
27
+ "nativeSyncIn",
28
+ "nativeSyncOut",
29
+ "persistentProcessSessions",
30
+ "independentControlCommands",
31
+ "incrementalSessionOutput",
32
+ ];
33
+ /**
34
+ * Opt-in capability keys. Most capabilities are a worker property: an absent
35
+ * declaration defers to the verified baseline and allows the capability. An
36
+ * opt-in capability is a behavioral guarantee that only some providers make, so
37
+ * an absent declaration denies it. A generic one-shot provider must not get an
38
+ * opt-in capability just because its worker verifies a shared verb such as
39
+ * `environmentExecute`. Only a provider that declares the key `true` (and still
40
+ * verifies the prerequisites and passes narrowing) gets the capability.
41
+ */
42
+ const SANDBOX_CAPABILITY_OPT_IN_KEYS = new Set([
43
+ "incrementalSessionOutput",
44
+ ]);
45
+ /**
46
+ * Verified prerequisite mapping: the worker methods each capability requires.
47
+ *
48
+ * Each capability maps to a list of requirement groups. A group holds one or
49
+ * more verbs; the group is met when the runtime verified AT LEAST ONE verb in
50
+ * it. A capability verifies only when EVERY group is met.
51
+ *
52
+ * The verbs are the worker method names from the worker discovery list (the
53
+ * plugin worker reports them from `handleInitialize`). A built-in provider maps
54
+ * its own methods to the same verb names through
55
+ * {@link builtinSandboxProviderVerifiedMethods}, so both branches share this
56
+ * mapping and the one normalizer below.
57
+ *
58
+ * The mapping was audited against the worker discovery list and the runtime
59
+ * execution guards:
60
+ * - `nativeSyncIn`/`nativeSyncOut` require the matching sync verb; the native
61
+ * sync guard checks both verbs before it routes a lease to the native hook.
62
+ * - `reusableLeases` requires `environmentResumeLease` (reattach),
63
+ * `environmentReleaseLease` (end-of-run release), and `environmentDestroyLease`
64
+ * (stale-lease teardown). All three run on the reuse path: the runtime resumes
65
+ * or releases the lease, and it destroys the stale lease when a resume fails
66
+ * before it acquires a fresh lease.
67
+ * - `persistentProcessSessions` and `independentControlCommands` require
68
+ * `environmentExecute`; both run commands through it.
69
+ * - `incrementalSessionOutput` requires `environmentExecute` too, because the
70
+ * provider tails the session log through it. The verified verb is necessary
71
+ * but not sufficient: this key is opt-in, so the declaration is the real gate
72
+ * (see {@link SANDBOX_CAPABILITY_OPT_IN_KEYS}).
73
+ */
74
+ const SANDBOX_CAPABILITY_PREREQUISITE_METHODS = {
75
+ // Reusable leases require ALL reuse verbs. Each verb is its own required
76
+ // group, so every one must be verified. The list function that publishes
77
+ // provider-level reusable support checks the same verbs; both read from
78
+ // `REUSABLE_LEASE_WORKER_METHODS`, so the runtime guard and the published
79
+ // value cannot drift.
80
+ reusableLeases: REUSABLE_LEASE_WORKER_METHODS.map((method) => [method]),
81
+ nativeSyncIn: [["environmentSyncIn"]],
82
+ nativeSyncOut: [["environmentSyncOut"]],
83
+ persistentProcessSessions: [["environmentExecute"]],
84
+ independentControlCommands: [["environmentExecute"]],
85
+ incrementalSessionOutput: [["environmentExecute"]],
86
+ };
87
+ function capabilityIsVerified(key, verifiedMethods) {
88
+ return SANDBOX_CAPABILITY_PREREQUISITE_METHODS[key].every((group) => group.some((verb) => verifiedMethods.has(verb)));
89
+ }
90
+ /**
91
+ * Map a built-in sandbox provider's own methods to the worker verb names the
92
+ * prerequisite mapping uses, so the built-in branch and the plug-in branch feed
93
+ * the SAME normalizer. A built-in provider has no native sync hooks, so it never
94
+ * verifies a sync verb. It verifies `environmentExecute` when it implements
95
+ * `execute`, and the reuse verbs only when it declares `supportsReusableLeases`.
96
+ * A built-in reusable provider destroys its own leases in-process, so it
97
+ * verifies `environmentDestroyLease` with the two reuse verbs.
98
+ */
99
+ export function builtinSandboxProviderVerifiedMethods(provider) {
100
+ if (!provider)
101
+ return [];
102
+ const methods = [];
103
+ if (typeof provider.execute === "function") {
104
+ methods.push("environmentExecute");
105
+ }
106
+ if (provider.supportsReusableLeases === true) {
107
+ methods.push("environmentResumeLease", "environmentReleaseLease", "environmentDestroyLease");
108
+ }
109
+ return methods;
110
+ }
111
+ /**
112
+ * The one normalizer for the sandbox capability contract. It resolves the
113
+ * effective capability as verified ∩ declared ∩ narrowing.
114
+ *
115
+ * - `verifiedMethods` is the runtime's verified worker verb list (the plug-in
116
+ * worker's `supportedMethods`, or a built-in provider mapped through
117
+ * {@link builtinSandboxProviderVerifiedMethods}). A missing or empty list
118
+ * verifies nothing, so every capability resolves `false` (fail closed).
119
+ * - `declared` is the provider's declaration. An absent flag defers to the
120
+ * verified discovery baseline; it never grants a capability. A present flag
121
+ * can only remove a verified capability, never add one.
122
+ * - `narrowing` is the per-target restriction from the config or lease. An
123
+ * absent key applies no restriction; a `false` value removes the capability.
124
+ *
125
+ * A declaration never grants a capability the runtime did not verify, so a
126
+ * declared capability whose worker lacks a prerequisite verb resolves `false`.
127
+ */
128
+ export function resolveEffectiveSandboxCapabilities(input) {
129
+ const verifiedMethods = new Set(input.verifiedMethods ?? []);
130
+ const declared = input.declared ?? {};
131
+ const narrowing = input.narrowing ?? {};
132
+ const resolve = (key) => {
133
+ const verified = capabilityIsVerified(key, verifiedMethods);
134
+ // An absent declaration defers to the verified baseline for a worker-property
135
+ // capability (true = no extra restriction), but denies an opt-in capability
136
+ // (false). A present declaration can only narrow.
137
+ const declaredDefault = SANDBOX_CAPABILITY_OPT_IN_KEYS.has(key) ? false : true;
138
+ const declaredAllows = declared[key] ?? declaredDefault;
139
+ // An absent narrowing applies no restriction.
140
+ const narrowingAllows = narrowing[key] ?? true;
141
+ return verified && declaredAllows && narrowingAllows;
142
+ };
143
+ return {
144
+ reusableLeases: resolve("reusableLeases"),
145
+ nativeSyncIn: resolve("nativeSyncIn"),
146
+ nativeSyncOut: resolve("nativeSyncOut"),
147
+ persistentProcessSessions: resolve("persistentProcessSessions"),
148
+ independentControlCommands: resolve("independentControlCommands"),
149
+ incrementalSessionOutput: resolve("incrementalSessionOutput"),
150
+ };
151
+ }
152
+ /**
153
+ * Build the per-target narrowing for a sandbox lease. Narrowing removes a
154
+ * capability that the provider verified and declared but that this specific
155
+ * lease or config cannot use. Each source is grounded in existing runtime
156
+ * behavior:
157
+ * - `reusableLeases` follows this lease's resolved policy (an ephemeral lease
158
+ * never reuses).
159
+ * - a Kubernetes Job lease disables native sync (mirrors the native sync guard,
160
+ * which falls back for a `job` backend or a `nativeFileSyncUnsupported` lease).
161
+ * - `configResolutionFailed` marks that the runtime could not resolve the
162
+ * provider config. A provider whose config cannot be resolved is untrusted, so
163
+ * the runtime fails closed and narrows `persistentProcessSessions` and
164
+ * `incrementalSessionOutput` to false. An empty config alone does not fail
165
+ * closed; only a resolution error does.
166
+ */
167
+ export function buildSandboxCapabilityNarrowing(input) {
168
+ const narrowing = {};
169
+ const metadata = input.leaseMetadata ?? {};
170
+ narrowing.reusableLeases = input.leasePolicy === "reuse_by_environment";
171
+ if (metadata.backend === "job" || metadata.nativeFileSyncUnsupported === true) {
172
+ narrowing.nativeSyncIn = false;
173
+ narrowing.nativeSyncOut = false;
174
+ }
175
+ if (input.configResolutionFailed === true) {
176
+ // The runtime could not resolve the provider config, so it fails closed and
177
+ // denies persistent process sessions and incremental session output. The
178
+ // session-output streaming gate reads `incrementalSessionOutput`, so it must
179
+ // narrow with the persistent-session gate to keep the fail-closed behavior.
180
+ narrowing.persistentProcessSessions = false;
181
+ narrowing.incrementalSessionOutput = false;
182
+ }
183
+ return narrowing;
184
+ }
14
185
  export function buildEnvironmentLeaseContext(input) {
15
186
  return {
16
187
  executionWorkspaceId: input.persistedExecutionWorkspace?.id ?? null,
@@ -70,14 +241,51 @@ function resolvePluginSandboxRpcTimeoutMs(config) {
70
241
  config,
71
242
  });
72
243
  }
244
+ /**
245
+ * The acquire provisioned a remote sandbox, the lease insert rejected it, the
246
+ * compensating teardown failed, and every retry of the durable pending-cleanup
247
+ * write also failed. A live sandbox now has no lease row and no cleanup record,
248
+ * so no sweep can find it. The acquire throws this error so the failure stays visible
249
+ * and never resolves to a clean rejection. The `cause` field holds the original
250
+ * lease insert rejection. The `cleanupWriteError` field holds the write failure.
251
+ */
252
+ export class SandboxOrphanCleanupWriteError extends Error {
253
+ provider;
254
+ providerLeaseId;
255
+ cleanupWriteError;
256
+ constructor(input) {
257
+ super(`Sandbox provider "${input.provider}" leaked lease ` +
258
+ `"${input.providerLeaseId ?? "unknown"}": the acquire could not tear it down ` +
259
+ "and could not record a durable pending-cleanup row. The live sandbox has no " +
260
+ "durable cleanup state and needs a manual teardown.", { cause: input.cause });
261
+ this.name = "SandboxOrphanCleanupWriteError";
262
+ this.provider = input.provider;
263
+ this.providerLeaseId = input.providerLeaseId;
264
+ this.cleanupWriteError = input.cleanupWriteError;
265
+ }
266
+ }
73
267
  const DEFAULT_PLUGIN_SANDBOX_WORKER_READY_TIMEOUT_MS = 5_000;
74
268
  const DEFAULT_PLUGIN_SANDBOX_WORKER_READY_POLL_MS = 100;
269
+ // The durable pending-cleanup write is the only automated way to find a leaked
270
+ // sandbox. A single insert can lose to a transient database fault (a dropped
271
+ // connection, a lock timeout, a serialization conflict). So the acquire retries
272
+ // the write a few times with a short backoff before it gives up. A later
273
+ // attempt can still land the durable row that a sweep finds.
274
+ const DEFAULT_SANDBOX_ORPHAN_CLEANUP_WRITE_ATTEMPTS = 3;
275
+ const DEFAULT_SANDBOX_ORPHAN_CLEANUP_WRITE_BACKOFF_MS = 100;
276
+ // A prolonged database outage after a failed teardown can buffer many orphan
277
+ // records in-process. The bound stops that buffer from growing without limit. A
278
+ // full buffer keeps the error log as the last durable handle for a new orphan.
279
+ const DEFAULT_DEFERRED_ORPHAN_CLEANUP_BUFFER_LIMIT = 256;
75
280
  function delay(ms) {
76
281
  return new Promise((resolve) => setTimeout(resolve, ms));
77
282
  }
78
283
  function getLeaseDriverKey(lease, environment) {
79
284
  const leaseDriver = typeof lease.metadata?.driver === "string" ? lease.metadata.driver : null;
80
- return leaseDriver ?? environment.driver;
285
+ // An orphan `pending_cleanup` row whose environment a delete removed keeps its
286
+ // driver in the metadata, so the sweep still finds the sandbox driver. Fall
287
+ // back to "sandbox" because only sandbox leases record orphan cleanup.
288
+ return leaseDriver ?? environment?.driver ?? "sandbox";
81
289
  }
82
290
  function toEnvironmentLeaseSnapshot(row) {
83
291
  return {
@@ -391,7 +599,279 @@ function createSandboxEnvironmentDriver(db, options = {}) {
391
599
  const pluginWorkerManager = options.pluginWorkerManager;
392
600
  const pluginWorkerReadyTimeoutMs = options.pluginWorkerReadyTimeoutMs ?? DEFAULT_PLUGIN_SANDBOX_WORKER_READY_TIMEOUT_MS;
393
601
  const pluginWorkerReadyPollMs = options.pluginWorkerReadyPollMs ?? DEFAULT_PLUGIN_SANDBOX_WORKER_READY_POLL_MS;
602
+ // The retry count is at least one attempt, so a zero or negative override
603
+ // never skips the durable write. The backoff is at least zero milliseconds.
604
+ const pendingCleanupWriteAttempts = Math.max(1, options.pendingCleanupWriteAttempts ?? DEFAULT_SANDBOX_ORPHAN_CLEANUP_WRITE_ATTEMPTS);
605
+ const pendingCleanupWriteBackoffMs = Math.max(0, options.pendingCleanupWriteBackoffMs ?? DEFAULT_SANDBOX_ORPHAN_CLEANUP_WRITE_BACKOFF_MS);
606
+ // The buffer holds at least one orphan, so a zero or negative override never
607
+ // drops every buffered record.
608
+ const deferredOrphanCleanupBufferLimit = Math.max(1, options.deferredOrphanCleanupBufferLimit ?? DEFAULT_DEFERRED_ORPHAN_CLEANUP_BUFFER_LIMIT);
394
609
  const environmentsSvc = environmentService(db);
610
+ // A live sandbox whose teardown failed needs a durable `pending_cleanup` row,
611
+ // so a sweep can find and release it. When every synchronous write attempt
612
+ // fails, the database is down but the process still runs. So the driver keeps
613
+ // the orphan record in this in-process buffer, and a later cleanup sweep
614
+ // flushes it back to the database. The buffer closes the common window where
615
+ // the database recovers after the synchronous attempts but while the process
616
+ // still runs. The `pending_cleanup` failure reason matches the synchronous
617
+ // write, so a flushed row and a synchronous row are indistinguishable to the
618
+ // sweep.
619
+ const deferredOrphanCleanups = [];
620
+ const DEFERRED_ORPHAN_CLEANUP_FAILURE_REASON = "acquire_rejected_teardown_failed";
621
+ // The in-process buffer alone loses an orphan on a restart. So the driver also
622
+ // persists each buffered orphan to this durable local spool. The spool keeps
623
+ // the record on the local disk, next to the other durable server state, so a
624
+ // restart still finds it. The first flush after a restart loads the spool back
625
+ // into the buffer, so the sweep re-inserts the durable row. The spool holds no
626
+ // secret value: it persists the same sanitized metadata the database row
627
+ // holds, so a restart-recovered record still authorizes the teardown but never
628
+ // exposes a credential. Only a crash during the outage before the durable
629
+ // spool write lands loses the record; the error log stays the last handle for
630
+ // that narrow case.
631
+ const orphanCleanupSpool = options.orphanCleanupSpool ?? createSandboxOrphanCleanupSpool(options.orphanCleanupSpoolDir);
632
+ // Add one orphan record to the in-process buffer. Report `false` only when the
633
+ // buffer is full, so the caller keeps the error log as the last durable handle.
634
+ const enqueueDeferredOrphanCleanup = (record) => {
635
+ // Dedup by the provider lease id, so a repeated failure for the same orphan
636
+ // never buffers it twice. Each acquire mints a unique provider lease id, so
637
+ // two distinct orphans never collide. Skip the dedup for a null lease id,
638
+ // because a null value carries no identity.
639
+ const alreadyBuffered = record.providerLeaseId !== null &&
640
+ deferredOrphanCleanups.some((entry) => entry.providerLeaseId === record.providerLeaseId);
641
+ if (alreadyBuffered)
642
+ return true;
643
+ if (deferredOrphanCleanups.length >= deferredOrphanCleanupBufferLimit)
644
+ return false;
645
+ deferredOrphanCleanups.push(record);
646
+ return true;
647
+ };
648
+ // Load the durable spool into the in-process buffer exactly once. The first
649
+ // flush after a restart runs this load, so the records a previous process
650
+ // could not land re-enter the flush path. A later flush sees the resolved
651
+ // promise and never reloads, so a record this process already buffered never
652
+ // doubles. The load runs before the flush splices the buffer, so a
653
+ // restart-recovered record flushes on the same tick.
654
+ let spoolLoadPromise = null;
655
+ const ensureSpoolLoaded = () => {
656
+ if (!spoolLoadPromise) {
657
+ spoolLoadPromise = (async () => {
658
+ const persisted = await orphanCleanupSpool.load();
659
+ for (const record of persisted) {
660
+ enqueueDeferredOrphanCleanup(record);
661
+ }
662
+ })();
663
+ }
664
+ return spoolLoadPromise;
665
+ };
666
+ const flushDeferredOrphanCleanups = async () => {
667
+ // Load the durable spool into the buffer first, so a restart-recovered record
668
+ // flushes on this tick. The load runs once, then a resolved promise returns.
669
+ await ensureSpoolLoaded();
670
+ if (deferredOrphanCleanups.length === 0) {
671
+ return { recovered: 0, pending: 0 };
672
+ }
673
+ // Take the whole batch synchronously before the first await, so a second
674
+ // concurrent flush sees an empty buffer and never re-inserts the same record.
675
+ const batch = deferredOrphanCleanups.splice(0, deferredOrphanCleanups.length);
676
+ let recovered = 0;
677
+ for (const record of batch) {
678
+ try {
679
+ await environmentsSvc.insertPendingCleanupLease({
680
+ companyId: record.companyId,
681
+ environmentId: record.environmentId,
682
+ executionWorkspaceId: record.executionWorkspaceId,
683
+ issueId: record.issueId,
684
+ heartbeatRunId: record.heartbeatRunId,
685
+ provider: record.provider,
686
+ providerLeaseId: record.providerLeaseId,
687
+ metadata: record.metadata,
688
+ failureReason: DEFERRED_ORPHAN_CLEANUP_FAILURE_REASON,
689
+ });
690
+ // The durable row exists now, so the sweep finds and releases the orphan.
691
+ // Drop the spooled copy only after the row lands. A crash between the two
692
+ // leaves the spooled copy for a later flush, which re-inserts a duplicate
693
+ // the idempotent teardown handles. This order never loses the orphan.
694
+ await orphanCleanupSpool.remove(record);
695
+ recovered += 1;
696
+ }
697
+ catch {
698
+ // The database is still down. Re-queue the record for a later flush,
699
+ // unless the buffer filled again. The identifiers carry no secret; the
700
+ // caught write exception never enters the log, because it can hold a
701
+ // credential in its message, code, cause, or stack.
702
+ const requeued = enqueueDeferredOrphanCleanup(record);
703
+ logger.warn({
704
+ errorKind: SANDBOX_ORPHAN_CLEANUP_WRITE_ERROR_KIND,
705
+ provider: record.provider,
706
+ providerLeaseId: record.providerLeaseId,
707
+ companyId: record.companyId,
708
+ environmentId: record.environmentId,
709
+ requeued,
710
+ }, requeued
711
+ ? "deferred sandbox orphan cleanup flush failed; kept the orphan buffered for a later flush"
712
+ : "deferred sandbox orphan cleanup flush failed and the in-process buffer is full; live sandbox needs a manual teardown");
713
+ }
714
+ }
715
+ return { recovered, pending: deferredOrphanCleanups.length };
716
+ };
717
+ // Build the safe diagnostic fields for an orphan record. The provider
718
+ // identifiers are the only fields any diagnostic log emits, and they carry no
719
+ // secret. A caught write exception never enters a log: it can carry a
720
+ // credential in its name, code, message, cause, or stack.
721
+ const orphanDiagnosticFields = (record) => ({
722
+ errorKind: SANDBOX_ORPHAN_CLEANUP_WRITE_ERROR_KIND,
723
+ provider: record.provider,
724
+ providerLeaseId: record.providerLeaseId,
725
+ companyId: record.companyId,
726
+ environmentId: record.environmentId,
727
+ executionWorkspaceId: record.executionWorkspaceId,
728
+ issueId: record.issueId,
729
+ heartbeatRunId: record.heartbeatRunId,
730
+ });
731
+ // Try to write the durable `pending_cleanup` row for an orphan sandbox, with a
732
+ // few retries. Report the new lease id on success, or a null lease id and the
733
+ // last write error on total failure. This function never buffers, never logs
734
+ // at error level, and never throws. The caller decides whether a total failure
735
+ // is fatal, because a later successful teardown removes the orphan and needs
736
+ // no durable row.
737
+ //
738
+ // The row carries the provider lease id and the same resolution metadata a
739
+ // normal lease holds, so a later cleanup sweep finds and releases the orphan.
740
+ // One atomic insert writes the row directly in the terminal `pending_cleanup`
741
+ // state, and the insert skips the company-binding assertion, so it records the
742
+ // orphan even for a foreign-bound environment. A transient database fault (a
743
+ // dropped connection, a lock timeout, a serialization conflict) can reject one
744
+ // insert but clear on the next, so the write retries a few times with a short
745
+ // backoff.
746
+ const tryWriteDurablePendingCleanup = async (record) => {
747
+ const diagnosticFields = orphanDiagnosticFields(record);
748
+ let lastCleanupWriteError;
749
+ for (let attempt = 1; attempt <= pendingCleanupWriteAttempts; attempt += 1) {
750
+ try {
751
+ const lease = await environmentsSvc.insertPendingCleanupLease({
752
+ companyId: record.companyId,
753
+ environmentId: record.environmentId,
754
+ executionWorkspaceId: record.executionWorkspaceId,
755
+ issueId: record.issueId,
756
+ heartbeatRunId: record.heartbeatRunId,
757
+ provider: record.provider,
758
+ providerLeaseId: record.providerLeaseId,
759
+ metadata: record.metadata,
760
+ failureReason: DEFERRED_ORPHAN_CLEANUP_FAILURE_REASON,
761
+ });
762
+ // The durable row exists now, so a sweep can find and release the orphan.
763
+ return { leaseId: lease.id, lastError: undefined };
764
+ }
765
+ catch (cleanupWriteError) {
766
+ lastCleanupWriteError = cleanupWriteError;
767
+ if (attempt < pendingCleanupWriteAttempts) {
768
+ // The write failed, but attempts remain. Log the retry at warn level
769
+ // and back off, then try again. A later attempt can still land the
770
+ // durable row.
771
+ logger.warn({ ...diagnosticFields, attempt, maxAttempts: pendingCleanupWriteAttempts }, "sandbox orphan cleanup write failed; retrying the durable pending-cleanup write");
772
+ await delay(pendingCleanupWriteBackoffMs * attempt);
773
+ }
774
+ }
775
+ }
776
+ return { leaseId: null, lastError: lastCleanupWriteError };
777
+ };
778
+ // Release the durable `pending_cleanup` row after a successful inline teardown.
779
+ // The teardown removed the orphan sandbox, so the row is no longer needed. The
780
+ // release moves the row to the terminal `expired` state with a `success`
781
+ // cleanup status, the same state the sweep records for a successful teardown.
782
+ //
783
+ // The release is best-effort. If it fails, the row stays `pending_cleanup`, so
784
+ // a later sweep runs the idempotent teardown on the already-gone sandbox and
785
+ // releases the row itself. A failed release never loses or leaks the orphan.
786
+ const releaseCleanedUpOrphanRow = async (leaseId, diagnosticFields) => {
787
+ try {
788
+ await environmentsSvc.releaseLease(leaseId, "expired", {
789
+ cleanupStatus: "success",
790
+ failureReason: "acquire_rejected_teardown_succeeded",
791
+ });
792
+ }
793
+ catch {
794
+ // The caught release exception never enters a log: it can carry a
795
+ // credential in its name, code, message, cause, or stack.
796
+ logger.warn(diagnosticFields, "could not release the pending-cleanup row after a successful orphan teardown; a later sweep reconciles the released row");
797
+ }
798
+ };
799
+ // Neither the durable write nor the inline teardown worked, so the orphan
800
+ // sandbox is live with no durable row. Persist the orphan to the durable spool
801
+ // and buffer it in-process, so a later cleanup-sweep flush lands the durable
802
+ // row. The spool survives a restart, so the flush recovers the orphan even
803
+ // after a crash. Then log the plain provider identifiers at error level, so an
804
+ // operator keeps a handle to tear the sandbox down by hand. Only a crash before
805
+ // the spool write lands loses the record, so the error log is the last handle
806
+ // for that narrow case. Throw `SandboxOrphanCleanupWriteError`, which the
807
+ // caller propagates. The error keeps the original insert rejection as its
808
+ // `cause`, so the real reason the acquire failed stays visible in the error
809
+ // chain.
810
+ const escalateUnwrittenOrphan = async (record, cause, cleanupWriteError) => {
811
+ const persisted = await orphanCleanupSpool.append(record);
812
+ const buffered = enqueueDeferredOrphanCleanup(record);
813
+ logger.error({
814
+ ...orphanDiagnosticFields(record),
815
+ persisted,
816
+ buffered,
817
+ deferredBufferSize: deferredOrphanCleanups.length,
818
+ }, persisted
819
+ ? "sandbox orphan cleanup write failed; persisted the orphan to the durable spool for a later cleanup-sweep flush"
820
+ : buffered
821
+ ? "sandbox orphan cleanup write failed and the durable spool write failed; buffered the orphan in-process only for a later cleanup-sweep flush"
822
+ : "sandbox orphan cleanup write failed and neither the durable spool nor the in-process buffer kept the orphan; live sandbox needs a manual teardown");
823
+ throw new SandboxOrphanCleanupWriteError({
824
+ provider: record.provider,
825
+ providerLeaseId: record.providerLeaseId,
826
+ cause,
827
+ cleanupWriteError,
828
+ });
829
+ };
830
+ // Clean up an orphan sandbox after the conditional lease insert rejected it.
831
+ // The acquire provisioned a remote sandbox, then the insert rejected (a
832
+ // foreign-company binding), so no lease row tracks the live sandbox. This
833
+ // handler records the durable `pending_cleanup` row FIRST, before the inline
834
+ // teardown, then tears the sandbox down.
835
+ //
836
+ // The write goes first on purpose. The conditional insert just returned a
837
+ // definitive rejection, so the database is reachable at this instant. An
838
+ // inline teardown can run long, and the database can drop during it. So a
839
+ // write after the teardown can fail and leave the orphan only in memory, which
840
+ // a restart loses. With the write first, the durable row lands while the
841
+ // database is proven up, so a crash after it still leaves a row that a sweep
842
+ // finds and releases.
843
+ //
844
+ // The teardown then runs. On success the orphan is gone, so the handler
845
+ // releases the durable row. On a failed teardown the row stays for the sweep.
846
+ // If the durable write itself failed and the teardown also failed, the handler
847
+ // buffers the orphan and throws `SandboxOrphanCleanupWriteError`. A successful
848
+ // teardown after a failed write leaves no orphan, so it needs no error.
849
+ const cleanUpRejectedOrphanSandbox = async (input) => {
850
+ const durable = await tryWriteDurablePendingCleanup(input.record);
851
+ let teardownFailed = !input.canTeardown;
852
+ if (!teardownFailed) {
853
+ try {
854
+ await input.teardown();
855
+ }
856
+ catch {
857
+ teardownFailed = true;
858
+ }
859
+ }
860
+ if (!teardownFailed) {
861
+ // The teardown removed the orphan, so drop the durable row if we wrote one.
862
+ if (durable.leaseId !== null) {
863
+ await releaseCleanedUpOrphanRow(durable.leaseId, orphanDiagnosticFields(input.record));
864
+ }
865
+ return;
866
+ }
867
+ // The teardown failed, so the orphan sandbox is still live.
868
+ if (durable.leaseId !== null) {
869
+ // The durable row already tracks the orphan, so a sweep finds and releases
870
+ // it. The caller rethrows the original insert rejection.
871
+ return;
872
+ }
873
+ await escalateUnwrittenOrphan(input.record, input.cause, durable.lastError);
874
+ };
395
875
  // The run-time exec parent context, held per lease id. A plugin sandbox
396
876
  // provider can open a persistent session on the first command and delete it
397
877
  // on lease release. The session open runs inside `execute`, under the run
@@ -464,14 +944,14 @@ function createSandboxEnvironmentDriver(db, options = {}) {
464
944
  config: sandboxConfigForLeaseMetadata(metadataConfig),
465
945
  });
466
946
  if (parsed.driver === "sandbox") {
467
- return parsed.config;
947
+ return dropInternalPluginSandboxConfigKeys(parsed.config);
468
948
  }
469
949
  }
470
950
  if (input.environment.driver === "sandbox") {
471
951
  try {
472
952
  const parsed = await resolveEnvironmentDriverConfigForRuntime(db, input.lease.companyId, input.environment);
473
953
  if (parsed.driver === "sandbox" && parsed.config.provider === input.provider) {
474
- return parsed.config;
954
+ return dropInternalPluginSandboxConfigKeys(parsed.config);
475
955
  }
476
956
  }
477
957
  catch {
@@ -481,7 +961,7 @@ function createSandboxEnvironmentDriver(db, options = {}) {
481
961
  }
482
962
  return {
483
963
  provider: input.provider,
484
- ...sanitizePluginSandboxConfigFromLeaseMetadata(input.lease.metadata),
964
+ ...dropInternalPluginSandboxConfigKeys(input.lease.metadata),
485
965
  };
486
966
  }
487
967
  async function cleanupObsoleteReusableSandboxLeases(input) {
@@ -559,7 +1039,20 @@ function createSandboxEnvironmentDriver(db, options = {}) {
559
1039
  const workerConfig = stripSandboxProviderEnvelope(parsed.config);
560
1040
  const storedConfig = storedParsed.config;
561
1041
  const providerConfigForLease = sandboxConfigForLeaseMetadata(storedConfig);
562
- const supportsReusableLeases = pluginProvider.resolved.driver.supportsReusableLeases === true;
1042
+ // Require the reusable-lease capability AND a worker that verifies the
1043
+ // reuse methods. The provider must first opt in through the declaration:
1044
+ // the nested `sandboxCapabilities.reusableLeases` wins over the legacy
1045
+ // `supportsReusableLeases` flag. The worker must also verify
1046
+ // `environmentResumeLease`, `environmentReleaseLease`, and
1047
+ // `environmentDestroyLease` (the reuse prerequisite verbs) before the
1048
+ // runtime resumes, releases, or tears down a reusable lease. A provider
1049
+ // that declares `reusableLeases` true but whose worker does not verify
1050
+ // all three methods fails closed and uses an ephemeral lease, so the
1051
+ // runtime never dispatches a resume, a release, or a destroy the worker
1052
+ // cannot serve, and it never strands a stale lease it cannot destroy.
1053
+ const declaredReusableLeases = resolveDeclaredSandboxCapabilities(pluginProvider.resolved.driver).reusableLeases === true;
1054
+ const pluginVerifiedMethods = new Set(pluginWorkerManager.getWorker(pluginProvider.resolved.plugin.id)?.supportedMethods ?? []);
1055
+ const supportsReusableLeases = declaredReusableLeases && capabilityIsVerified("reusableLeases", pluginVerifiedMethods);
563
1056
  const leaseFingerprint = supportsReusableLeases &&
564
1057
  parsed.config.reuseLease &&
565
1058
  input.heartbeatRunId !== null &&
@@ -632,29 +1125,41 @@ function createSandboxEnvironmentDriver(db, options = {}) {
632
1125
  : null;
633
1126
  let providerLease = null;
634
1127
  if (reusableLease?.providerLeaseId) {
635
- try {
636
- const resumed = await pluginWorkerManager.call(pluginProvider.resolved.plugin.id, "environmentResumeLease", {
637
- driverKey: parsed.config.provider,
638
- companyId: input.companyId,
639
- environmentId: input.environment.id,
640
- issueId: input.issueId,
641
- config: workerConfig,
642
- providerLeaseId: reusableLease.providerLeaseId,
643
- leaseMetadata: reusableLease.metadata ?? undefined,
644
- }, resolvePluginSandboxRpcTimeoutMs(workerConfig));
645
- providerLease =
646
- typeof resumed.providerLeaseId === "string" && resumed.providerLeaseId.length > 0
647
- ? resumed
648
- : null;
649
- }
650
- catch {
651
- providerLease = null;
1128
+ // The `supportsReusableLeases` check above reads a snapshot of the
1129
+ // worker methods. The runtime then does asynchronous database work
1130
+ // (list, fingerprint, obsolete-lease cleanup) before this dispatch. A
1131
+ // worker restart in that window can drop `environmentResumeLease`
1132
+ // while the snapshot still marks the method verified. Re-check the
1133
+ // live worker here and fail closed when the method is absent: skip the
1134
+ // resume, destroy the stale reusable lease, and acquire a fresh lease
1135
+ // below. The runtime never dispatches a resume the live worker cannot
1136
+ // serve.
1137
+ const workerVerifiesResume = pluginWorkerVerifiesLifecycleMethod(pluginProvider.resolved.plugin.id, "environmentResumeLease");
1138
+ if (workerVerifiesResume) {
1139
+ try {
1140
+ const resumed = await pluginWorkerManager.call(pluginProvider.resolved.plugin.id, "environmentResumeLease", {
1141
+ driverKey: parsed.config.provider,
1142
+ companyId: input.companyId,
1143
+ environmentId: input.environment.id,
1144
+ issueId: input.issueId,
1145
+ config: workerConfig,
1146
+ providerLeaseId: reusableLease.providerLeaseId,
1147
+ leaseMetadata: reusableLease.metadata ?? undefined,
1148
+ }, resolvePluginSandboxRpcTimeoutMs(workerConfig));
1149
+ providerLease =
1150
+ typeof resumed.providerLeaseId === "string" && resumed.providerLeaseId.length > 0
1151
+ ? resumed
1152
+ : null;
1153
+ }
1154
+ catch {
1155
+ providerLease = null;
1156
+ }
652
1157
  }
653
1158
  if (!providerLease) {
654
1159
  await destroyReusableSandboxLease({
655
1160
  environment: input.environment,
656
1161
  lease: reusableLease,
657
- failureReason: "resume_failed",
1162
+ failureReason: workerVerifiesResume ? "resume_failed" : "resume_capability_lost",
658
1163
  });
659
1164
  }
660
1165
  }
@@ -679,6 +1184,11 @@ function createSandboxEnvironmentDriver(db, options = {}) {
679
1184
  // environment's default adapter image (a pi agent then runs in the
680
1185
  // opencode image and the harness binary is missing at exec time).
681
1186
  adapterType: input.adapterType ?? undefined,
1187
+ // Forward a caller deadline so the provider configures a provider-side
1188
+ // expiry at or before it and returns the real provider expiry.
1189
+ ...(requestedExpiresAtParam(input.requestedExpiresAt) !== undefined
1190
+ ? { requestedExpiresAt: requestedExpiresAtParam(input.requestedExpiresAt) }
1191
+ : {}),
682
1192
  }, resolvePluginSandboxRpcTimeoutMs(workerConfig));
683
1193
  // Ad-hoc test leases are never publishable for reuse: storing them
684
1194
  // as `reuse_by_environment` would let a concurrent heartbeat resume
@@ -703,28 +1213,76 @@ function createSandboxEnvironmentDriver(db, options = {}) {
703
1213
  providerMetadata: sanitizedProviderMetadata,
704
1214
  })
705
1215
  : null;
706
- return await environmentsSvc.acquireLease({
707
- companyId: input.companyId,
708
- environmentId: input.environment.id,
709
- executionWorkspaceId: input.executionWorkspaceId,
710
- issueId: input.issueId,
711
- heartbeatRunId: input.heartbeatRunId,
712
- leasePolicy: resolvedLeasePolicy,
713
- provider: parsed.config.provider,
714
- providerLeaseId: acquiredLease.providerLeaseId,
715
- expiresAt: acquiredLease.expiresAt ? new Date(acquiredLease.expiresAt) : undefined,
716
- metadata: {
717
- ...(input.agentId ? { agentId: input.agentId } : {}),
718
- driver: input.environment.driver,
719
- executionWorkspaceMode: input.executionWorkspaceMode,
720
- pluginId: pluginProvider.resolved.plugin.id,
721
- pluginKey: pluginProvider.resolved.plugin.pluginKey,
722
- sandboxProviderPlugin: true,
723
- ...sandboxConfigForLeaseMetadata(storedConfig),
724
- ...sanitizedProviderMetadata,
725
- ...(reusableScope ? { reusableSandboxLease: reusableScope } : {}),
726
- },
727
- });
1216
+ const pluginLeaseMetadata = {
1217
+ ...(input.agentId ? { agentId: input.agentId } : {}),
1218
+ driver: input.environment.driver,
1219
+ executionWorkspaceMode: input.executionWorkspaceMode,
1220
+ pluginId: pluginProvider.resolved.plugin.id,
1221
+ pluginKey: pluginProvider.resolved.plugin.pluginKey,
1222
+ sandboxProviderPlugin: true,
1223
+ ...sandboxConfigForLeaseMetadata(storedConfig),
1224
+ ...sanitizedProviderMetadata,
1225
+ ...(reusableScope ? { reusableSandboxLease: reusableScope } : {}),
1226
+ };
1227
+ try {
1228
+ return await environmentsSvc.acquireLease({
1229
+ companyId: input.companyId,
1230
+ environmentId: input.environment.id,
1231
+ executionWorkspaceId: input.executionWorkspaceId,
1232
+ issueId: input.issueId,
1233
+ heartbeatRunId: input.heartbeatRunId,
1234
+ assertCompanyBinding: input.assertCompanyBinding,
1235
+ leasePolicy: resolvedLeasePolicy,
1236
+ provider: parsed.config.provider,
1237
+ providerLeaseId: acquiredLease.providerLeaseId,
1238
+ expiresAt: providerAttestedLeaseExpiry(input.requestedExpiresAt, acquiredLease.expiresAt ? new Date(acquiredLease.expiresAt) : undefined),
1239
+ metadata: pluginLeaseMetadata,
1240
+ });
1241
+ }
1242
+ catch (error) {
1243
+ // The conditional lease insert rejected, so no lease row exists. A
1244
+ // managed reconciliation can bind the environment to another company
1245
+ // between the route guard and this insert, so the insert fails closed
1246
+ // with `environment_company_mismatch`. This call already provisioned the
1247
+ // remote plugin sandbox above, so tear it down now. Without this step
1248
+ // the rejected insert leaks a live sandbox that no lease row tracks. The
1249
+ // Claude login runs on a plugin-backed sandbox, so this path is the one
1250
+ // the login uses. Do not tear down a reused sandbox that an earlier
1251
+ // lease still owns.
1252
+ if (!reusableLease || acquiredLease.providerLeaseId !== reusableLease.providerLeaseId) {
1253
+ // Record the durable pending-cleanup row before the teardown, then
1254
+ // tear the sandbox down. On a successful teardown the handler releases
1255
+ // the row. If the durable write and the teardown both fail, the
1256
+ // handler throws a `SandboxOrphanCleanupWriteError` that carries the
1257
+ // original rejection as its cause.
1258
+ await cleanUpRejectedOrphanSandbox({
1259
+ record: {
1260
+ companyId: input.companyId,
1261
+ environmentId: input.environment.id,
1262
+ executionWorkspaceId: input.executionWorkspaceId ?? null,
1263
+ issueId: input.issueId ?? null,
1264
+ heartbeatRunId: input.heartbeatRunId ?? null,
1265
+ provider: parsed.config.provider,
1266
+ providerLeaseId: acquiredLease.providerLeaseId,
1267
+ metadata: pluginLeaseMetadata,
1268
+ },
1269
+ cause: error,
1270
+ canTeardown: pluginWorkerManager.isRunning(pluginProvider.resolved.plugin.id),
1271
+ teardown: async () => {
1272
+ await pluginWorkerManager.call(pluginProvider.resolved.plugin.id, "environmentDestroyLease", {
1273
+ driverKey: parsed.config.provider,
1274
+ companyId: input.companyId,
1275
+ environmentId: input.environment.id,
1276
+ issueId: input.issueId,
1277
+ config: workerConfig,
1278
+ providerLeaseId: acquiredLease.providerLeaseId,
1279
+ leaseMetadata: acquiredLease.metadata ?? undefined,
1280
+ }, resolvePluginSandboxRpcTimeoutMs(workerConfig));
1281
+ },
1282
+ });
1283
+ }
1284
+ throw error;
1285
+ }
728
1286
  }
729
1287
  // Built-in sandbox provider path. Same guard as the plugin-backed path:
730
1288
  // ad-hoc tests (heartbeatRunId === null) must never resume an existing
@@ -732,7 +1290,11 @@ function createSandboxEnvironmentDriver(db, options = {}) {
732
1290
  // heartbeat run that shares it. Filter to reusable policies and statuses
733
1291
  // so non-reusable, cleanup-pending, or terminal rows can never be matched.
734
1292
  const builtinSandboxProvider = getBuiltinSandboxProvider(parsed.config.provider);
735
- const supportsReusableLeases = builtinSandboxProvider?.supportsReusableLeases === true;
1293
+ // Resolve the DECLARED reusable-lease capability through the same contract
1294
+ // as the plugin path, so the nested capability wins over the legacy flag.
1295
+ const supportsReusableLeases = resolveDeclaredSandboxCapabilities({
1296
+ supportsReusableLeases: builtinSandboxProvider?.supportsReusableLeases,
1297
+ }).reusableLeases === true;
736
1298
  const providerConfigForLease = sandboxConfigForLeaseMetadata(parsed.config);
737
1299
  const leaseFingerprint = supportsReusableLeases &&
738
1300
  parsed.config.reuseLease &&
@@ -802,6 +1364,9 @@ function createSandboxEnvironmentDriver(db, options = {}) {
802
1364
  agentId: input.agentId,
803
1365
  executionWorkspaceId: input.executionWorkspaceId,
804
1366
  reusableProviderLeaseId,
1367
+ // Forward a caller deadline so the provider configures a provider-side
1368
+ // expiry at or before it and returns the real provider expiry.
1369
+ requestedExpiresAt: requestedExpiresAtParam(input.requestedExpiresAt),
805
1370
  });
806
1371
  }
807
1372
  catch (error) {
@@ -839,23 +1404,65 @@ function createSandboxEnvironmentDriver(db, options = {}) {
839
1404
  providerMetadata: providerLease.metadata,
840
1405
  })
841
1406
  : null;
842
- return await environmentsSvc.acquireLease({
843
- companyId: input.companyId,
844
- environmentId: input.environment.id,
845
- executionWorkspaceId: input.executionWorkspaceId,
846
- issueId: input.issueId,
847
- heartbeatRunId: input.heartbeatRunId,
848
- leasePolicy: resolvedLeasePolicy,
849
- provider: parsed.config.provider,
850
- providerLeaseId: providerLease.providerLeaseId,
851
- metadata: {
852
- ...(input.agentId ? { agentId: input.agentId } : {}),
853
- driver: input.environment.driver,
854
- executionWorkspaceMode: input.executionWorkspaceMode,
855
- ...providerLease.metadata,
856
- ...(reusableScope ? { reusableSandboxLease: reusableScope } : {}),
857
- },
858
- });
1407
+ const builtinLeaseMetadata = {
1408
+ ...(input.agentId ? { agentId: input.agentId } : {}),
1409
+ driver: input.environment.driver,
1410
+ executionWorkspaceMode: input.executionWorkspaceMode,
1411
+ ...providerLease.metadata,
1412
+ ...(reusableScope ? { reusableSandboxLease: reusableScope } : {}),
1413
+ };
1414
+ try {
1415
+ return await environmentsSvc.acquireLease({
1416
+ companyId: input.companyId,
1417
+ environmentId: input.environment.id,
1418
+ executionWorkspaceId: input.executionWorkspaceId,
1419
+ issueId: input.issueId,
1420
+ heartbeatRunId: input.heartbeatRunId,
1421
+ assertCompanyBinding: input.assertCompanyBinding,
1422
+ leasePolicy: resolvedLeasePolicy,
1423
+ provider: parsed.config.provider,
1424
+ providerLeaseId: providerLease.providerLeaseId,
1425
+ expiresAt: providerAttestedLeaseExpiry(input.requestedExpiresAt, providerLease.expiresAt ? new Date(providerLease.expiresAt) : undefined),
1426
+ metadata: builtinLeaseMetadata,
1427
+ });
1428
+ }
1429
+ catch (error) {
1430
+ // The conditional lease insert rejected, so no lease row exists. A managed
1431
+ // reconciliation can bind the environment to another company between the
1432
+ // route guard and this insert, so the insert fails closed with
1433
+ // `environment_company_mismatch`. This call already provisioned the remote
1434
+ // sandbox above, so release it now. Without this teardown the rejected
1435
+ // insert leaks a live sandbox that no lease row tracks. Do not tear down a
1436
+ // reused sandbox that an earlier lease still owns.
1437
+ if (!reusableLease || providerLease.providerLeaseId !== reusableLease.providerLeaseId) {
1438
+ // Record the durable pending-cleanup row before the teardown, then
1439
+ // release the remote sandbox. On a successful teardown the handler
1440
+ // releases the row. If the durable write and the teardown both fail,
1441
+ // the handler throws a `SandboxOrphanCleanupWriteError` that carries the
1442
+ // original rejection as its cause.
1443
+ await cleanUpRejectedOrphanSandbox({
1444
+ record: {
1445
+ companyId: input.companyId,
1446
+ environmentId: input.environment.id,
1447
+ executionWorkspaceId: input.executionWorkspaceId ?? null,
1448
+ issueId: input.issueId ?? null,
1449
+ heartbeatRunId: input.heartbeatRunId ?? null,
1450
+ provider: parsed.config.provider,
1451
+ providerLeaseId: providerLease.providerLeaseId,
1452
+ metadata: builtinLeaseMetadata,
1453
+ },
1454
+ cause: error,
1455
+ canTeardown: true,
1456
+ teardown: async () => {
1457
+ await destroySandboxProviderLease({
1458
+ config: parsed.config,
1459
+ providerLeaseId: providerLease.providerLeaseId,
1460
+ });
1461
+ },
1462
+ });
1463
+ }
1464
+ throw error;
1465
+ }
859
1466
  },
860
1467
  async releaseRunLease(input) {
861
1468
  if (input.status === "expired" && input.lease.leasePolicy === "reuse_by_environment") {
@@ -906,6 +1513,116 @@ function createSandboxEnvironmentDriver(db, options = {}) {
906
1513
  cleanupStatus,
907
1514
  });
908
1515
  },
1516
+ async retryPendingSandboxTeardown(input) {
1517
+ // Resolve the teardown from the immutable orphan lease row, not from the
1518
+ // current environment. The row keeps the provider, the provider lease id,
1519
+ // and the sandbox config in its metadata. A provider change re-points the
1520
+ // environment, and an environment delete removes it, but neither must
1521
+ // strand this teardown. So the retry reads the recorded provider and the
1522
+ // recorded config, and never the current environment provider.
1523
+ const recordedProvider = input.lease.provider ??
1524
+ (typeof input.lease.metadata?.provider === "string" ? input.lease.metadata.provider : null);
1525
+ if (!recordedProvider) {
1526
+ throw new Error(`Pending-cleanup lease "${input.lease.id}" has no recorded provider for teardown.`);
1527
+ }
1528
+ // Build the config from the lease metadata under the orphan's company
1529
+ // scope, so the retry resolves the same connection secrets the failed
1530
+ // acquire used. The recorded config is the sole source of truth here: a
1531
+ // delete removed the environment, and a provider change re-points it, so
1532
+ // the retry never reads the current environment config.
1533
+ const metadataConfig = sandboxConfigFromLeaseMetadataLoose(input.lease);
1534
+ if (!metadataConfig || metadataConfig.provider !== recordedProvider) {
1535
+ throw new Error(`Pending-cleanup lease "${input.lease.id}" has no recorded config for provider "${recordedProvider}".`);
1536
+ }
1537
+ // Plugin-backed provider path. The Claude login runs on a plugin-backed
1538
+ // sandbox, so this path tears down the login orphan.
1539
+ if (!isBuiltinSandboxProvider(recordedProvider)) {
1540
+ if (!pluginWorkerManager) {
1541
+ throw new Error(`Sandbox provider "${recordedProvider}" needs a plugin worker manager for cleanup, but none is available.`);
1542
+ }
1543
+ const pluginProvider = await resolveSandboxProviderPlugin({ provider: recordedProvider });
1544
+ if (pluginProvider.state !== "running") {
1545
+ throw new Error(`Sandbox provider plugin for "${recordedProvider}" is ${pluginProvider.state}, so the cleanup teardown cannot run yet.`);
1546
+ }
1547
+ // Resolve the recorded secret refs through the durable orphan record,
1548
+ // not the environment binding. A delete removed the binding, or a
1549
+ // provider change replaced it, so environment-bound resolution would
1550
+ // reject the recorded API-key ref and strand the teardown forever.
1551
+ const config = await resolveSandboxCleanupConfigSecrets(db, input.lease.companyId, metadataConfig, { issueId: input.lease.issueId, heartbeatRunId: input.lease.heartbeatRunId });
1552
+ const workerConfig = stripSandboxProviderEnvelope(config);
1553
+ await pluginWorkerManager.call(pluginProvider.resolved.plugin.id, "environmentDestroyLease", {
1554
+ driverKey: recordedProvider,
1555
+ companyId: input.lease.companyId,
1556
+ // The provider teardown keys on the provider lease id. The
1557
+ // environment id is only context, and it is empty when a delete
1558
+ // removed the environment before this orphan was recorded.
1559
+ environmentId: input.lease.environmentId ?? input.environment?.id ?? "",
1560
+ issueId: input.lease.issueId,
1561
+ config: workerConfig,
1562
+ providerLeaseId: input.lease.providerLeaseId,
1563
+ leaseMetadata: input.lease.metadata ?? undefined,
1564
+ }, resolvePluginSandboxRpcTimeoutMs(workerConfig));
1565
+ return;
1566
+ }
1567
+ // Built-in provider path. Resolve the recorded config secrets through the
1568
+ // durable orphan record, not the environment binding, for the same reason
1569
+ // as the plugin path above. The teardown targets the recorded provider,
1570
+ // never the current environment provider.
1571
+ const cleanupConfig = await resolveSandboxCleanupConfigSecrets(db, input.lease.companyId, metadataConfig, { issueId: input.lease.issueId, heartbeatRunId: input.lease.heartbeatRunId });
1572
+ await destroySandboxProviderLease({
1573
+ config: cleanupConfig,
1574
+ providerLeaseId: input.lease.providerLeaseId,
1575
+ });
1576
+ },
1577
+ async isPendingCleanupWorkerReady(input) {
1578
+ // Resolve the recorded provider the same way `retryPendingSandboxTeardown`
1579
+ // does, so the probe reads the same target the teardown uses.
1580
+ const recordedProvider = input.lease.provider ??
1581
+ (typeof input.lease.metadata?.provider === "string" ? input.lease.metadata.provider : null);
1582
+ // A missing provider is a permanent misconfiguration. Report ready, so the
1583
+ // teardown runs, throws, and counts toward the cap.
1584
+ if (!recordedProvider)
1585
+ return true;
1586
+ // A built-in provider has no plugin worker, so it is always ready.
1587
+ if (isBuiltinSandboxProvider(recordedProvider))
1588
+ return true;
1589
+ // No worker manager is a permanent condition here. Report ready, so the
1590
+ // teardown runs, throws its own "no worker manager" error, and counts
1591
+ // toward the cap.
1592
+ if (!pluginWorkerManager)
1593
+ return true;
1594
+ // Resolve the installed plugin without a wait. A plugin reload or a plugin
1595
+ // reinstall can remove the plugin row for a short window, so a missing
1596
+ // plugin is a transient condition, not a permanent one. Report not ready,
1597
+ // so the sweep skips the lease without a claim, and a later sweep retries
1598
+ // after the plugin returns. A teardown while the plugin is missing only
1599
+ // throws and burns a finite attempt, so a long reload could exhaust the
1600
+ // retries and strand the sandbox. A permanent uninstall also cannot tear
1601
+ // the sandbox down, because there is no worker to call, so the preserved
1602
+ // pending_cleanup row still holds the durable cleanup state for later.
1603
+ const installed = await resolvePluginSandboxProviderDriverByKey({
1604
+ db,
1605
+ driverKey: recordedProvider,
1606
+ workerManager: pluginWorkerManager,
1607
+ requireRunning: false,
1608
+ });
1609
+ if (!installed)
1610
+ return false;
1611
+ // The plugin is installed but not ready yet. A plugin reload or a plugin
1612
+ // reinstall moves the plugin through this state, so it is a transient
1613
+ // window, not a permanent condition. Report not ready, so the sweep skips
1614
+ // the lease without a claim, and a later sweep retries after the plugin
1615
+ // becomes ready. A teardown here only throws "is not_ready" and burns a
1616
+ // finite attempt, so a long reload could exhaust the retries and strand the
1617
+ // sandbox.
1618
+ if (installed.plugin.status !== "ready")
1619
+ return false;
1620
+ // The plugin is installed and ready, so gate on the live worker. A running
1621
+ // worker is ready. A down worker is the transient restart window, so report
1622
+ // not ready and let a later sweep retry after the worker recovers.
1623
+ return pluginWorkerManager.isRunning(installed.plugin.id);
1624
+ },
1625
+ flushDeferredOrphanCleanups,
909
1626
  async realizeWorkspace(input) {
910
1627
  // Resolve the realized cwd and any provider metadata first, then build ONE
911
1628
  // workspace-realization record and wrap it the SAME way for every driver. A
@@ -1077,6 +1794,81 @@ function createSandboxEnvironmentDriver(db, options = {}) {
1077
1794
  async syncOut(input) {
1078
1795
  return await callPluginEnvironmentSync("environmentSyncOut", input);
1079
1796
  },
1797
+ async effectiveSandboxCapabilities(input) {
1798
+ const metadata = input.lease.metadata ?? {};
1799
+ const providerKey = readString(metadata.provider) ??
1800
+ (input.environment.driver === "sandbox"
1801
+ ? readString(parseEnvironmentDriverConfig(input.environment).config.provider)
1802
+ : null);
1803
+ if (!providerKey) {
1804
+ return resolveEffectiveSandboxCapabilities({ verifiedMethods: [], declared: null, narrowing: null });
1805
+ }
1806
+ let declared = null;
1807
+ let verifiedMethods = [];
1808
+ let configResolutionFailed = false;
1809
+ if (metadata.sandboxProviderPlugin) {
1810
+ const pluginId = readString(metadata.pluginId);
1811
+ // Read the declaration from the exact plugin that acquired the lease,
1812
+ // not the first installed plugin with this driver key. A driver key is
1813
+ // only unique inside one manifest, so two plugins can share it. The
1814
+ // by-key resolver could intersect this lease's verified methods with a
1815
+ // different plugin's declaration. This resolver fails closed: it returns
1816
+ // null when the pinned plugin id is absent, or when that exact plugin no
1817
+ // longer declares this provider key with the `sandbox_provider` kind.
1818
+ const resolvedDriver = pluginId
1819
+ ? await resolvePluginSandboxProviderDriverById({
1820
+ db,
1821
+ pluginId,
1822
+ driverKey: providerKey,
1823
+ })
1824
+ : null;
1825
+ if (!pluginId || !resolvedDriver) {
1826
+ // Exact-plugin identity failure. The lease pins a plugin id, but the
1827
+ // id is missing, that plugin is absent, or it no longer declares this
1828
+ // provider key. Fail closed: resolve every effective capability to
1829
+ // false, no matter what methods a stale or running worker still
1830
+ // advertises. Do not read the worker methods here; passing them would
1831
+ // let an identity-less lease keep a verified baseline. This differs
1832
+ // from a valid plugin whose manifest merely omits
1833
+ // `sandboxCapabilities`: that case keeps `declared` null below and
1834
+ // defers to verified worker discovery.
1835
+ return resolveEffectiveSandboxCapabilities({
1836
+ verifiedMethods: [],
1837
+ declared: null,
1838
+ narrowing: null,
1839
+ });
1840
+ }
1841
+ verifiedMethods = pluginWorkerManager?.getWorker(pluginId)?.supportedMethods ?? [];
1842
+ declared = resolveDeclaredSandboxCapabilities(resolvedDriver.driver);
1843
+ try {
1844
+ // Resolve the provider config to confirm it is readable. A provider
1845
+ // whose config cannot be resolved is untrusted, so a resolution error
1846
+ // fails closed on persistent process sessions below. The resolved
1847
+ // value itself is no longer read for the narrowing decision.
1848
+ await resolvePluginSandboxRuntimeConfig({
1849
+ environment: input.environment,
1850
+ lease: input.lease,
1851
+ provider: providerKey,
1852
+ });
1853
+ }
1854
+ catch {
1855
+ configResolutionFailed = true;
1856
+ }
1857
+ }
1858
+ else {
1859
+ const builtin = getBuiltinSandboxProvider(providerKey);
1860
+ verifiedMethods = builtinSandboxProviderVerifiedMethods(builtin);
1861
+ declared = resolveDeclaredSandboxCapabilities({
1862
+ supportsReusableLeases: builtin?.supportsReusableLeases,
1863
+ });
1864
+ }
1865
+ const narrowing = buildSandboxCapabilityNarrowing({
1866
+ leasePolicy: input.lease.leasePolicy,
1867
+ leaseMetadata: metadata,
1868
+ configResolutionFailed,
1869
+ });
1870
+ return resolveEffectiveSandboxCapabilities({ verifiedMethods, declared, narrowing });
1871
+ },
1080
1872
  async destroyRunLease(input) {
1081
1873
  return await destroyReusableSandboxLease({
1082
1874
  environment: input.environment,
@@ -1085,12 +1877,29 @@ function createSandboxEnvironmentDriver(db, options = {}) {
1085
1877
  });
1086
1878
  },
1087
1879
  };
1880
+ /**
1881
+ * Verify that the live plugin worker still advertises a reusable-lease
1882
+ * lifecycle method before the runtime dispatches that RPC. The worker reports
1883
+ * `supportedMethods` from its discovery list on every start. A worker restart
1884
+ * can drop a lifecycle method a reusable lease was created under. The runtime
1885
+ * must not dispatch a lifecycle RPC the live worker does not advertise. It
1886
+ * fails closed when the worker is absent or the method is stale, so a lease
1887
+ * that a worker can no longer clean up goes to the cleanup reaper instead of
1888
+ * a doomed RPC.
1889
+ */
1890
+ function pluginWorkerVerifiesLifecycleMethod(pluginId, method) {
1891
+ const advertised = pluginWorkerManager?.getWorker(pluginId)?.supportedMethods ?? [];
1892
+ return advertised.includes(method);
1893
+ }
1088
1894
  async function releasePluginBackedSandboxLease(input) {
1089
1895
  const metadata = input.lease.metadata ?? {};
1090
1896
  const pluginId = readString(metadata.pluginId);
1091
1897
  const providerKey = readString(metadata.provider);
1092
1898
  let cleanupStatus = "success";
1093
- if (pluginId && providerKey && pluginWorkerManager?.isRunning(pluginId)) {
1899
+ if (pluginId &&
1900
+ providerKey &&
1901
+ pluginWorkerManager?.isRunning(pluginId) &&
1902
+ pluginWorkerVerifiesLifecycleMethod(pluginId, "environmentReleaseLease")) {
1094
1903
  try {
1095
1904
  const config = await resolvePluginSandboxRuntimeConfig({
1096
1905
  environment: input.environment,
@@ -1114,11 +1923,24 @@ function createSandboxEnvironmentDriver(db, options = {}) {
1114
1923
  else {
1115
1924
  cleanupStatus = "failed";
1116
1925
  }
1117
- const releaseStatus = input.lease.leasePolicy === "retain_on_failure" && input.status === "failed"
1926
+ // A failed release verification leaves the provider resource active. The
1927
+ // cleanup reaper retries only `pending_cleanup` leases, so route a failed
1928
+ // release into that retry flow. A `retain_on_failure` lease keeps the
1929
+ // resource on purpose for reuse, so it stays `retained` and never enters the
1930
+ // reaper, which would destroy the resource the retain policy wants to keep.
1931
+ const retained = input.lease.leasePolicy === "retain_on_failure" && input.status === "failed";
1932
+ const releaseStatus = retained
1118
1933
  ? "retained"
1119
- : input.status;
1934
+ : cleanupStatus === "failed"
1935
+ ? "pending_cleanup"
1936
+ : input.status;
1937
+ const failureReason = input.status === "failed"
1938
+ ? "adapter_or_run_failure"
1939
+ : cleanupStatus === "failed"
1940
+ ? "release_cleanup_failed"
1941
+ : undefined;
1120
1942
  return await environmentsSvc.releaseLease(input.lease.id, releaseStatus, {
1121
- failureReason: input.status === "failed" ? "adapter_or_run_failure" : undefined,
1943
+ failureReason,
1122
1944
  cleanupStatus,
1123
1945
  });
1124
1946
  }
@@ -1129,7 +1951,10 @@ function createSandboxEnvironmentDriver(db, options = {}) {
1129
1951
  if (metadata.sandboxProviderPlugin) {
1130
1952
  const pluginId = readString(metadata.pluginId);
1131
1953
  const providerKey = readString(metadata.provider);
1132
- if (!pluginId || !providerKey || !pluginWorkerManager?.isRunning(pluginId)) {
1954
+ if (!pluginId ||
1955
+ !providerKey ||
1956
+ !pluginWorkerManager?.isRunning(pluginId) ||
1957
+ !pluginWorkerVerifiesLifecycleMethod(pluginId, "environmentDestroyLease")) {
1133
1958
  cleanupStatus = "failed";
1134
1959
  }
1135
1960
  else {
@@ -1184,24 +2009,66 @@ function parseExpiresAt(value) {
1184
2009
  const parsed = new Date(value);
1185
2010
  return Number.isNaN(parsed.getTime()) ? null : parsed;
1186
2011
  }
2012
+ /**
2013
+ * Resolves the persisted lease expiry for a caller-requested deadline. It records
2014
+ * ONLY a provider-attested expiry as evidence of provider enforcement. It never
2015
+ * synthesizes the requested deadline onto the lease row: a database-only expiry
2016
+ * does not stop a remote sandbox after a server crash, so it must not stand in for
2017
+ * a provider-side bound. It returns the real provider expiry when the caller sets
2018
+ * a deadline (the caller then verifies the expiry bounds the deadline and fails
2019
+ * closed when it does not). It returns null when the caller sets a deadline and the
2020
+ * provider grants no expiry. It returns the provider expiry unchanged when the
2021
+ * caller requests no deadline, so all other callers keep the current behavior. It
2022
+ * ignores an invalid requested deadline.
2023
+ */
2024
+ function providerAttestedLeaseExpiry(requestedExpiresAt, providerExpiresAt) {
2025
+ const requested = requestedExpiresAt instanceof Date && !Number.isNaN(requestedExpiresAt.getTime())
2026
+ ? requestedExpiresAt
2027
+ : null;
2028
+ if (!requested)
2029
+ return providerExpiresAt;
2030
+ return providerExpiresAt ?? null;
2031
+ }
2032
+ /**
2033
+ * Converts a caller-requested deadline to the ISO 8601 string a provider acquire
2034
+ * RPC carries. It returns undefined for an absent or invalid deadline, so a
2035
+ * generic caller without a deadline sends no requested expiry and keeps the
2036
+ * current provider behavior.
2037
+ */
2038
+ function requestedExpiresAtParam(requestedExpiresAt) {
2039
+ return requestedExpiresAt instanceof Date && !Number.isNaN(requestedExpiresAt.getTime())
2040
+ ? requestedExpiresAt.toISOString()
2041
+ : undefined;
2042
+ }
1187
2043
  function pluginDriverProviderKey(config) {
1188
2044
  return `${config.pluginKey}:${config.driverKey}`;
1189
2045
  }
1190
2046
  function readString(value) {
1191
2047
  return typeof value === "string" && value.length > 0 ? value : null;
1192
2048
  }
2049
+ // Keys the runtime stores in the lease metadata that are not part of the
2050
+ // provider driver config. Some are host-internal control fields. `remoteCwd` is
2051
+ // a per-lease runtime value. The host reads `remoteCwd` from the lease metadata
2052
+ // directly, so the worker never needs it as config. Drop every key here before
2053
+ // the runtime sends a config to a lifecycle RPC.
1193
2054
  const INTERNAL_PLUGIN_SANDBOX_CONFIG_KEYS = new Set([
1194
2055
  "driver",
1195
2056
  "executionWorkspaceMode",
1196
2057
  "pluginId",
1197
2058
  "pluginKey",
1198
2059
  "providerMetadata",
2060
+ "remoteCwd",
1199
2061
  "shellCommand",
1200
2062
  "sandboxProviderPlugin",
1201
2063
  ]);
1202
- function sanitizePluginSandboxConfigFromLeaseMetadata(metadata) {
2064
+ // Drop the host-internal and per-lease runtime keys from a sandbox config
2065
+ // record. The runtime stores these keys in the lease metadata and in some
2066
+ // resolved configs, but the plugin worker must receive only the provider driver
2067
+ // config. Use this on every config the runtime sends to a lifecycle RPC, so no
2068
+ // host-internal field reaches the worker.
2069
+ function dropInternalPluginSandboxConfigKeys(config) {
1203
2070
  const sanitized = {};
1204
- for (const [key, value] of Object.entries(metadata ?? {})) {
2071
+ for (const [key, value] of Object.entries(config ?? {})) {
1205
2072
  if (INTERNAL_PLUGIN_SANDBOX_CONFIG_KEYS.has(key))
1206
2073
  continue;
1207
2074
  sanitized[key] = value;
@@ -1308,6 +2175,11 @@ function createPluginEnvironmentDriver(db, workerManager) {
1308
2175
  executionWorkspaceId: input.executionWorkspaceId ?? undefined,
1309
2176
  adapterType: input.adapterType ?? undefined,
1310
2177
  executionWorkspaceSettings: input.executionWorkspaceSettings,
2178
+ // Forward a caller deadline so the provider configures a provider-side
2179
+ // expiry at or before it and returns the real provider expiry.
2180
+ ...(requestedExpiresAtParam(input.requestedExpiresAt) !== undefined
2181
+ ? { requestedExpiresAt: requestedExpiresAtParam(input.requestedExpiresAt) }
2182
+ : {}),
1311
2183
  });
1312
2184
  return await environmentsSvc.acquireLease({
1313
2185
  companyId: input.companyId,
@@ -1318,7 +2190,7 @@ function createPluginEnvironmentDriver(db, workerManager) {
1318
2190
  leasePolicy: "ephemeral",
1319
2191
  provider: `plugin:${parsed.config.pluginKey}:${parsed.config.driverKey}`,
1320
2192
  providerLeaseId: providerLease.providerLeaseId,
1321
- expiresAt: parseExpiresAt(providerLease.expiresAt),
2193
+ expiresAt: providerAttestedLeaseExpiry(input.requestedExpiresAt, parseExpiresAt(providerLease.expiresAt)),
1322
2194
  metadata: {
1323
2195
  ...(input.agentId ? { agentId: input.agentId } : {}),
1324
2196
  providerMetadata: providerLease.metadata ?? {},
@@ -1466,6 +2338,11 @@ export function environmentRuntimeService(db, options = {}) {
1466
2338
  pluginWorkerManager: options.pluginWorkerManager,
1467
2339
  pluginWorkerReadyTimeoutMs: options.pluginWorkerReadyTimeoutMs,
1468
2340
  pluginWorkerReadyPollMs: options.pluginWorkerReadyPollMs,
2341
+ pendingCleanupWriteAttempts: options.pendingCleanupWriteAttempts,
2342
+ pendingCleanupWriteBackoffMs: options.pendingCleanupWriteBackoffMs,
2343
+ deferredOrphanCleanupBufferLimit: options.deferredOrphanCleanupBufferLimit,
2344
+ orphanCleanupSpool: options.orphanCleanupSpool,
2345
+ orphanCleanupSpoolDir: options.orphanCleanupSpoolDir,
1469
2346
  }),
1470
2347
  ...(options.pluginWorkerManager
1471
2348
  ? [createPluginEnvironmentDriver(db, options.pluginWorkerManager)]
@@ -1512,6 +2389,8 @@ export function environmentRuntimeService(db, options = {}) {
1512
2389
  executionWorkspaceSettings: input.executionWorkspaceSettings ?? null,
1513
2390
  adapterType: input.adapterType ?? null,
1514
2391
  applyCustomImageTemplate: input.applyCustomImageTemplate ?? false,
2392
+ requestedExpiresAt: input.requestedExpiresAt ?? null,
2393
+ assertCompanyBinding: input.assertCompanyBinding,
1515
2394
  });
1516
2395
  return {
1517
2396
  environment: input.environment,
@@ -1519,7 +2398,7 @@ export function environmentRuntimeService(db, options = {}) {
1519
2398
  leaseContext,
1520
2399
  };
1521
2400
  },
1522
- async releaseRunLeases(heartbeatRunId, status = "released") {
2401
+ async releaseRunLeases(heartbeatRunId, status = "released", onLeaseReleaseError) {
1523
2402
  const leaseRows = await db
1524
2403
  .select()
1525
2404
  .from(environmentLeases)
@@ -1527,33 +2406,87 @@ export function environmentRuntimeService(db, options = {}) {
1527
2406
  if (leaseRows.length === 0) {
1528
2407
  return [];
1529
2408
  }
2409
+ // Release each lease in its own try/catch. One driver error must not stop
2410
+ // the release of the later leases. The caller records each lease-specific
2411
+ // error through `onLeaseReleaseError` for its log path. Keep the order
2412
+ // serial.
1530
2413
  const released = [];
1531
2414
  for (const leaseRow of leaseRows) {
1532
- const environment = await environmentsSvc.getById(leaseRow.environmentId);
1533
- if (!environment)
1534
- continue;
1535
- const leaseSnapshot = toEnvironmentLeaseSnapshot(leaseRow);
1536
- const driver = getDriver(getLeaseDriverKey(leaseSnapshot, environment));
1537
- const lease = driver
1538
- ? await driver.releaseRunLease({
2415
+ try {
2416
+ const environment = leaseRow.environmentId
2417
+ ? await environmentsSvc.getById(leaseRow.environmentId)
2418
+ : null;
2419
+ if (!environment)
2420
+ continue;
2421
+ const leaseSnapshot = toEnvironmentLeaseSnapshot(leaseRow);
2422
+ const driver = getDriver(getLeaseDriverKey(leaseSnapshot, environment));
2423
+ const lease = driver
2424
+ ? await driver.releaseRunLease({
2425
+ environment,
2426
+ lease: leaseSnapshot,
2427
+ status,
2428
+ })
2429
+ : await environmentsSvc.releaseLease(leaseRow.id, status);
2430
+ if (!lease)
2431
+ continue;
2432
+ released.push({
1539
2433
  environment,
1540
- lease: leaseSnapshot,
1541
- status,
1542
- })
1543
- : await environmentsSvc.releaseLease(leaseRow.id, status);
1544
- if (!lease)
1545
- continue;
1546
- released.push({
1547
- environment,
1548
- lease,
1549
- leaseContext: {
1550
- executionWorkspaceId: lease.executionWorkspaceId,
1551
- executionWorkspaceMode: lease.metadata?.executionWorkspaceMode ?? null,
1552
- },
1553
- });
2434
+ lease,
2435
+ leaseContext: {
2436
+ executionWorkspaceId: lease.executionWorkspaceId,
2437
+ executionWorkspaceMode: lease.metadata?.executionWorkspaceMode ?? null,
2438
+ },
2439
+ });
2440
+ }
2441
+ catch (error) {
2442
+ onLeaseReleaseError?.(leaseRow.id, error);
2443
+ }
1554
2444
  }
1555
2445
  return released;
1556
2446
  },
2447
+ // Tear an orphan ephemeral sandbox down from its recorded provider config.
2448
+ // A failed acquire records the orphan as a `pending_cleanup` lease. The row
2449
+ // keeps the provider, the provider lease id, and the sandbox config, so this
2450
+ // teardown runs without the environment row and accepts a null environment.
2451
+ // The teardown resolves the recorded secret refs through the durable orphan
2452
+ // record, so a deleted or foreign-bound environment never strands it. The
2453
+ // caller owns the lease release; this dispatcher only runs the provider
2454
+ // teardown and throws when the driver teardown throws.
2455
+ async retryPendingSandboxTeardown(input) {
2456
+ const driver = requireDriverKey(getLeaseDriverKey(input.lease, input.environment));
2457
+ if (!driver.retryPendingSandboxTeardown) {
2458
+ throw new Error(`Environment driver "${driver.driver}" does not support orphan sandbox teardown.`);
2459
+ }
2460
+ await driver.retryPendingSandboxTeardown(input);
2461
+ },
2462
+ // Report whether the provider worker can run an orphan teardown now. The
2463
+ // cleanup sweep calls this before it claims a finite retry attempt, so a
2464
+ // briefly-down plugin worker never burns an attempt. This dispatcher never
2465
+ // throws: an unregistered driver, or a driver with no probe, reports ready,
2466
+ // so the teardown still runs and its own failure counts toward the cap.
2467
+ async isPendingCleanupWorkerReady(input) {
2468
+ const driver = getDriver(getLeaseDriverKey(input.lease, input.environment));
2469
+ if (!driver?.isPendingCleanupWorkerReady)
2470
+ return true;
2471
+ return driver.isPendingCleanupWorkerReady(input);
2472
+ },
2473
+ // Flush the in-process orphan-cleanup buffers of every driver that keeps one.
2474
+ // The cleanup sweep calls this each tick, so a buffered orphan lands a durable
2475
+ // `pending_cleanup` row once the database recovers, and the same sweep tears
2476
+ // it down. The dispatcher sums the recovered and pending counts across all
2477
+ // drivers. A driver with no buffer contributes nothing.
2478
+ async flushDeferredOrphanCleanups() {
2479
+ let recovered = 0;
2480
+ let pending = 0;
2481
+ for (const driver of drivers.values()) {
2482
+ if (!driver.flushDeferredOrphanCleanups)
2483
+ continue;
2484
+ const result = await driver.flushDeferredOrphanCleanups();
2485
+ recovered += result.recovered;
2486
+ pending += result.pending;
2487
+ }
2488
+ return { recovered, pending };
2489
+ },
1557
2490
  async destroyReusableSandboxLeases(input) {
1558
2491
  const scopeConditions = [
1559
2492
  input.issueId ? eq(environmentLeases.issueId, input.issueId) : undefined,
@@ -1567,7 +2500,9 @@ export function environmentRuntimeService(db, options = {}) {
1567
2500
  .where(and(eq(environmentLeases.companyId, input.companyId), eq(environmentLeases.leasePolicy, "reuse_by_environment"), inArray(environmentLeases.status, ["active", "released", "retained", "pending_cleanup"]), ...scopeConditions));
1568
2501
  const destroyed = [];
1569
2502
  for (const leaseRow of leaseRows) {
1570
- const environment = await environmentsSvc.getById(leaseRow.environmentId);
2503
+ const environment = leaseRow.environmentId
2504
+ ? await environmentsSvc.getById(leaseRow.environmentId)
2505
+ : null;
1571
2506
  if (!environment)
1572
2507
  continue;
1573
2508
  const leaseSnapshot = toEnvironmentLeaseSnapshot(leaseRow);
@@ -1627,6 +2562,10 @@ export function environmentRuntimeService(db, options = {}) {
1627
2562
  const driver = getDriver(getLeaseDriverKey(input.lease, input.environment));
1628
2563
  return driver?.supportsSync?.(input) ?? false;
1629
2564
  },
2565
+ async effectiveSandboxCapabilities(input) {
2566
+ const driver = getDriver(getLeaseDriverKey(input.lease, input.environment));
2567
+ return (await driver?.effectiveSandboxCapabilities?.(input)) ?? null;
2568
+ },
1630
2569
  async syncIn(input) {
1631
2570
  const driver = requireDriverKey(getLeaseDriverKey(input.lease, input.environment));
1632
2571
  if (!driver.syncIn) {