@tickernelz/paperclip-pro-adapter-utils 2026.925.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1826) hide show
  1. package/README.md +37 -0
  2. package/dist/acpx-engine/cli.d.ts +2 -0
  3. package/dist/acpx-engine/cli.d.ts.map +1 -0
  4. package/dist/acpx-engine/cli.js +128 -0
  5. package/dist/acpx-engine/cli.js.map +1 -0
  6. package/dist/acpx-engine/composed-run-characterization.test.d.ts +2 -0
  7. package/dist/acpx-engine/composed-run-characterization.test.d.ts.map +1 -0
  8. package/dist/acpx-engine/composed-run-characterization.test.js +739 -0
  9. package/dist/acpx-engine/composed-run-characterization.test.js.map +1 -0
  10. package/dist/acpx-engine/constants.d.ts +20 -0
  11. package/dist/acpx-engine/constants.d.ts.map +1 -0
  12. package/dist/acpx-engine/constants.js +37 -0
  13. package/dist/acpx-engine/constants.js.map +1 -0
  14. package/dist/acpx-engine/execute-identity.test.d.ts +2 -0
  15. package/dist/acpx-engine/execute-identity.test.d.ts.map +1 -0
  16. package/dist/acpx-engine/execute-identity.test.js +218 -0
  17. package/dist/acpx-engine/execute-identity.test.js.map +1 -0
  18. package/dist/acpx-engine/execute.d.ts +325 -0
  19. package/dist/acpx-engine/execute.d.ts.map +1 -0
  20. package/dist/acpx-engine/execute.js +4448 -0
  21. package/dist/acpx-engine/execute.js.map +1 -0
  22. package/dist/acpx-engine/execute.test.d.ts +2 -0
  23. package/dist/acpx-engine/execute.test.d.ts.map +1 -0
  24. package/dist/acpx-engine/execute.test.js +6694 -0
  25. package/dist/acpx-engine/execute.test.js.map +1 -0
  26. package/dist/acpx-engine/index.d.ts +6 -0
  27. package/dist/acpx-engine/index.d.ts.map +1 -0
  28. package/dist/acpx-engine/index.js +6 -0
  29. package/dist/acpx-engine/index.js.map +1 -0
  30. package/dist/acpx-engine/local-process-control.d.ts +5 -0
  31. package/dist/acpx-engine/local-process-control.d.ts.map +1 -0
  32. package/dist/acpx-engine/local-process-control.js +39 -0
  33. package/dist/acpx-engine/local-process-control.js.map +1 -0
  34. package/dist/acpx-engine/local-process-control.test.d.ts +2 -0
  35. package/dist/acpx-engine/local-process-control.test.d.ts.map +1 -0
  36. package/dist/acpx-engine/local-process-control.test.js +38 -0
  37. package/dist/acpx-engine/local-process-control.test.js.map +1 -0
  38. package/dist/acpx-engine/operator-stop.test.d.ts +2 -0
  39. package/dist/acpx-engine/operator-stop.test.d.ts.map +1 -0
  40. package/dist/acpx-engine/operator-stop.test.js +107 -0
  41. package/dist/acpx-engine/operator-stop.test.js.map +1 -0
  42. package/dist/acpx-engine/remote-spawn-smoke.test.d.ts +2 -0
  43. package/dist/acpx-engine/remote-spawn-smoke.test.d.ts.map +1 -0
  44. package/dist/acpx-engine/remote-spawn-smoke.test.js +151 -0
  45. package/dist/acpx-engine/remote-spawn-smoke.test.js.map +1 -0
  46. package/dist/acpx-engine/run-contracts.d.ts +413 -0
  47. package/dist/acpx-engine/run-contracts.d.ts.map +1 -0
  48. package/dist/acpx-engine/run-contracts.js +20 -0
  49. package/dist/acpx-engine/run-contracts.js.map +1 -0
  50. package/dist/acpx-engine/run-contracts.test.d.ts +2 -0
  51. package/dist/acpx-engine/run-contracts.test.d.ts.map +1 -0
  52. package/dist/acpx-engine/run-contracts.test.js +57 -0
  53. package/dist/acpx-engine/run-contracts.test.js.map +1 -0
  54. package/dist/acpx-engine/run-coordinator.d.ts +57 -0
  55. package/dist/acpx-engine/run-coordinator.d.ts.map +1 -0
  56. package/dist/acpx-engine/run-coordinator.js +63 -0
  57. package/dist/acpx-engine/run-coordinator.js.map +1 -0
  58. package/dist/acpx-engine/run-coordinator.test.d.ts +2 -0
  59. package/dist/acpx-engine/run-coordinator.test.d.ts.map +1 -0
  60. package/dist/acpx-engine/run-coordinator.test.js +160 -0
  61. package/dist/acpx-engine/run-coordinator.test.js.map +1 -0
  62. package/dist/acpx-engine/run-fault-matrix.test.d.ts +2 -0
  63. package/dist/acpx-engine/run-fault-matrix.test.d.ts.map +1 -0
  64. package/dist/acpx-engine/run-fault-matrix.test.js +829 -0
  65. package/dist/acpx-engine/run-fault-matrix.test.js.map +1 -0
  66. package/dist/acpx-engine/run-resource-ledger.d.ts +30 -0
  67. package/dist/acpx-engine/run-resource-ledger.d.ts.map +1 -0
  68. package/dist/acpx-engine/run-resource-ledger.js +148 -0
  69. package/dist/acpx-engine/run-resource-ledger.js.map +1 -0
  70. package/dist/acpx-engine/run-resource-ledger.test.d.ts +2 -0
  71. package/dist/acpx-engine/run-resource-ledger.test.d.ts.map +1 -0
  72. package/dist/acpx-engine/run-resource-ledger.test.js +154 -0
  73. package/dist/acpx-engine/run-resource-ledger.test.js.map +1 -0
  74. package/dist/acpx-engine/run-site-host.d.ts +73 -0
  75. package/dist/acpx-engine/run-site-host.d.ts.map +1 -0
  76. package/dist/acpx-engine/run-site-host.js +65 -0
  77. package/dist/acpx-engine/run-site-host.js.map +1 -0
  78. package/dist/acpx-engine/run-site-host.test.d.ts +2 -0
  79. package/dist/acpx-engine/run-site-host.test.d.ts.map +1 -0
  80. package/dist/acpx-engine/run-site-host.test.js +98 -0
  81. package/dist/acpx-engine/run-site-host.test.js.map +1 -0
  82. package/dist/acpx-engine/run-site-sandbox.d.ts +142 -0
  83. package/dist/acpx-engine/run-site-sandbox.d.ts.map +1 -0
  84. package/dist/acpx-engine/run-site-sandbox.js +278 -0
  85. package/dist/acpx-engine/run-site-sandbox.js.map +1 -0
  86. package/dist/acpx-engine/run-site-sandbox.test.d.ts +2 -0
  87. package/dist/acpx-engine/run-site-sandbox.test.d.ts.map +1 -0
  88. package/dist/acpx-engine/run-site-sandbox.test.js +275 -0
  89. package/dist/acpx-engine/run-site-sandbox.test.js.map +1 -0
  90. package/dist/acpx-engine/session-codec.d.ts +3 -0
  91. package/dist/acpx-engine/session-codec.d.ts.map +1 -0
  92. package/dist/acpx-engine/session-codec.js +49 -0
  93. package/dist/acpx-engine/session-codec.js.map +1 -0
  94. package/dist/acpx-engine/session-reuse-store.d.ts +62 -0
  95. package/dist/acpx-engine/session-reuse-store.d.ts.map +1 -0
  96. package/dist/acpx-engine/session-reuse-store.js +134 -0
  97. package/dist/acpx-engine/session-reuse-store.js.map +1 -0
  98. package/dist/acpx-engine/session-reuse-store.test.d.ts +2 -0
  99. package/dist/acpx-engine/session-reuse-store.test.d.ts.map +1 -0
  100. package/dist/acpx-engine/session-reuse-store.test.js +134 -0
  101. package/dist/acpx-engine/session-reuse-store.test.js.map +1 -0
  102. package/dist/acpx-engine/settlement-characterization.test.d.ts +2 -0
  103. package/dist/acpx-engine/settlement-characterization.test.d.ts.map +1 -0
  104. package/dist/acpx-engine/settlement-characterization.test.js +880 -0
  105. package/dist/acpx-engine/settlement-characterization.test.js.map +1 -0
  106. package/dist/acpx-engine/settlement-sequence.d.ts +77 -0
  107. package/dist/acpx-engine/settlement-sequence.d.ts.map +1 -0
  108. package/dist/acpx-engine/settlement-sequence.js +119 -0
  109. package/dist/acpx-engine/settlement-sequence.js.map +1 -0
  110. package/dist/acpx-engine/settlement-sequence.test.d.ts +2 -0
  111. package/dist/acpx-engine/settlement-sequence.test.d.ts.map +1 -0
  112. package/dist/acpx-engine/settlement-sequence.test.js +272 -0
  113. package/dist/acpx-engine/settlement-sequence.test.js.map +1 -0
  114. package/dist/acpx-engine/spawn-smoke.test.d.ts +2 -0
  115. package/dist/acpx-engine/spawn-smoke.test.d.ts.map +1 -0
  116. package/dist/acpx-engine/spawn-smoke.test.js +184 -0
  117. package/dist/acpx-engine/spawn-smoke.test.js.map +1 -0
  118. package/dist/acpx-engine/startup-cancellation.d.ts +8 -0
  119. package/dist/acpx-engine/startup-cancellation.d.ts.map +1 -0
  120. package/dist/acpx-engine/startup-cancellation.js +92 -0
  121. package/dist/acpx-engine/startup-cancellation.js.map +1 -0
  122. package/dist/acpx-engine/startup-cancellation.test.d.ts +2 -0
  123. package/dist/acpx-engine/startup-cancellation.test.d.ts.map +1 -0
  124. package/dist/acpx-engine/startup-cancellation.test.js +115 -0
  125. package/dist/acpx-engine/startup-cancellation.test.js.map +1 -0
  126. package/dist/acpx-engine/startup-characterization.test.d.ts +2 -0
  127. package/dist/acpx-engine/startup-characterization.test.d.ts.map +1 -0
  128. package/dist/acpx-engine/startup-characterization.test.js +742 -0
  129. package/dist/acpx-engine/startup-characterization.test.js.map +1 -0
  130. package/dist/acpx-engine/startup-timing.d.ts +419 -0
  131. package/dist/acpx-engine/startup-timing.d.ts.map +1 -0
  132. package/dist/acpx-engine/startup-timing.js +616 -0
  133. package/dist/acpx-engine/startup-timing.js.map +1 -0
  134. package/dist/acpx-engine/startup-timing.test.d.ts +2 -0
  135. package/dist/acpx-engine/startup-timing.test.d.ts.map +1 -0
  136. package/dist/acpx-engine/startup-timing.test.js +619 -0
  137. package/dist/acpx-engine/startup-timing.test.js.map +1 -0
  138. package/dist/acpx-engine/turn-characterization.test.d.ts +2 -0
  139. package/dist/acpx-engine/turn-characterization.test.d.ts.map +1 -0
  140. package/dist/acpx-engine/turn-characterization.test.js +566 -0
  141. package/dist/acpx-engine/turn-characterization.test.js.map +1 -0
  142. package/dist/acpx-engine/turn-sequence.d.ts +54 -0
  143. package/dist/acpx-engine/turn-sequence.d.ts.map +1 -0
  144. package/dist/acpx-engine/turn-sequence.js +75 -0
  145. package/dist/acpx-engine/turn-sequence.js.map +1 -0
  146. package/dist/acpx-engine/turn-sequence.test.d.ts +2 -0
  147. package/dist/acpx-engine/turn-sequence.test.d.ts.map +1 -0
  148. package/dist/acpx-engine/turn-sequence.test.js +177 -0
  149. package/dist/acpx-engine/turn-sequence.test.js.map +1 -0
  150. package/dist/acpx-engine/ui.d.ts +3 -0
  151. package/dist/acpx-engine/ui.d.ts.map +1 -0
  152. package/dist/acpx-engine/ui.js +162 -0
  153. package/dist/acpx-engine/ui.js.map +1 -0
  154. package/dist/billing.d.ts +2 -0
  155. package/dist/billing.d.ts.map +1 -0
  156. package/dist/billing.js +16 -0
  157. package/dist/billing.js.map +1 -0
  158. package/dist/billing.test.d.ts +2 -0
  159. package/dist/billing.test.d.ts.map +1 -0
  160. package/dist/billing.test.js +14 -0
  161. package/dist/billing.test.js.map +1 -0
  162. package/dist/bridge-transport-contract.d.ts +43 -0
  163. package/dist/bridge-transport-contract.d.ts.map +1 -0
  164. package/dist/bridge-transport-contract.js +33 -0
  165. package/dist/bridge-transport-contract.js.map +1 -0
  166. package/dist/chat-file-delivery.d.ts +10 -0
  167. package/dist/chat-file-delivery.d.ts.map +1 -0
  168. package/dist/chat-file-delivery.js +38 -0
  169. package/dist/chat-file-delivery.js.map +1 -0
  170. package/dist/chat-file-delivery.test.d.ts +2 -0
  171. package/dist/chat-file-delivery.test.d.ts.map +1 -0
  172. package/dist/chat-file-delivery.test.js +92 -0
  173. package/dist/chat-file-delivery.test.js.map +1 -0
  174. package/dist/command-managed-runtime.d.ts +162 -0
  175. package/dist/command-managed-runtime.d.ts.map +1 -0
  176. package/dist/command-managed-runtime.js +456 -0
  177. package/dist/command-managed-runtime.js.map +1 -0
  178. package/dist/command-managed-runtime.test.d.ts +2 -0
  179. package/dist/command-managed-runtime.test.d.ts.map +1 -0
  180. package/dist/command-managed-runtime.test.js +908 -0
  181. package/dist/command-managed-runtime.test.js.map +1 -0
  182. package/dist/command-redaction.d.ts +15 -0
  183. package/dist/command-redaction.d.ts.map +1 -0
  184. package/dist/command-redaction.js +97 -0
  185. package/dist/command-redaction.js.map +1 -0
  186. package/dist/command-redaction.test.d.ts +2 -0
  187. package/dist/command-redaction.test.d.ts.map +1 -0
  188. package/dist/command-redaction.test.js +80 -0
  189. package/dist/command-redaction.test.js.map +1 -0
  190. package/dist/duplex-frame-codec.d.ts +124 -0
  191. package/dist/duplex-frame-codec.d.ts.map +1 -0
  192. package/dist/duplex-frame-codec.js +134 -0
  193. package/dist/duplex-frame-codec.js.map +1 -0
  194. package/dist/duplex-frame-codec.test.d.ts +2 -0
  195. package/dist/duplex-frame-codec.test.d.ts.map +1 -0
  196. package/dist/duplex-frame-codec.test.js +172 -0
  197. package/dist/duplex-frame-codec.test.js.map +1 -0
  198. package/dist/duplex-observability.d.ts +220 -0
  199. package/dist/duplex-observability.d.ts.map +1 -0
  200. package/dist/duplex-observability.js +248 -0
  201. package/dist/duplex-observability.js.map +1 -0
  202. package/dist/duplex-observability.test.d.ts +2 -0
  203. package/dist/duplex-observability.test.d.ts.map +1 -0
  204. package/dist/duplex-observability.test.js +55 -0
  205. package/dist/duplex-observability.test.js.map +1 -0
  206. package/dist/env-bindings.d.ts +17 -0
  207. package/dist/env-bindings.d.ts.map +1 -0
  208. package/dist/env-bindings.js +98 -0
  209. package/dist/env-bindings.js.map +1 -0
  210. package/dist/env-bindings.test.d.ts +2 -0
  211. package/dist/env-bindings.test.d.ts.map +1 -0
  212. package/dist/env-bindings.test.js +66 -0
  213. package/dist/env-bindings.test.js.map +1 -0
  214. package/dist/exclude-patterns.d.ts +4 -0
  215. package/dist/exclude-patterns.d.ts.map +1 -0
  216. package/dist/exclude-patterns.js +26 -0
  217. package/dist/exclude-patterns.js.map +1 -0
  218. package/dist/execution-target-sandbox.test.d.ts +2 -0
  219. package/dist/execution-target-sandbox.test.d.ts.map +1 -0
  220. package/dist/execution-target-sandbox.test.js +6522 -0
  221. package/dist/execution-target-sandbox.test.js.map +1 -0
  222. package/dist/execution-target-stdin-race.test.d.ts +2 -0
  223. package/dist/execution-target-stdin-race.test.d.ts.map +1 -0
  224. package/dist/execution-target-stdin-race.test.js +1842 -0
  225. package/dist/execution-target-stdin-race.test.js.map +1 -0
  226. package/dist/execution-target.d.ts +537 -0
  227. package/dist/execution-target.d.ts.map +1 -0
  228. package/dist/execution-target.js +3877 -0
  229. package/dist/execution-target.js.map +1 -0
  230. package/dist/execution-target.test.d.ts +2 -0
  231. package/dist/execution-target.test.d.ts.map +1 -0
  232. package/dist/execution-target.test.js +346 -0
  233. package/dist/execution-target.test.js.map +1 -0
  234. package/dist/git-workspace-sync.d.ts +193 -0
  235. package/dist/git-workspace-sync.d.ts.map +1 -0
  236. package/dist/git-workspace-sync.js +786 -0
  237. package/dist/git-workspace-sync.js.map +1 -0
  238. package/dist/git-workspace-sync.test.d.ts +2 -0
  239. package/dist/git-workspace-sync.test.d.ts.map +1 -0
  240. package/dist/git-workspace-sync.test.js +830 -0
  241. package/dist/git-workspace-sync.test.js.map +1 -0
  242. package/dist/github-launcher-environment.test.d.ts +2 -0
  243. package/dist/github-launcher-environment.test.d.ts.map +1 -0
  244. package/dist/github-launcher-environment.test.js +285 -0
  245. package/dist/github-launcher-environment.test.js.map +1 -0
  246. package/dist/github-launcher.d.ts +8 -0
  247. package/dist/github-launcher.d.ts.map +1 -0
  248. package/dist/github-launcher.js +127 -0
  249. package/dist/github-launcher.js.map +1 -0
  250. package/dist/github-launcher.test.d.ts +2 -0
  251. package/dist/github-launcher.test.d.ts.map +1 -0
  252. package/dist/github-launcher.test.js +166 -0
  253. package/dist/github-launcher.test.js.map +1 -0
  254. package/dist/http2-bridge-server.d.ts +465 -0
  255. package/dist/http2-bridge-server.d.ts.map +1 -0
  256. package/dist/http2-bridge-server.js +1087 -0
  257. package/dist/http2-bridge-server.js.map +1 -0
  258. package/dist/http2-bridge-server.test.d.ts +2 -0
  259. package/dist/http2-bridge-server.test.d.ts.map +1 -0
  260. package/dist/http2-bridge-server.test.js +2129 -0
  261. package/dist/http2-bridge-server.test.js.map +1 -0
  262. package/dist/index.d.ts +20 -0
  263. package/dist/index.d.ts.map +1 -0
  264. package/dist/index.js +12 -0
  265. package/dist/index.js.map +1 -0
  266. package/dist/local-process-sandbox.d.ts +41 -0
  267. package/dist/local-process-sandbox.d.ts.map +1 -0
  268. package/dist/local-process-sandbox.js +432 -0
  269. package/dist/local-process-sandbox.js.map +1 -0
  270. package/dist/local-process-sandbox.test.d.ts +2 -0
  271. package/dist/local-process-sandbox.test.d.ts.map +1 -0
  272. package/dist/local-process-sandbox.test.js +399 -0
  273. package/dist/local-process-sandbox.test.js.map +1 -0
  274. package/dist/log-redaction.d.ts +9 -0
  275. package/dist/log-redaction.d.ts.map +1 -0
  276. package/dist/log-redaction.js +88 -0
  277. package/dist/log-redaction.js.map +1 -0
  278. package/dist/login-capability.d.ts +88 -0
  279. package/dist/login-capability.d.ts.map +1 -0
  280. package/dist/login-capability.js +78 -0
  281. package/dist/login-capability.js.map +1 -0
  282. package/dist/login-capability.test.d.ts +2 -0
  283. package/dist/login-capability.test.d.ts.map +1 -0
  284. package/dist/login-capability.test.js +78 -0
  285. package/dist/login-capability.test.js.map +1 -0
  286. package/dist/login-pty-transport.d.ts +80 -0
  287. package/dist/login-pty-transport.d.ts.map +1 -0
  288. package/dist/login-pty-transport.js +75 -0
  289. package/dist/login-pty-transport.js.map +1 -0
  290. package/dist/login-pty-transport.test.d.ts +2 -0
  291. package/dist/login-pty-transport.test.d.ts.map +1 -0
  292. package/dist/login-pty-transport.test.js +185 -0
  293. package/dist/login-pty-transport.test.js.map +1 -0
  294. package/dist/login-runner-lifecycle.d.ts +70 -0
  295. package/dist/login-runner-lifecycle.d.ts.map +1 -0
  296. package/dist/login-runner-lifecycle.js +52 -0
  297. package/dist/login-runner-lifecycle.js.map +1 -0
  298. package/dist/login-runner-lifecycle.test.d.ts +2 -0
  299. package/dist/login-runner-lifecycle.test.d.ts.map +1 -0
  300. package/dist/login-runner-lifecycle.test.js +122 -0
  301. package/dist/login-runner-lifecycle.test.js.map +1 -0
  302. package/dist/mcp-isolation.integration.test.d.ts +2 -0
  303. package/dist/mcp-isolation.integration.test.d.ts.map +1 -0
  304. package/dist/mcp-isolation.integration.test.js +202 -0
  305. package/dist/mcp-isolation.integration.test.js.map +1 -0
  306. package/dist/paperclip-runner-permissions.d.ts +110 -0
  307. package/dist/paperclip-runner-permissions.d.ts.map +1 -0
  308. package/dist/paperclip-runner-permissions.js +150 -0
  309. package/dist/paperclip-runner-permissions.js.map +1 -0
  310. package/dist/paperclip-runner-permissions.test.d.ts +2 -0
  311. package/dist/paperclip-runner-permissions.test.d.ts.map +1 -0
  312. package/dist/paperclip-runner-permissions.test.js +47 -0
  313. package/dist/paperclip-runner-permissions.test.js.map +1 -0
  314. package/dist/published-exports.test.d.ts +2 -0
  315. package/dist/published-exports.test.d.ts.map +1 -0
  316. package/dist/published-exports.test.js +17 -0
  317. package/dist/published-exports.test.js.map +1 -0
  318. package/dist/remote-execution-env.d.ts +2 -0
  319. package/dist/remote-execution-env.d.ts.map +1 -0
  320. package/dist/remote-execution-env.js +46 -0
  321. package/dist/remote-execution-env.js.map +1 -0
  322. package/dist/remote-managed-runtime.d.ts +45 -0
  323. package/dist/remote-managed-runtime.d.ts.map +1 -0
  324. package/dist/remote-managed-runtime.js +183 -0
  325. package/dist/remote-managed-runtime.js.map +1 -0
  326. package/dist/remote-managed-runtime.test.d.ts +2 -0
  327. package/dist/remote-managed-runtime.test.d.ts.map +1 -0
  328. package/dist/remote-managed-runtime.test.js +294 -0
  329. package/dist/remote-managed-runtime.test.js.map +1 -0
  330. package/dist/runner-connectivity.d.ts +61 -0
  331. package/dist/runner-connectivity.d.ts.map +1 -0
  332. package/dist/runner-connectivity.js +81 -0
  333. package/dist/runner-connectivity.js.map +1 -0
  334. package/dist/runner-connectivity.test.d.ts +2 -0
  335. package/dist/runner-connectivity.test.d.ts.map +1 -0
  336. package/dist/runner-connectivity.test.js +183 -0
  337. package/dist/runner-connectivity.test.js.map +1 -0
  338. package/dist/runtime-progress.d.ts +48 -0
  339. package/dist/runtime-progress.d.ts.map +1 -0
  340. package/dist/runtime-progress.js +99 -0
  341. package/dist/runtime-progress.js.map +1 -0
  342. package/dist/runtime-progress.test.d.ts +2 -0
  343. package/dist/runtime-progress.test.d.ts.map +1 -0
  344. package/dist/runtime-progress.test.js +240 -0
  345. package/dist/runtime-progress.test.js.map +1 -0
  346. package/dist/sandbox-callback-bridge-body.d.ts +12 -0
  347. package/dist/sandbox-callback-bridge-body.d.ts.map +1 -0
  348. package/dist/sandbox-callback-bridge-body.js +39 -0
  349. package/dist/sandbox-callback-bridge-body.js.map +1 -0
  350. package/dist/sandbox-callback-bridge-body.test.d.ts +2 -0
  351. package/dist/sandbox-callback-bridge-body.test.d.ts.map +1 -0
  352. package/dist/sandbox-callback-bridge-body.test.js +31 -0
  353. package/dist/sandbox-callback-bridge-body.test.js.map +1 -0
  354. package/dist/sandbox-callback-bridge.d.ts +268 -0
  355. package/dist/sandbox-callback-bridge.d.ts.map +1 -0
  356. package/dist/sandbox-callback-bridge.js +2357 -0
  357. package/dist/sandbox-callback-bridge.js.map +1 -0
  358. package/dist/sandbox-callback-bridge.test.d.ts +2 -0
  359. package/dist/sandbox-callback-bridge.test.d.ts.map +1 -0
  360. package/dist/sandbox-callback-bridge.test.js +3253 -0
  361. package/dist/sandbox-callback-bridge.test.js.map +1 -0
  362. package/dist/sandbox-file-sync.test.d.ts +2 -0
  363. package/dist/sandbox-file-sync.test.d.ts.map +1 -0
  364. package/dist/sandbox-file-sync.test.js +419 -0
  365. package/dist/sandbox-file-sync.test.js.map +1 -0
  366. package/dist/sandbox-install-command.d.ts +2 -0
  367. package/dist/sandbox-install-command.d.ts.map +1 -0
  368. package/dist/sandbox-install-command.js +47 -0
  369. package/dist/sandbox-install-command.js.map +1 -0
  370. package/dist/sandbox-install-command.test.d.ts +2 -0
  371. package/dist/sandbox-install-command.test.d.ts.map +1 -0
  372. package/dist/sandbox-install-command.test.js +22 -0
  373. package/dist/sandbox-install-command.test.js.map +1 -0
  374. package/dist/sandbox-managed-runtime.d.ts +417 -0
  375. package/dist/sandbox-managed-runtime.d.ts.map +1 -0
  376. package/dist/sandbox-managed-runtime.js +1337 -0
  377. package/dist/sandbox-managed-runtime.js.map +1 -0
  378. package/dist/sandbox-managed-runtime.test.d.ts +2 -0
  379. package/dist/sandbox-managed-runtime.test.d.ts.map +1 -0
  380. package/dist/sandbox-managed-runtime.test.js +3746 -0
  381. package/dist/sandbox-managed-runtime.test.js.map +1 -0
  382. package/dist/sandbox-run-log-stream.d.ts +44 -0
  383. package/dist/sandbox-run-log-stream.d.ts.map +1 -0
  384. package/dist/sandbox-run-log-stream.js +203 -0
  385. package/dist/sandbox-run-log-stream.js.map +1 -0
  386. package/dist/sandbox-shell.d.ts +3 -0
  387. package/dist/sandbox-shell.d.ts.map +1 -0
  388. package/dist/sandbox-shell.js +7 -0
  389. package/dist/sandbox-shell.js.map +1 -0
  390. package/dist/server-utils-env.test.d.ts +2 -0
  391. package/dist/server-utils-env.test.d.ts.map +1 -0
  392. package/dist/server-utils-env.test.js +15 -0
  393. package/dist/server-utils-env.test.js.map +1 -0
  394. package/dist/server-utils.d.ts +617 -0
  395. package/dist/server-utils.d.ts.map +1 -0
  396. package/dist/server-utils.js +3258 -0
  397. package/dist/server-utils.js.map +1 -0
  398. package/dist/server-utils.test.d.ts +2 -0
  399. package/dist/server-utils.test.d.ts.map +1 -0
  400. package/dist/server-utils.test.js +3047 -0
  401. package/dist/server-utils.test.js.map +1 -0
  402. package/dist/session-compaction.d.ts +25 -0
  403. package/dist/session-compaction.d.ts.map +1 -0
  404. package/dist/session-compaction.js +165 -0
  405. package/dist/session-compaction.js.map +1 -0
  406. package/dist/skill-library-manifest.d.ts +6 -0
  407. package/dist/skill-library-manifest.d.ts.map +1 -0
  408. package/dist/skill-library-manifest.js +53 -0
  409. package/dist/skill-library-manifest.js.map +1 -0
  410. package/dist/skill-library-manifest.test.d.ts +2 -0
  411. package/dist/skill-library-manifest.test.d.ts.map +1 -0
  412. package/dist/skill-library-manifest.test.js +84 -0
  413. package/dist/skill-library-manifest.test.js.map +1 -0
  414. package/dist/skills-staging-follow-symlinks.test.d.ts +2 -0
  415. package/dist/skills-staging-follow-symlinks.test.d.ts.map +1 -0
  416. package/dist/skills-staging-follow-symlinks.test.js +126 -0
  417. package/dist/skills-staging-follow-symlinks.test.js.map +1 -0
  418. package/dist/ssh-fixture.test.d.ts +2 -0
  419. package/dist/ssh-fixture.test.d.ts.map +1 -0
  420. package/dist/ssh-fixture.test.js +832 -0
  421. package/dist/ssh-fixture.test.js.map +1 -0
  422. package/dist/ssh.d.ts +124 -0
  423. package/dist/ssh.d.ts.map +1 -0
  424. package/dist/ssh.js +1667 -0
  425. package/dist/ssh.js.map +1 -0
  426. package/dist/sync-operation-schedule.d.ts +17 -0
  427. package/dist/sync-operation-schedule.d.ts.map +1 -0
  428. package/dist/sync-operation-schedule.js +66 -0
  429. package/dist/sync-operation-schedule.js.map +1 -0
  430. package/dist/sync-operation-schedule.test.d.ts +2 -0
  431. package/dist/sync-operation-schedule.test.d.ts.map +1 -0
  432. package/dist/sync-operation-schedule.test.js +130 -0
  433. package/dist/sync-operation-schedule.test.js.map +1 -0
  434. package/dist/test-support/mcp-isolation-harness.d.ts +23 -0
  435. package/dist/test-support/mcp-isolation-harness.d.ts.map +1 -0
  436. package/dist/test-support/mcp-isolation-harness.js +67 -0
  437. package/dist/test-support/mcp-isolation-harness.js.map +1 -0
  438. package/dist/types.d.ts +780 -0
  439. package/dist/types.d.ts.map +1 -0
  440. package/dist/types.js +5 -0
  441. package/dist/types.js.map +1 -0
  442. package/dist/workspace-restore-merge.d.ts +77 -0
  443. package/dist/workspace-restore-merge.d.ts.map +1 -0
  444. package/dist/workspace-restore-merge.js +396 -0
  445. package/dist/workspace-restore-merge.js.map +1 -0
  446. package/dist/workspace-restore-merge.test.d.ts +2 -0
  447. package/dist/workspace-restore-merge.test.d.ts.map +1 -0
  448. package/dist/workspace-restore-merge.test.js +438 -0
  449. package/dist/workspace-restore-merge.test.js.map +1 -0
  450. package/dist/workspace-restore-teardown.d.ts +29 -0
  451. package/dist/workspace-restore-teardown.d.ts.map +1 -0
  452. package/dist/workspace-restore-teardown.js +34 -0
  453. package/dist/workspace-restore-teardown.js.map +1 -0
  454. package/dist/workspace-restore-teardown.test.d.ts +2 -0
  455. package/dist/workspace-restore-teardown.test.d.ts.map +1 -0
  456. package/dist/workspace-restore-teardown.test.js +71 -0
  457. package/dist/workspace-restore-teardown.test.js.map +1 -0
  458. package/node_modules/@agentclientprotocol/sdk/LICENSE +191 -0
  459. package/node_modules/@agentclientprotocol/sdk/README.md +70 -0
  460. package/node_modules/@agentclientprotocol/sdk/dist/acp.d.ts +1803 -0
  461. package/node_modules/@agentclientprotocol/sdk/dist/acp.js +1869 -0
  462. package/node_modules/@agentclientprotocol/sdk/dist/acp.js.map +1 -0
  463. package/node_modules/@agentclientprotocol/sdk/dist/acp.test.d.ts +1 -0
  464. package/node_modules/@agentclientprotocol/sdk/dist/acp.test.js +5036 -0
  465. package/node_modules/@agentclientprotocol/sdk/dist/acp.test.js.map +1 -0
  466. package/node_modules/@agentclientprotocol/sdk/dist/connection.d.ts +89 -0
  467. package/node_modules/@agentclientprotocol/sdk/dist/connection.js +490 -0
  468. package/node_modules/@agentclientprotocol/sdk/dist/connection.js.map +1 -0
  469. package/node_modules/@agentclientprotocol/sdk/dist/connection.test.d.ts +1 -0
  470. package/node_modules/@agentclientprotocol/sdk/dist/connection.test.js +507 -0
  471. package/node_modules/@agentclientprotocol/sdk/dist/connection.test.js.map +1 -0
  472. package/node_modules/@agentclientprotocol/sdk/dist/cookie-store.d.ts +26 -0
  473. package/node_modules/@agentclientprotocol/sdk/dist/cookie-store.js +96 -0
  474. package/node_modules/@agentclientprotocol/sdk/dist/cookie-store.js.map +1 -0
  475. package/node_modules/@agentclientprotocol/sdk/dist/cookie-store.test.d.ts +1 -0
  476. package/node_modules/@agentclientprotocol/sdk/dist/cookie-store.test.js +77 -0
  477. package/node_modules/@agentclientprotocol/sdk/dist/cookie-store.test.js.map +1 -0
  478. package/node_modules/@agentclientprotocol/sdk/dist/examples/agent.d.ts +2 -0
  479. package/node_modules/@agentclientprotocol/sdk/dist/examples/agent.js +232 -0
  480. package/node_modules/@agentclientprotocol/sdk/dist/examples/agent.js.map +1 -0
  481. package/node_modules/@agentclientprotocol/sdk/dist/examples/client.d.ts +2 -0
  482. package/node_modules/@agentclientprotocol/sdk/dist/examples/client.js +132 -0
  483. package/node_modules/@agentclientprotocol/sdk/dist/examples/client.js.map +1 -0
  484. package/node_modules/@agentclientprotocol/sdk/dist/examples/dual-version-agent.d.ts +9 -0
  485. package/node_modules/@agentclientprotocol/sdk/dist/examples/dual-version-agent.js +221 -0
  486. package/node_modules/@agentclientprotocol/sdk/dist/examples/dual-version-agent.js.map +1 -0
  487. package/node_modules/@agentclientprotocol/sdk/dist/examples/http-client.d.ts +2 -0
  488. package/node_modules/@agentclientprotocol/sdk/dist/examples/http-client.js +86 -0
  489. package/node_modules/@agentclientprotocol/sdk/dist/examples/http-client.js.map +1 -0
  490. package/node_modules/@agentclientprotocol/sdk/dist/examples/http-server.d.ts +2 -0
  491. package/node_modules/@agentclientprotocol/sdk/dist/examples/http-server.js +112 -0
  492. package/node_modules/@agentclientprotocol/sdk/dist/examples/http-server.js.map +1 -0
  493. package/node_modules/@agentclientprotocol/sdk/dist/examples/ws-client.d.ts +2 -0
  494. package/node_modules/@agentclientprotocol/sdk/dist/examples/ws-client.js +88 -0
  495. package/node_modules/@agentclientprotocol/sdk/dist/examples/ws-client.js.map +1 -0
  496. package/node_modules/@agentclientprotocol/sdk/dist/http-stream.d.ts +33 -0
  497. package/node_modules/@agentclientprotocol/sdk/dist/http-stream.js +435 -0
  498. package/node_modules/@agentclientprotocol/sdk/dist/http-stream.js.map +1 -0
  499. package/node_modules/@agentclientprotocol/sdk/dist/http-stream.test.d.ts +1 -0
  500. package/node_modules/@agentclientprotocol/sdk/dist/http-stream.test.js +1380 -0
  501. package/node_modules/@agentclientprotocol/sdk/dist/http-stream.test.js.map +1 -0
  502. package/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.d.ts +603 -0
  503. package/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js +1076 -0
  504. package/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js.map +1 -0
  505. package/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.test.d.ts +1 -0
  506. package/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.test.js +917 -0
  507. package/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.test.js.map +1 -0
  508. package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.d.ts +22 -0
  509. package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.js +64 -0
  510. package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.js.map +1 -0
  511. package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.test.d.ts +1 -0
  512. package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.test.js +45 -0
  513. package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.test.js.map +1 -0
  514. package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.d.ts +17 -0
  515. package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.js +426 -0
  516. package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.js.map +1 -0
  517. package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.test.d.ts +1 -0
  518. package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.test.js +499 -0
  519. package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.test.js.map +1 -0
  520. package/node_modules/@agentclientprotocol/sdk/dist/protocol-router.d.ts +37 -0
  521. package/node_modules/@agentclientprotocol/sdk/dist/protocol-router.js +474 -0
  522. package/node_modules/@agentclientprotocol/sdk/dist/protocol-router.js.map +1 -0
  523. package/node_modules/@agentclientprotocol/sdk/dist/protocol-router.test.d.ts +1 -0
  524. package/node_modules/@agentclientprotocol/sdk/dist/protocol-router.test.js +535 -0
  525. package/node_modules/@agentclientprotocol/sdk/dist/protocol-router.test.js.map +1 -0
  526. package/node_modules/@agentclientprotocol/sdk/dist/protocol.d.ts +11 -0
  527. package/node_modules/@agentclientprotocol/sdk/dist/protocol.js +69 -0
  528. package/node_modules/@agentclientprotocol/sdk/dist/protocol.js.map +1 -0
  529. package/node_modules/@agentclientprotocol/sdk/dist/protocol.test.d.ts +1 -0
  530. package/node_modules/@agentclientprotocol/sdk/dist/protocol.test.js +82 -0
  531. package/node_modules/@agentclientprotocol/sdk/dist/protocol.test.js.map +1 -0
  532. package/node_modules/@agentclientprotocol/sdk/dist/schema/guards.gen.d.ts +183 -0
  533. package/node_modules/@agentclientprotocol/sdk/dist/schema/guards.gen.js +207 -0
  534. package/node_modules/@agentclientprotocol/sdk/dist/schema/guards.gen.js.map +1 -0
  535. package/node_modules/@agentclientprotocol/sdk/dist/schema/index.d.ts +51 -0
  536. package/node_modules/@agentclientprotocol/sdk/dist/schema/index.js +52 -0
  537. package/node_modules/@agentclientprotocol/sdk/dist/schema/index.js.map +1 -0
  538. package/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.d.ts +6219 -0
  539. package/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.js +3 -0
  540. package/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.js.map +1 -0
  541. package/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.d.ts +15215 -0
  542. package/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js +3084 -0
  543. package/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js.map +1 -0
  544. package/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.d.ts +30 -0
  545. package/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.js +98 -0
  546. package/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.js.map +1 -0
  547. package/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.test.d.ts +1 -0
  548. package/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.test.js +93 -0
  549. package/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.test.js.map +1 -0
  550. package/node_modules/@agentclientprotocol/sdk/dist/server-permission.test.d.ts +1 -0
  551. package/node_modules/@agentclientprotocol/sdk/dist/server-permission.test.js +261 -0
  552. package/node_modules/@agentclientprotocol/sdk/dist/server-permission.test.js.map +1 -0
  553. package/node_modules/@agentclientprotocol/sdk/dist/server-session-sse.test.d.ts +1 -0
  554. package/node_modules/@agentclientprotocol/sdk/dist/server-session-sse.test.js +536 -0
  555. package/node_modules/@agentclientprotocol/sdk/dist/server-session-sse.test.js.map +1 -0
  556. package/node_modules/@agentclientprotocol/sdk/dist/server-sse.d.ts +2 -0
  557. package/node_modules/@agentclientprotocol/sdk/dist/server-sse.js +88 -0
  558. package/node_modules/@agentclientprotocol/sdk/dist/server-sse.js.map +1 -0
  559. package/node_modules/@agentclientprotocol/sdk/dist/server-sse.test.d.ts +1 -0
  560. package/node_modules/@agentclientprotocol/sdk/dist/server-sse.test.js +53 -0
  561. package/node_modules/@agentclientprotocol/sdk/dist/server-sse.test.js.map +1 -0
  562. package/node_modules/@agentclientprotocol/sdk/dist/server-websocket-upgrade.test.d.ts +1 -0
  563. package/node_modules/@agentclientprotocol/sdk/dist/server-websocket-upgrade.test.js +992 -0
  564. package/node_modules/@agentclientprotocol/sdk/dist/server-websocket-upgrade.test.js.map +1 -0
  565. package/node_modules/@agentclientprotocol/sdk/dist/server.d.ts +81 -0
  566. package/node_modules/@agentclientprotocol/sdk/dist/server.js +386 -0
  567. package/node_modules/@agentclientprotocol/sdk/dist/server.js.map +1 -0
  568. package/node_modules/@agentclientprotocol/sdk/dist/server.test.d.ts +1 -0
  569. package/node_modules/@agentclientprotocol/sdk/dist/server.test.js +1116 -0
  570. package/node_modules/@agentclientprotocol/sdk/dist/server.test.js.map +1 -0
  571. package/node_modules/@agentclientprotocol/sdk/dist/sse.d.ts +4 -0
  572. package/node_modules/@agentclientprotocol/sdk/dist/sse.js +91 -0
  573. package/node_modules/@agentclientprotocol/sdk/dist/sse.js.map +1 -0
  574. package/node_modules/@agentclientprotocol/sdk/dist/sse.test.d.ts +1 -0
  575. package/node_modules/@agentclientprotocol/sdk/dist/sse.test.js +126 -0
  576. package/node_modules/@agentclientprotocol/sdk/dist/sse.test.js.map +1 -0
  577. package/node_modules/@agentclientprotocol/sdk/dist/stream.d.ts +35 -0
  578. package/node_modules/@agentclientprotocol/sdk/dist/stream.js +115 -0
  579. package/node_modules/@agentclientprotocol/sdk/dist/stream.js.map +1 -0
  580. package/node_modules/@agentclientprotocol/sdk/dist/stream.test.d.ts +1 -0
  581. package/node_modules/@agentclientprotocol/sdk/dist/stream.test.js +195 -0
  582. package/node_modules/@agentclientprotocol/sdk/dist/stream.test.js.map +1 -0
  583. package/node_modules/@agentclientprotocol/sdk/dist/test-support/streams.d.ts +8 -0
  584. package/node_modules/@agentclientprotocol/sdk/dist/test-support/streams.js +51 -0
  585. package/node_modules/@agentclientprotocol/sdk/dist/test-support/streams.js.map +1 -0
  586. package/node_modules/@agentclientprotocol/sdk/dist/test-support/test-agent.d.ts +24 -0
  587. package/node_modules/@agentclientprotocol/sdk/dist/test-support/test-agent.js +163 -0
  588. package/node_modules/@agentclientprotocol/sdk/dist/test-support/test-agent.js.map +1 -0
  589. package/node_modules/@agentclientprotocol/sdk/dist/test-support/test-http-server.d.ts +9 -0
  590. package/node_modules/@agentclientprotocol/sdk/dist/test-support/test-http-server.js +74 -0
  591. package/node_modules/@agentclientprotocol/sdk/dist/test-support/test-http-server.js.map +1 -0
  592. package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.d.ts +957 -0
  593. package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.js +1484 -0
  594. package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.js.map +1 -0
  595. package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.test.d.ts +1 -0
  596. package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.test.js +1287 -0
  597. package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.test.js.map +1 -0
  598. package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/guards.gen.d.ts +946 -0
  599. package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/guards.gen.js +1046 -0
  600. package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/guards.gen.js.map +1 -0
  601. package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/index.d.ts +42 -0
  602. package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/index.js +43 -0
  603. package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/index.js.map +1 -0
  604. package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/types.gen.d.ts +6208 -0
  605. package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/types.gen.js +3 -0
  606. package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/types.gen.js.map +1 -0
  607. package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/zod.gen.d.ts +33267 -0
  608. package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/zod.gen.js +3199 -0
  609. package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/zod.gen.js.map +1 -0
  610. package/node_modules/@agentclientprotocol/sdk/dist/ws-server.d.ts +14 -0
  611. package/node_modules/@agentclientprotocol/sdk/dist/ws-server.js +357 -0
  612. package/node_modules/@agentclientprotocol/sdk/dist/ws-server.js.map +1 -0
  613. package/node_modules/@agentclientprotocol/sdk/dist/ws-stream.d.ts +54 -0
  614. package/node_modules/@agentclientprotocol/sdk/dist/ws-stream.js +253 -0
  615. package/node_modules/@agentclientprotocol/sdk/dist/ws-stream.js.map +1 -0
  616. package/node_modules/@agentclientprotocol/sdk/dist/ws-stream.test.d.ts +1 -0
  617. package/node_modules/@agentclientprotocol/sdk/dist/ws-stream.test.js +836 -0
  618. package/node_modules/@agentclientprotocol/sdk/dist/ws-stream.test.js.map +1 -0
  619. package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.d.ts +13 -0
  620. package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.js +78 -0
  621. package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.js.map +1 -0
  622. package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.test.d.ts +1 -0
  623. package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.test.js +42 -0
  624. package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.test.js.map +1 -0
  625. package/node_modules/@agentclientprotocol/sdk/package.json +104 -0
  626. package/node_modules/@agentclientprotocol/sdk/schema/schema.json +8967 -0
  627. package/node_modules/@agentclientprotocol/sdk/schema/v2/schema.unstable.json +10242 -0
  628. package/node_modules/@clack/core/LICENSE +9 -0
  629. package/node_modules/@clack/core/README.md +22 -0
  630. package/node_modules/@clack/core/dist/index.d.mts +503 -0
  631. package/node_modules/@clack/core/dist/index.mjs +1003 -0
  632. package/node_modules/@clack/core/package.json +63 -0
  633. package/node_modules/@clack/prompts/LICENSE +9 -0
  634. package/node_modules/@clack/prompts/README.md +396 -0
  635. package/node_modules/@clack/prompts/dist/index.d.mts +949 -0
  636. package/node_modules/@clack/prompts/dist/index.mjs +1423 -0
  637. package/node_modules/@clack/prompts/package.json +67 -0
  638. package/node_modules/@esbuild/linux-x64/README.md +3 -0
  639. package/node_modules/@esbuild/linux-x64/bin/esbuild +0 -0
  640. package/node_modules/@esbuild/linux-x64/package.json +20 -0
  641. package/node_modules/acpx/LICENSE +21 -0
  642. package/node_modules/acpx/README.md +424 -0
  643. package/node_modules/acpx/dist/cli-CxsS1XQp.js +197 -0
  644. package/node_modules/acpx/dist/cli-CxsS1XQp.js.map +1 -0
  645. package/node_modules/acpx/dist/cli.d.ts +14 -0
  646. package/node_modules/acpx/dist/cli.d.ts.map +1 -0
  647. package/node_modules/acpx/dist/cli.js +2683 -0
  648. package/node_modules/acpx/dist/cli.js.map +1 -0
  649. package/node_modules/acpx/dist/client-DwNyFauq.d.ts +192 -0
  650. package/node_modules/acpx/dist/client-DwNyFauq.d.ts.map +1 -0
  651. package/node_modules/acpx/dist/flags-Bjq_XQ4-.js +260 -0
  652. package/node_modules/acpx/dist/flags-Bjq_XQ4-.js.map +1 -0
  653. package/node_modules/acpx/dist/flows-Db_LZKu9.js +1920 -0
  654. package/node_modules/acpx/dist/flows-Db_LZKu9.js.map +1 -0
  655. package/node_modules/acpx/dist/flows.d.ts +318 -0
  656. package/node_modules/acpx/dist/flows.d.ts.map +1 -0
  657. package/node_modules/acpx/dist/flows.js +2 -0
  658. package/node_modules/acpx/dist/live-checkpoint-ClPCSdrW.js +6210 -0
  659. package/node_modules/acpx/dist/live-checkpoint-ClPCSdrW.js.map +1 -0
  660. package/node_modules/acpx/dist/output-CjdF5rHk.js +4593 -0
  661. package/node_modules/acpx/dist/output-CjdF5rHk.js.map +1 -0
  662. package/node_modules/acpx/dist/runtime.d.ts +472 -0
  663. package/node_modules/acpx/dist/runtime.d.ts.map +1 -0
  664. package/node_modules/acpx/dist/runtime.js +1711 -0
  665. package/node_modules/acpx/dist/runtime.js.map +1 -0
  666. package/node_modules/acpx/dist/session-options-jkYbBxGE.d.ts +278 -0
  667. package/node_modules/acpx/dist/session-options-jkYbBxGE.d.ts.map +1 -0
  668. package/node_modules/acpx/package.json +129 -0
  669. package/node_modules/acpx/skills/acpx/SKILL.md +617 -0
  670. package/node_modules/b4a/LICENSE +201 -0
  671. package/node_modules/b4a/README.md +153 -0
  672. package/node_modules/b4a/browser.js +546 -0
  673. package/node_modules/b4a/index.d.ts +144 -0
  674. package/node_modules/b4a/index.js +193 -0
  675. package/node_modules/b4a/lib/ascii.js +31 -0
  676. package/node_modules/b4a/lib/base64.js +64 -0
  677. package/node_modules/b4a/lib/hex.js +51 -0
  678. package/node_modules/b4a/lib/latin1.js +31 -0
  679. package/node_modules/b4a/lib/utf16le.js +40 -0
  680. package/node_modules/b4a/lib/utf8.js +144 -0
  681. package/node_modules/b4a/package.json +54 -0
  682. package/node_modules/b4a/react-native.js +5 -0
  683. package/node_modules/b4a/runtime.d.ts +14 -0
  684. package/node_modules/bare-events/LICENSE +201 -0
  685. package/node_modules/bare-events/README.md +25 -0
  686. package/node_modules/bare-events/global.d.ts +15 -0
  687. package/node_modules/bare-events/global.js +5 -0
  688. package/node_modules/bare-events/index.d.ts +89 -0
  689. package/node_modules/bare-events/index.js +397 -0
  690. package/node_modules/bare-events/lib/errors.js +23 -0
  691. package/node_modules/bare-events/package.json +63 -0
  692. package/node_modules/bare-events/web.d.ts +79 -0
  693. package/node_modules/bare-events/web.js +417 -0
  694. package/node_modules/bare-fs/CMakeLists.txt +13 -0
  695. package/node_modules/bare-fs/LICENSE +201 -0
  696. package/node_modules/bare-fs/README.md +33 -0
  697. package/node_modules/bare-fs/binding.c +2913 -0
  698. package/node_modules/bare-fs/binding.js +1 -0
  699. package/node_modules/bare-fs/index.d.ts +1502 -0
  700. package/node_modules/bare-fs/index.js +2855 -0
  701. package/node_modules/bare-fs/lib/constants.d.ts +50 -0
  702. package/node_modules/bare-fs/lib/constants.js +49 -0
  703. package/node_modules/bare-fs/lib/errors.d.ts +13 -0
  704. package/node_modules/bare-fs/lib/errors.js +55 -0
  705. package/node_modules/bare-fs/package.json +74 -0
  706. package/node_modules/bare-fs/prebuilds/android-arm/bare-fs.bare +0 -0
  707. package/node_modules/bare-fs/prebuilds/android-arm64/bare-fs.bare +0 -0
  708. package/node_modules/bare-fs/prebuilds/android-ia32/bare-fs.bare +0 -0
  709. package/node_modules/bare-fs/prebuilds/android-x64/bare-fs.bare +0 -0
  710. package/node_modules/bare-fs/prebuilds/darwin-arm64/bare-fs.bare +0 -0
  711. package/node_modules/bare-fs/prebuilds/darwin-x64/bare-fs.bare +0 -0
  712. package/node_modules/bare-fs/prebuilds/ios-arm64/bare-fs.bare +0 -0
  713. package/node_modules/bare-fs/prebuilds/ios-arm64-simulator/bare-fs.bare +0 -0
  714. package/node_modules/bare-fs/prebuilds/ios-x64-simulator/bare-fs.bare +0 -0
  715. package/node_modules/bare-fs/prebuilds/linux-arm64/bare-fs.bare +0 -0
  716. package/node_modules/bare-fs/prebuilds/linux-x64/bare-fs.bare +0 -0
  717. package/node_modules/bare-fs/prebuilds/win32-arm64/bare-fs.bare +0 -0
  718. package/node_modules/bare-fs/prebuilds/win32-x64/bare-fs.bare +0 -0
  719. package/node_modules/bare-fs/promises.d.ts +480 -0
  720. package/node_modules/bare-fs/promises.js +119 -0
  721. package/node_modules/bare-path/CMakeLists.txt +13 -0
  722. package/node_modules/bare-path/LICENSE +201 -0
  723. package/node_modules/bare-path/NOTICE +28 -0
  724. package/node_modules/bare-path/README.md +19 -0
  725. package/node_modules/bare-path/binding.c +106 -0
  726. package/node_modules/bare-path/binding.js +1 -0
  727. package/node_modules/bare-path/index.d.ts +27 -0
  728. package/node_modules/bare-path/index.js +9 -0
  729. package/node_modules/bare-path/lib/constants.js +11 -0
  730. package/node_modules/bare-path/lib/posix.js +263 -0
  731. package/node_modules/bare-path/lib/shared.js +81 -0
  732. package/node_modules/bare-path/lib/win32.js +564 -0
  733. package/node_modules/bare-path/package.json +47 -0
  734. package/node_modules/bare-path/prebuilds/android-arm/bare-path.bare +0 -0
  735. package/node_modules/bare-path/prebuilds/android-arm64/bare-path.bare +0 -0
  736. package/node_modules/bare-path/prebuilds/android-ia32/bare-path.bare +0 -0
  737. package/node_modules/bare-path/prebuilds/android-x64/bare-path.bare +0 -0
  738. package/node_modules/bare-path/prebuilds/darwin-arm64/bare-path.bare +0 -0
  739. package/node_modules/bare-path/prebuilds/darwin-x64/bare-path.bare +0 -0
  740. package/node_modules/bare-path/prebuilds/ios-arm64/bare-path.bare +0 -0
  741. package/node_modules/bare-path/prebuilds/ios-arm64-simulator/bare-path.bare +0 -0
  742. package/node_modules/bare-path/prebuilds/ios-x64-simulator/bare-path.bare +0 -0
  743. package/node_modules/bare-path/prebuilds/linux-arm64/bare-path.bare +0 -0
  744. package/node_modules/bare-path/prebuilds/linux-x64/bare-path.bare +0 -0
  745. package/node_modules/bare-path/prebuilds/win32-arm64/bare-path.bare +0 -0
  746. package/node_modules/bare-path/prebuilds/win32-x64/bare-path.bare +0 -0
  747. package/node_modules/bare-stream/LICENSE +201 -0
  748. package/node_modules/bare-stream/README.md +354 -0
  749. package/node_modules/bare-stream/global.d.ts +31 -0
  750. package/node_modules/bare-stream/global.js +15 -0
  751. package/node_modules/bare-stream/index.d.ts +235 -0
  752. package/node_modules/bare-stream/index.js +493 -0
  753. package/node_modules/bare-stream/package.json +75 -0
  754. package/node_modules/bare-stream/promises.js +3 -0
  755. package/node_modules/bare-stream/web.d.ts +152 -0
  756. package/node_modules/bare-stream/web.js +607 -0
  757. package/node_modules/bare-url/CMakeLists.txt +36 -0
  758. package/node_modules/bare-url/LICENSE +201 -0
  759. package/node_modules/bare-url/README.md +33 -0
  760. package/node_modules/bare-url/binding.c +422 -0
  761. package/node_modules/bare-url/binding.js +1 -0
  762. package/node_modules/bare-url/global.d.ts +24 -0
  763. package/node_modules/bare-url/global.js +2 -0
  764. package/node_modules/bare-url/index.d.ts +96 -0
  765. package/node_modules/bare-url/index.js +676 -0
  766. package/node_modules/bare-url/lib/errors.d.ts +10 -0
  767. package/node_modules/bare-url/lib/errors.js +33 -0
  768. package/node_modules/bare-url/lib/url-search-params.d.ts +66 -0
  769. package/node_modules/bare-url/lib/url-search-params.js +276 -0
  770. package/node_modules/bare-url/package.json +54 -0
  771. package/node_modules/bare-url/prebuilds/android-arm/bare-url.bare +0 -0
  772. package/node_modules/bare-url/prebuilds/android-arm64/bare-url.bare +0 -0
  773. package/node_modules/bare-url/prebuilds/android-ia32/bare-url.bare +0 -0
  774. package/node_modules/bare-url/prebuilds/android-x64/bare-url.bare +0 -0
  775. package/node_modules/bare-url/prebuilds/darwin-arm64/bare-url.bare +0 -0
  776. package/node_modules/bare-url/prebuilds/darwin-x64/bare-url.bare +0 -0
  777. package/node_modules/bare-url/prebuilds/ios-arm64/bare-url.bare +0 -0
  778. package/node_modules/bare-url/prebuilds/ios-arm64-simulator/bare-url.bare +0 -0
  779. package/node_modules/bare-url/prebuilds/ios-x64-simulator/bare-url.bare +0 -0
  780. package/node_modules/bare-url/prebuilds/linux-arm64/bare-url.bare +0 -0
  781. package/node_modules/bare-url/prebuilds/linux-x64/bare-url.bare +0 -0
  782. package/node_modules/bare-url/prebuilds/win32-arm64/bare-url.bare +0 -0
  783. package/node_modules/bare-url/prebuilds/win32-x64/bare-url.bare +0 -0
  784. package/node_modules/commander/LICENSE +22 -0
  785. package/node_modules/commander/Readme.md +1172 -0
  786. package/node_modules/commander/index.js +21 -0
  787. package/node_modules/commander/lib/argument.js +147 -0
  788. package/node_modules/commander/lib/command.js +2790 -0
  789. package/node_modules/commander/lib/error.js +36 -0
  790. package/node_modules/commander/lib/help.js +731 -0
  791. package/node_modules/commander/lib/option.js +377 -0
  792. package/node_modules/commander/lib/suggestSimilar.js +99 -0
  793. package/node_modules/commander/package-support.json +19 -0
  794. package/node_modules/commander/package.json +64 -0
  795. package/node_modules/commander/typings/index.d.ts +1113 -0
  796. package/node_modules/esbuild/LICENSE.md +21 -0
  797. package/node_modules/esbuild/README.md +3 -0
  798. package/node_modules/esbuild/bin/esbuild +223 -0
  799. package/node_modules/esbuild/install.js +300 -0
  800. package/node_modules/esbuild/lib/main.d.ts +720 -0
  801. package/node_modules/esbuild/lib/main.js +2536 -0
  802. package/node_modules/esbuild/package.json +74 -0
  803. package/node_modules/events-universal/LICENSE +201 -0
  804. package/node_modules/events-universal/README.md +17 -0
  805. package/node_modules/events-universal/bare.js +1 -0
  806. package/node_modules/events-universal/default.js +1 -0
  807. package/node_modules/events-universal/index.js +1 -0
  808. package/node_modules/events-universal/package.json +39 -0
  809. package/node_modules/events-universal/react-native.js +1 -0
  810. package/node_modules/fast-fifo/LICENSE +21 -0
  811. package/node_modules/fast-fifo/README.md +78 -0
  812. package/node_modules/fast-fifo/fixed-size.js +39 -0
  813. package/node_modules/fast-fifo/index.js +48 -0
  814. package/node_modules/fast-fifo/package.json +28 -0
  815. package/node_modules/fast-string-truncated-width/dist/index.d.ts +4 -0
  816. package/node_modules/fast-string-truncated-width/dist/index.js +111 -0
  817. package/node_modules/fast-string-truncated-width/dist/types.d.ts +19 -0
  818. package/node_modules/fast-string-truncated-width/dist/types.js +2 -0
  819. package/node_modules/fast-string-truncated-width/dist/utils.d.ts +4 -0
  820. package/node_modules/fast-string-truncated-width/dist/utils.js +20 -0
  821. package/node_modules/fast-string-truncated-width/license +21 -0
  822. package/node_modules/fast-string-truncated-width/package.json +35 -0
  823. package/node_modules/fast-string-truncated-width/readme.md +59 -0
  824. package/node_modules/fast-string-width/dist/index.d.ts +4 -0
  825. package/node_modules/fast-string-width/dist/index.js +14 -0
  826. package/node_modules/fast-string-width/license +21 -0
  827. package/node_modules/fast-string-width/package.json +34 -0
  828. package/node_modules/fast-string-width/readme.md +42 -0
  829. package/node_modules/fast-wrap-ansi/LICENSE +23 -0
  830. package/node_modules/fast-wrap-ansi/README.md +26 -0
  831. package/node_modules/fast-wrap-ansi/lib/main.d.ts +6 -0
  832. package/node_modules/fast-wrap-ansi/lib/main.js +218 -0
  833. package/node_modules/fast-wrap-ansi/package.json +51 -0
  834. package/node_modules/sisteransi/license +21 -0
  835. package/node_modules/sisteransi/package.json +34 -0
  836. package/node_modules/sisteransi/readme.md +113 -0
  837. package/node_modules/sisteransi/src/index.js +58 -0
  838. package/node_modules/sisteransi/src/sisteransi.d.ts +35 -0
  839. package/node_modules/skillflag/LICENSE +21 -0
  840. package/node_modules/skillflag/README.md +44 -0
  841. package/node_modules/skillflag/dist/bin/skill-install.d.ts +3 -0
  842. package/node_modules/skillflag/dist/bin/skill-install.d.ts.map +1 -0
  843. package/node_modules/skillflag/dist/bin/skill-install.js +6 -0
  844. package/node_modules/skillflag/dist/bin/skill-install.js.map +1 -0
  845. package/node_modules/skillflag/dist/bin/skillflag.d.ts +3 -0
  846. package/node_modules/skillflag/dist/bin/skillflag.d.ts.map +1 -0
  847. package/node_modules/skillflag/dist/bin/skillflag.js +25 -0
  848. package/node_modules/skillflag/dist/bin/skillflag.js.map +1 -0
  849. package/node_modules/skillflag/dist/core/digest.d.ts +2 -0
  850. package/node_modules/skillflag/dist/core/digest.d.ts.map +1 -0
  851. package/node_modules/skillflag/dist/core/digest.js +15 -0
  852. package/node_modules/skillflag/dist/core/digest.js.map +1 -0
  853. package/node_modules/skillflag/dist/core/errors.d.ts +6 -0
  854. package/node_modules/skillflag/dist/core/errors.d.ts.map +1 -0
  855. package/node_modules/skillflag/dist/core/errors.js +13 -0
  856. package/node_modules/skillflag/dist/core/errors.js.map +1 -0
  857. package/node_modules/skillflag/dist/core/export.d.ts +2 -0
  858. package/node_modules/skillflag/dist/core/export.d.ts.map +1 -0
  859. package/node_modules/skillflag/dist/core/export.js +28 -0
  860. package/node_modules/skillflag/dist/core/export.js.map +1 -0
  861. package/node_modules/skillflag/dist/core/list.d.ts +21 -0
  862. package/node_modules/skillflag/dist/core/list.d.ts.map +1 -0
  863. package/node_modules/skillflag/dist/core/list.js +71 -0
  864. package/node_modules/skillflag/dist/core/list.js.map +1 -0
  865. package/node_modules/skillflag/dist/core/paths.d.ts +15 -0
  866. package/node_modules/skillflag/dist/core/paths.d.ts.map +1 -0
  867. package/node_modules/skillflag/dist/core/paths.js +149 -0
  868. package/node_modules/skillflag/dist/core/paths.js.map +1 -0
  869. package/node_modules/skillflag/dist/core/show.d.ts +2 -0
  870. package/node_modules/skillflag/dist/core/show.d.ts.map +1 -0
  871. package/node_modules/skillflag/dist/core/show.js +8 -0
  872. package/node_modules/skillflag/dist/core/show.js.map +1 -0
  873. package/node_modules/skillflag/dist/core/skillflag.d.ts +3 -0
  874. package/node_modules/skillflag/dist/core/skillflag.d.ts.map +1 -0
  875. package/node_modules/skillflag/dist/core/skillflag.js +2 -0
  876. package/node_modules/skillflag/dist/core/skillflag.js.map +1 -0
  877. package/node_modules/skillflag/dist/core/tar.d.ts +16 -0
  878. package/node_modules/skillflag/dist/core/tar.d.ts.map +1 -0
  879. package/node_modules/skillflag/dist/core/tar.js +140 -0
  880. package/node_modules/skillflag/dist/core/tar.js.map +1 -0
  881. package/node_modules/skillflag/dist/index.d.ts +5 -0
  882. package/node_modules/skillflag/dist/index.d.ts.map +1 -0
  883. package/node_modules/skillflag/dist/index.js +3 -0
  884. package/node_modules/skillflag/dist/index.js.map +1 -0
  885. package/node_modules/skillflag/dist/install/cli.d.ts +57 -0
  886. package/node_modules/skillflag/dist/install/cli.d.ts.map +1 -0
  887. package/node_modules/skillflag/dist/install/cli.js +653 -0
  888. package/node_modules/skillflag/dist/install/cli.js.map +1 -0
  889. package/node_modules/skillflag/dist/install/copy.d.ts +2 -0
  890. package/node_modules/skillflag/dist/install/copy.d.ts.map +1 -0
  891. package/node_modules/skillflag/dist/install/copy.js +19 -0
  892. package/node_modules/skillflag/dist/install/copy.js.map +1 -0
  893. package/node_modules/skillflag/dist/install/errors.d.ts +6 -0
  894. package/node_modules/skillflag/dist/install/errors.d.ts.map +1 -0
  895. package/node_modules/skillflag/dist/install/errors.js +13 -0
  896. package/node_modules/skillflag/dist/install/errors.js.map +1 -0
  897. package/node_modules/skillflag/dist/install/extract.d.ts +3 -0
  898. package/node_modules/skillflag/dist/install/extract.d.ts.map +1 -0
  899. package/node_modules/skillflag/dist/install/extract.js +76 -0
  900. package/node_modules/skillflag/dist/install/extract.js.map +1 -0
  901. package/node_modules/skillflag/dist/install/install.d.ts +21 -0
  902. package/node_modules/skillflag/dist/install/install.d.ts.map +1 -0
  903. package/node_modules/skillflag/dist/install/install.js +35 -0
  904. package/node_modules/skillflag/dist/install/install.js.map +1 -0
  905. package/node_modules/skillflag/dist/install/resolve.d.ts +12 -0
  906. package/node_modules/skillflag/dist/install/resolve.d.ts.map +1 -0
  907. package/node_modules/skillflag/dist/install/resolve.js +119 -0
  908. package/node_modules/skillflag/dist/install/resolve.js.map +1 -0
  909. package/node_modules/skillflag/dist/install/validate.d.ts +7 -0
  910. package/node_modules/skillflag/dist/install/validate.d.ts.map +1 -0
  911. package/node_modules/skillflag/dist/install/validate.js +28 -0
  912. package/node_modules/skillflag/dist/install/validate.js.map +1 -0
  913. package/node_modules/skillflag/dist/shared/frontmatter.d.ts +2 -0
  914. package/node_modules/skillflag/dist/shared/frontmatter.d.ts.map +1 -0
  915. package/node_modules/skillflag/dist/shared/frontmatter.js +28 -0
  916. package/node_modules/skillflag/dist/shared/frontmatter.js.map +1 -0
  917. package/node_modules/skillflag/dist/skillflag.d.ts +29 -0
  918. package/node_modules/skillflag/dist/skillflag.d.ts.map +1 -0
  919. package/node_modules/skillflag/dist/skillflag.js +230 -0
  920. package/node_modules/skillflag/dist/skillflag.js.map +1 -0
  921. package/node_modules/skillflag/dist/utils/collections.d.ts +3 -0
  922. package/node_modules/skillflag/dist/utils/collections.d.ts.map +1 -0
  923. package/node_modules/skillflag/dist/utils/collections.js +18 -0
  924. package/node_modules/skillflag/dist/utils/collections.js.map +1 -0
  925. package/node_modules/skillflag/package.json +78 -0
  926. package/node_modules/skillflag/skills/skillflag/SKILL.md +144 -0
  927. package/node_modules/streamx/LICENSE +21 -0
  928. package/node_modules/streamx/README.md +550 -0
  929. package/node_modules/streamx/index.d.ts +198 -0
  930. package/node_modules/streamx/index.js +1314 -0
  931. package/node_modules/streamx/lib/errors.d.ts +13 -0
  932. package/node_modules/streamx/lib/errors.js +47 -0
  933. package/node_modules/streamx/package.json +51 -0
  934. package/node_modules/streamx/runtime.d.ts +55 -0
  935. package/node_modules/tar-stream/LICENSE +21 -0
  936. package/node_modules/tar-stream/README.md +183 -0
  937. package/node_modules/tar-stream/constants.js +14 -0
  938. package/node_modules/tar-stream/extract.js +418 -0
  939. package/node_modules/tar-stream/headers.js +322 -0
  940. package/node_modules/tar-stream/index.d.ts +60 -0
  941. package/node_modules/tar-stream/index.js +2 -0
  942. package/node_modules/tar-stream/pack.js +287 -0
  943. package/node_modules/tar-stream/package.json +50 -0
  944. package/node_modules/teex/LICENSE +21 -0
  945. package/node_modules/teex/README.md +31 -0
  946. package/node_modules/teex/example.js +16 -0
  947. package/node_modules/teex/index.js +52 -0
  948. package/node_modules/teex/package.json +26 -0
  949. package/node_modules/teex/test.js +68 -0
  950. package/node_modules/text-decoder/LICENSE +201 -0
  951. package/node_modules/text-decoder/README.md +36 -0
  952. package/node_modules/text-decoder/index.js +64 -0
  953. package/node_modules/text-decoder/lib/pass-through-decoder.js +19 -0
  954. package/node_modules/text-decoder/lib/utf8-decoder.js +185 -0
  955. package/node_modules/text-decoder/package.json +39 -0
  956. package/node_modules/tsx/LICENSE +21 -0
  957. package/node_modules/tsx/README.md +32 -0
  958. package/node_modules/tsx/dist/cjs/api/index.cjs +2 -0
  959. package/node_modules/tsx/dist/cjs/api/index.d.cts +35 -0
  960. package/node_modules/tsx/dist/cjs/api/index.d.mts +35 -0
  961. package/node_modules/tsx/dist/cjs/api/index.mjs +2 -0
  962. package/node_modules/tsx/dist/cjs/index.cjs +1 -0
  963. package/node_modules/tsx/dist/cli.mjs +55 -0
  964. package/node_modules/tsx/dist/client-XItNFmsq.cjs +1 -0
  965. package/node_modules/tsx/dist/esm/api/index.cjs +2 -0
  966. package/node_modules/tsx/dist/esm/api/index.d.cts +37 -0
  967. package/node_modules/tsx/dist/esm/api/index.d.mts +37 -0
  968. package/node_modules/tsx/dist/esm/api/index.mjs +2 -0
  969. package/node_modules/tsx/dist/esm/index.mjs +2 -0
  970. package/node_modules/tsx/dist/index-Bqjv9TxC.mjs +1 -0
  971. package/node_modules/tsx/dist/index-DE3OBZuV.mjs +14 -0
  972. package/node_modules/tsx/dist/lexer-CGfpMDSb.cjs +3 -0
  973. package/node_modules/tsx/dist/lexer-CanA6ArK.mjs +3 -0
  974. package/node_modules/tsx/dist/loader.mjs +2 -0
  975. package/node_modules/tsx/dist/package-Dj0mHsMt.mjs +1 -0
  976. package/node_modules/tsx/dist/patch-repl.cjs +1 -0
  977. package/node_modules/tsx/dist/preflight.cjs +1 -0
  978. package/node_modules/tsx/dist/register-B1c7OH6V.cjs +23 -0
  979. package/node_modules/tsx/dist/register-nyXW-TH3.mjs +2 -0
  980. package/node_modules/tsx/dist/repl.mjs +4 -0
  981. package/node_modules/tsx/dist/require-CBjy4Foe.mjs +10 -0
  982. package/node_modules/tsx/dist/require-CVaYn9z3.cjs +1 -0
  983. package/node_modules/tsx/dist/suppress-warnings.cjs +1 -0
  984. package/node_modules/tsx/dist/temporary-directory-Du7LpLp9.mjs +1 -0
  985. package/node_modules/tsx/package.json +70 -0
  986. package/node_modules/zod/LICENSE +21 -0
  987. package/node_modules/zod/README.md +218 -0
  988. package/node_modules/zod/compile.cjs +96 -0
  989. package/node_modules/zod/compile.d.cts +1 -0
  990. package/node_modules/zod/compile.d.ts +1 -0
  991. package/node_modules/zod/compile.js +52 -0
  992. package/node_modules/zod/index.cjs +52 -0
  993. package/node_modules/zod/index.d.cts +3 -0
  994. package/node_modules/zod/index.d.ts +3 -0
  995. package/node_modules/zod/index.js +7 -0
  996. package/node_modules/zod/locales/index.cjs +36 -0
  997. package/node_modules/zod/locales/index.d.cts +1 -0
  998. package/node_modules/zod/locales/index.d.ts +1 -0
  999. package/node_modules/zod/locales/index.js +1 -0
  1000. package/node_modules/zod/locales/package.json +7 -0
  1001. package/node_modules/zod/mini/index.cjs +51 -0
  1002. package/node_modules/zod/mini/index.d.cts +3 -0
  1003. package/node_modules/zod/mini/index.d.ts +3 -0
  1004. package/node_modules/zod/mini/index.js +3 -0
  1005. package/node_modules/zod/mini/package.json +7 -0
  1006. package/node_modules/zod/package.json +147 -0
  1007. package/node_modules/zod/src/compile.ts +56 -0
  1008. package/node_modules/zod/src/index.ts +7 -0
  1009. package/node_modules/zod/src/locales/index.ts +1 -0
  1010. package/node_modules/zod/src/mini/index.ts +3 -0
  1011. package/node_modules/zod/src/v3/ZodError.ts +340 -0
  1012. package/node_modules/zod/src/v3/benchmarks/datetime.ts +58 -0
  1013. package/node_modules/zod/src/v3/benchmarks/discriminatedUnion.ts +80 -0
  1014. package/node_modules/zod/src/v3/benchmarks/index.ts +59 -0
  1015. package/node_modules/zod/src/v3/benchmarks/ipv4.ts +57 -0
  1016. package/node_modules/zod/src/v3/benchmarks/object.ts +69 -0
  1017. package/node_modules/zod/src/v3/benchmarks/primitives.ts +162 -0
  1018. package/node_modules/zod/src/v3/benchmarks/realworld.ts +63 -0
  1019. package/node_modules/zod/src/v3/benchmarks/string.ts +55 -0
  1020. package/node_modules/zod/src/v3/benchmarks/union.ts +80 -0
  1021. package/node_modules/zod/src/v3/errors.ts +13 -0
  1022. package/node_modules/zod/src/v3/external.ts +6 -0
  1023. package/node_modules/zod/src/v3/helpers/enumUtil.ts +17 -0
  1024. package/node_modules/zod/src/v3/helpers/errorUtil.ts +8 -0
  1025. package/node_modules/zod/src/v3/helpers/parseUtil.ts +176 -0
  1026. package/node_modules/zod/src/v3/helpers/partialUtil.ts +34 -0
  1027. package/node_modules/zod/src/v3/helpers/typeAliases.ts +2 -0
  1028. package/node_modules/zod/src/v3/helpers/util.ts +224 -0
  1029. package/node_modules/zod/src/v3/index.ts +4 -0
  1030. package/node_modules/zod/src/v3/locales/en.ts +124 -0
  1031. package/node_modules/zod/src/v3/standard-schema.ts +113 -0
  1032. package/node_modules/zod/src/v3/tests/Mocker.ts +54 -0
  1033. package/node_modules/zod/src/v3/tests/all-errors.test.ts +157 -0
  1034. package/node_modules/zod/src/v3/tests/anyunknown.test.ts +28 -0
  1035. package/node_modules/zod/src/v3/tests/array.test.ts +71 -0
  1036. package/node_modules/zod/src/v3/tests/async-parsing.test.ts +388 -0
  1037. package/node_modules/zod/src/v3/tests/async-refinements.test.ts +46 -0
  1038. package/node_modules/zod/src/v3/tests/base.test.ts +29 -0
  1039. package/node_modules/zod/src/v3/tests/bigint.test.ts +55 -0
  1040. package/node_modules/zod/src/v3/tests/branded.test.ts +53 -0
  1041. package/node_modules/zod/src/v3/tests/catch.test.ts +220 -0
  1042. package/node_modules/zod/src/v3/tests/coerce.test.ts +133 -0
  1043. package/node_modules/zod/src/v3/tests/complex.test.ts +70 -0
  1044. package/node_modules/zod/src/v3/tests/custom.test.ts +31 -0
  1045. package/node_modules/zod/src/v3/tests/date.test.ts +32 -0
  1046. package/node_modules/zod/src/v3/tests/deepmasking.test.ts +186 -0
  1047. package/node_modules/zod/src/v3/tests/default.test.ts +112 -0
  1048. package/node_modules/zod/src/v3/tests/description.test.ts +33 -0
  1049. package/node_modules/zod/src/v3/tests/discriminated-unions.test.ts +315 -0
  1050. package/node_modules/zod/src/v3/tests/enum.test.ts +80 -0
  1051. package/node_modules/zod/src/v3/tests/error.test.ts +586 -0
  1052. package/node_modules/zod/src/v3/tests/firstparty.test.ts +87 -0
  1053. package/node_modules/zod/src/v3/tests/firstpartyschematypes.test.ts +21 -0
  1054. package/node_modules/zod/src/v3/tests/function.test.ts +261 -0
  1055. package/node_modules/zod/src/v3/tests/generics.test.ts +48 -0
  1056. package/node_modules/zod/src/v3/tests/instanceof.test.ts +37 -0
  1057. package/node_modules/zod/src/v3/tests/intersection.test.ts +128 -0
  1058. package/node_modules/zod/src/v3/tests/language-server.source.ts +76 -0
  1059. package/node_modules/zod/src/v3/tests/language-server.test.ts +206 -0
  1060. package/node_modules/zod/src/v3/tests/literal.test.ts +36 -0
  1061. package/node_modules/zod/src/v3/tests/map.test.ts +110 -0
  1062. package/node_modules/zod/src/v3/tests/masking.test.ts +4 -0
  1063. package/node_modules/zod/src/v3/tests/mocker.test.ts +19 -0
  1064. package/node_modules/zod/src/v3/tests/nan.test.ts +24 -0
  1065. package/node_modules/zod/src/v3/tests/nativeEnum.test.ts +87 -0
  1066. package/node_modules/zod/src/v3/tests/nullable.test.ts +42 -0
  1067. package/node_modules/zod/src/v3/tests/number.test.ts +176 -0
  1068. package/node_modules/zod/src/v3/tests/object-augmentation.test.ts +29 -0
  1069. package/node_modules/zod/src/v3/tests/object-in-es5-env.test.ts +29 -0
  1070. package/node_modules/zod/src/v3/tests/object.test.ts +431 -0
  1071. package/node_modules/zod/src/v3/tests/optional.test.ts +42 -0
  1072. package/node_modules/zod/src/v3/tests/parseUtil.test.ts +23 -0
  1073. package/node_modules/zod/src/v3/tests/parser.test.ts +41 -0
  1074. package/node_modules/zod/src/v3/tests/partials.test.ts +243 -0
  1075. package/node_modules/zod/src/v3/tests/pickomit.test.ts +111 -0
  1076. package/node_modules/zod/src/v3/tests/pipeline.test.ts +29 -0
  1077. package/node_modules/zod/src/v3/tests/preprocess.test.ts +186 -0
  1078. package/node_modules/zod/src/v3/tests/primitive.test.ts +440 -0
  1079. package/node_modules/zod/src/v3/tests/promise.test.ts +90 -0
  1080. package/node_modules/zod/src/v3/tests/readonly.test.ts +114 -0
  1081. package/node_modules/zod/src/v3/tests/record.test.ts +171 -0
  1082. package/node_modules/zod/src/v3/tests/recursive.test.ts +197 -0
  1083. package/node_modules/zod/src/v3/tests/refine.test.ts +313 -0
  1084. package/node_modules/zod/src/v3/tests/safeparse.test.ts +27 -0
  1085. package/node_modules/zod/src/v3/tests/set.test.ts +142 -0
  1086. package/node_modules/zod/src/v3/tests/standard-schema.test.ts +83 -0
  1087. package/node_modules/zod/src/v3/tests/string.test.ts +922 -0
  1088. package/node_modules/zod/src/v3/tests/transformer.test.ts +233 -0
  1089. package/node_modules/zod/src/v3/tests/tuple.test.ts +90 -0
  1090. package/node_modules/zod/src/v3/tests/unions.test.ts +57 -0
  1091. package/node_modules/zod/src/v3/tests/validations.test.ts +133 -0
  1092. package/node_modules/zod/src/v3/tests/void.test.ts +15 -0
  1093. package/node_modules/zod/src/v3/types.ts +5138 -0
  1094. package/node_modules/zod/src/v4/classic/checks.ts +33 -0
  1095. package/node_modules/zod/src/v4/classic/coerce.ts +27 -0
  1096. package/node_modules/zod/src/v4/classic/compat.ts +78 -0
  1097. package/node_modules/zod/src/v4/classic/deep-partial.ts +76 -0
  1098. package/node_modules/zod/src/v4/classic/errors.ts +97 -0
  1099. package/node_modules/zod/src/v4/classic/external.ts +57 -0
  1100. package/node_modules/zod/src/v4/classic/from-json-schema.ts +944 -0
  1101. package/node_modules/zod/src/v4/classic/in-out.ts +45 -0
  1102. package/node_modules/zod/src/v4/classic/index.ts +5 -0
  1103. package/node_modules/zod/src/v4/classic/iso.ts +20 -0
  1104. package/node_modules/zod/src/v4/classic/parse.ts +88 -0
  1105. package/node_modules/zod/src/v4/classic/schemas.ts +2937 -0
  1106. package/node_modules/zod/src/v4/classic/tests/anyunknown.test.ts +26 -0
  1107. package/node_modules/zod/src/v4/classic/tests/apply.test.ts +86 -0
  1108. package/node_modules/zod/src/v4/classic/tests/array.test.ts +264 -0
  1109. package/node_modules/zod/src/v4/classic/tests/assignability.test.ts +402 -0
  1110. package/node_modules/zod/src/v4/classic/tests/async-parsing.test.ts +381 -0
  1111. package/node_modules/zod/src/v4/classic/tests/async-refinements.test.ts +68 -0
  1112. package/node_modules/zod/src/v4/classic/tests/base.test.ts +7 -0
  1113. package/node_modules/zod/src/v4/classic/tests/bigint.test.ts +89 -0
  1114. package/node_modules/zod/src/v4/classic/tests/brand.test.ts +106 -0
  1115. package/node_modules/zod/src/v4/classic/tests/catch.test.ts +604 -0
  1116. package/node_modules/zod/src/v4/classic/tests/coalesce.test.ts +20 -0
  1117. package/node_modules/zod/src/v4/classic/tests/codec-examples.test.ts +572 -0
  1118. package/node_modules/zod/src/v4/classic/tests/codec.test.ts +704 -0
  1119. package/node_modules/zod/src/v4/classic/tests/coerce.test.ts +160 -0
  1120. package/node_modules/zod/src/v4/classic/tests/continuability.test.ts +374 -0
  1121. package/node_modules/zod/src/v4/classic/tests/credit-card.test.ts +76 -0
  1122. package/node_modules/zod/src/v4/classic/tests/custom.test.ts +40 -0
  1123. package/node_modules/zod/src/v4/classic/tests/cyclic-data.test.ts +985 -0
  1124. package/node_modules/zod/src/v4/classic/tests/date.test.ts +92 -0
  1125. package/node_modules/zod/src/v4/classic/tests/datetime.test.ts +316 -0
  1126. package/node_modules/zod/src/v4/classic/tests/deep-partial.test.ts +380 -0
  1127. package/node_modules/zod/src/v4/classic/tests/default-locale.test.ts +32 -0
  1128. package/node_modules/zod/src/v4/classic/tests/default.test.ts +455 -0
  1129. package/node_modules/zod/src/v4/classic/tests/describe-meta-checks.test.ts +27 -0
  1130. package/node_modules/zod/src/v4/classic/tests/description.test.ts +32 -0
  1131. package/node_modules/zod/src/v4/classic/tests/detached-methods.test.ts +221 -0
  1132. package/node_modules/zod/src/v4/classic/tests/discriminated-unions.test.ts +942 -0
  1133. package/node_modules/zod/src/v4/classic/tests/enum.test.ts +305 -0
  1134. package/node_modules/zod/src/v4/classic/tests/error-utils.test.ts +917 -0
  1135. package/node_modules/zod/src/v4/classic/tests/error.test.ts +1108 -0
  1136. package/node_modules/zod/src/v4/classic/tests/file.test.ts +96 -0
  1137. package/node_modules/zod/src/v4/classic/tests/firstparty.test.ts +179 -0
  1138. package/node_modules/zod/src/v4/classic/tests/fix-json-issue.test.ts +26 -0
  1139. package/node_modules/zod/src/v4/classic/tests/from-json-schema.test.ts +1467 -0
  1140. package/node_modules/zod/src/v4/classic/tests/function.test.ts +391 -0
  1141. package/node_modules/zod/src/v4/classic/tests/generics.test.ts +72 -0
  1142. package/node_modules/zod/src/v4/classic/tests/global-config.test.ts +32 -0
  1143. package/node_modules/zod/src/v4/classic/tests/hash.test.ts +67 -0
  1144. package/node_modules/zod/src/v4/classic/tests/iban.test.ts +81 -0
  1145. package/node_modules/zod/src/v4/classic/tests/in-out.test.ts +159 -0
  1146. package/node_modules/zod/src/v4/classic/tests/index.test.ts +958 -0
  1147. package/node_modules/zod/src/v4/classic/tests/instance-footprint.test.ts +319 -0
  1148. package/node_modules/zod/src/v4/classic/tests/instanceof.test.ts +127 -0
  1149. package/node_modules/zod/src/v4/classic/tests/intersection.test.ts +334 -0
  1150. package/node_modules/zod/src/v4/classic/tests/issue-schema.test.ts +215 -0
  1151. package/node_modules/zod/src/v4/classic/tests/jitless-allows-eval.test.ts +39 -0
  1152. package/node_modules/zod/src/v4/classic/tests/json.test.ts +109 -0
  1153. package/node_modules/zod/src/v4/classic/tests/lazy.test.ts +262 -0
  1154. package/node_modules/zod/src/v4/classic/tests/literal.test.ts +128 -0
  1155. package/node_modules/zod/src/v4/classic/tests/locales_ka.test.ts +29 -0
  1156. package/node_modules/zod/src/v4/classic/tests/locales_ro.test.ts +24 -0
  1157. package/node_modules/zod/src/v4/classic/tests/map.test.ts +342 -0
  1158. package/node_modules/zod/src/v4/classic/tests/nan.test.ts +21 -0
  1159. package/node_modules/zod/src/v4/classic/tests/nested-refine.test.ts +168 -0
  1160. package/node_modules/zod/src/v4/classic/tests/nonoptional.test.ts +101 -0
  1161. package/node_modules/zod/src/v4/classic/tests/nullable.test.ts +22 -0
  1162. package/node_modules/zod/src/v4/classic/tests/number.test.ts +401 -0
  1163. package/node_modules/zod/src/v4/classic/tests/object.test.ts +1006 -0
  1164. package/node_modules/zod/src/v4/classic/tests/optin-ladder.test.ts +235 -0
  1165. package/node_modules/zod/src/v4/classic/tests/optional.test.ts +366 -0
  1166. package/node_modules/zod/src/v4/classic/tests/partial.test.ts +497 -0
  1167. package/node_modules/zod/src/v4/classic/tests/pickomit.test.ts +211 -0
  1168. package/node_modules/zod/src/v4/classic/tests/pipe.test.ts +101 -0
  1169. package/node_modules/zod/src/v4/classic/tests/prefault.test.ts +144 -0
  1170. package/node_modules/zod/src/v4/classic/tests/preprocess-types.test.ts +44 -0
  1171. package/node_modules/zod/src/v4/classic/tests/preprocess.test.ts +351 -0
  1172. package/node_modules/zod/src/v4/classic/tests/primitive.test.ts +175 -0
  1173. package/node_modules/zod/src/v4/classic/tests/promise.test.ts +81 -0
  1174. package/node_modules/zod/src/v4/classic/tests/properties.test.ts +141 -0
  1175. package/node_modules/zod/src/v4/classic/tests/prototypes.test.ts +23 -0
  1176. package/node_modules/zod/src/v4/classic/tests/readonly.test.ts +184 -0
  1177. package/node_modules/zod/src/v4/classic/tests/record.test.ts +885 -0
  1178. package/node_modules/zod/src/v4/classic/tests/recursive-types.test.ts +707 -0
  1179. package/node_modules/zod/src/v4/classic/tests/redos.test.ts +68 -0
  1180. package/node_modules/zod/src/v4/classic/tests/refine.test.ts +693 -0
  1181. package/node_modules/zod/src/v4/classic/tests/registries.test.ts +243 -0
  1182. package/node_modules/zod/src/v4/classic/tests/set.test.ts +190 -0
  1183. package/node_modules/zod/src/v4/classic/tests/standard-schema.test.ts +153 -0
  1184. package/node_modules/zod/src/v4/classic/tests/string-formats.test.ts +125 -0
  1185. package/node_modules/zod/src/v4/classic/tests/string.test.ts +1440 -0
  1186. package/node_modules/zod/src/v4/classic/tests/stringbool.test.ts +124 -0
  1187. package/node_modules/zod/src/v4/classic/tests/template-literal.test.ts +812 -0
  1188. package/node_modules/zod/src/v4/classic/tests/to-json-schema-methods.test.ts +438 -0
  1189. package/node_modules/zod/src/v4/classic/tests/to-json-schema.test.ts +4733 -0
  1190. package/node_modules/zod/src/v4/classic/tests/transform.test.ts +378 -0
  1191. package/node_modules/zod/src/v4/classic/tests/tuple.test.ts +511 -0
  1192. package/node_modules/zod/src/v4/classic/tests/union.test.ts +310 -0
  1193. package/node_modules/zod/src/v4/classic/tests/url.test.ts +13 -0
  1194. package/node_modules/zod/src/v4/classic/tests/validate.test.ts +645 -0
  1195. package/node_modules/zod/src/v4/classic/tests/validations.test.ts +283 -0
  1196. package/node_modules/zod/src/v4/classic/tests/void.test.ts +12 -0
  1197. package/node_modules/zod/src/v4/core/api.ts +1875 -0
  1198. package/node_modules/zod/src/v4/core/checks.ts +1266 -0
  1199. package/node_modules/zod/src/v4/core/compile.ts +2313 -0
  1200. package/node_modules/zod/src/v4/core/core.ts +230 -0
  1201. package/node_modules/zod/src/v4/core/doc.ts +52 -0
  1202. package/node_modules/zod/src/v4/core/errors.ts +543 -0
  1203. package/node_modules/zod/src/v4/core/index.ts +19 -0
  1204. package/node_modules/zod/src/v4/core/json-schema-generator.ts +131 -0
  1205. package/node_modules/zod/src/v4/core/json-schema-processors.ts +934 -0
  1206. package/node_modules/zod/src/v4/core/json-schema.ts +148 -0
  1207. package/node_modules/zod/src/v4/core/memoizer.ts +331 -0
  1208. package/node_modules/zod/src/v4/core/parse.ts +309 -0
  1209. package/node_modules/zod/src/v4/core/regexes.ts +217 -0
  1210. package/node_modules/zod/src/v4/core/registries.ts +105 -0
  1211. package/node_modules/zod/src/v4/core/schemas.ts +5192 -0
  1212. package/node_modules/zod/src/v4/core/standard-schema.ts +159 -0
  1213. package/node_modules/zod/src/v4/core/tests/cached.test.ts +54 -0
  1214. package/node_modules/zod/src/v4/core/tests/compile-differential.test.ts +1004 -0
  1215. package/node_modules/zod/src/v4/core/tests/compile-generative.test.ts +445 -0
  1216. package/node_modules/zod/src/v4/core/tests/compile-global.test.ts +89 -0
  1217. package/node_modules/zod/src/v4/core/tests/compile-off-by-default.test.ts +26 -0
  1218. package/node_modules/zod/src/v4/core/tests/compile-stats.test.ts +34 -0
  1219. package/node_modules/zod/src/v4/core/tests/compile.test.ts +2036 -0
  1220. package/node_modules/zod/src/v4/core/tests/email-regex.test.ts +140 -0
  1221. package/node_modules/zod/src/v4/core/tests/extend.test.ts +59 -0
  1222. package/node_modules/zod/src/v4/core/tests/index.test.ts +46 -0
  1223. package/node_modules/zod/src/v4/core/tests/locales/be.test.ts +124 -0
  1224. package/node_modules/zod/src/v4/core/tests/locales/el.test.ts +214 -0
  1225. package/node_modules/zod/src/v4/core/tests/locales/en.test.ts +22 -0
  1226. package/node_modules/zod/src/v4/core/tests/locales/es.test.ts +180 -0
  1227. package/node_modules/zod/src/v4/core/tests/locales/fr.test.ts +72 -0
  1228. package/node_modules/zod/src/v4/core/tests/locales/he.test.ts +379 -0
  1229. package/node_modules/zod/src/v4/core/tests/locales/hr.test.ts +163 -0
  1230. package/node_modules/zod/src/v4/core/tests/locales/nl.test.ts +46 -0
  1231. package/node_modules/zod/src/v4/core/tests/locales/parity.test.ts +42 -0
  1232. package/node_modules/zod/src/v4/core/tests/locales/ru.test.ts +128 -0
  1233. package/node_modules/zod/src/v4/core/tests/locales/tg.test.ts +85 -0
  1234. package/node_modules/zod/src/v4/core/tests/locales/tk.test.ts +69 -0
  1235. package/node_modules/zod/src/v4/core/tests/locales/tr.test.ts +69 -0
  1236. package/node_modules/zod/src/v4/core/tests/locales/uz.test.ts +105 -0
  1237. package/node_modules/zod/src/v4/core/tests/polyfill-collision.test.ts +46 -0
  1238. package/node_modules/zod/src/v4/core/tests/record-constructor.test.ts +125 -0
  1239. package/node_modules/zod/src/v4/core/tests/recursive-tuples.test.ts +45 -0
  1240. package/node_modules/zod/src/v4/core/tests/treeshake.test.ts +51 -0
  1241. package/node_modules/zod/src/v4/core/tests/url-no-canparse.test.ts +133 -0
  1242. package/node_modules/zod/src/v4/core/to-json-schema.ts +857 -0
  1243. package/node_modules/zod/src/v4/core/util.ts +1280 -0
  1244. package/node_modules/zod/src/v4/core/versions.ts +5 -0
  1245. package/node_modules/zod/src/v4/core/visit.ts +193 -0
  1246. package/node_modules/zod/src/v4/core/zsf.ts +323 -0
  1247. package/node_modules/zod/src/v4/index.ts +4 -0
  1248. package/node_modules/zod/src/v4/locales/ar.ts +120 -0
  1249. package/node_modules/zod/src/v4/locales/az.ts +116 -0
  1250. package/node_modules/zod/src/v4/locales/be.ts +188 -0
  1251. package/node_modules/zod/src/v4/locales/bg.ts +133 -0
  1252. package/node_modules/zod/src/v4/locales/bn.ts +123 -0
  1253. package/node_modules/zod/src/v4/locales/ca.ts +121 -0
  1254. package/node_modules/zod/src/v4/locales/ckb.ts +140 -0
  1255. package/node_modules/zod/src/v4/locales/cs.ts +123 -0
  1256. package/node_modules/zod/src/v4/locales/da.ts +128 -0
  1257. package/node_modules/zod/src/v4/locales/de.ts +121 -0
  1258. package/node_modules/zod/src/v4/locales/el.ts +124 -0
  1259. package/node_modules/zod/src/v4/locales/en.ts +136 -0
  1260. package/node_modules/zod/src/v4/locales/eo.ts +123 -0
  1261. package/node_modules/zod/src/v4/locales/es.ts +145 -0
  1262. package/node_modules/zod/src/v4/locales/fa.ts +131 -0
  1263. package/node_modules/zod/src/v4/locales/fi.ts +126 -0
  1264. package/node_modules/zod/src/v4/locales/fr-CA.ts +121 -0
  1265. package/node_modules/zod/src/v4/locales/fr.ts +139 -0
  1266. package/node_modules/zod/src/v4/locales/gu.ts +123 -0
  1267. package/node_modules/zod/src/v4/locales/he.ts +253 -0
  1268. package/node_modules/zod/src/v4/locales/hi.ts +126 -0
  1269. package/node_modules/zod/src/v4/locales/hr.ts +135 -0
  1270. package/node_modules/zod/src/v4/locales/hu.ts +122 -0
  1271. package/node_modules/zod/src/v4/locales/hy.ts +175 -0
  1272. package/node_modules/zod/src/v4/locales/id.ts +120 -0
  1273. package/node_modules/zod/src/v4/locales/index.ts +63 -0
  1274. package/node_modules/zod/src/v4/locales/is.ts +124 -0
  1275. package/node_modules/zod/src/v4/locales/it.ts +121 -0
  1276. package/node_modules/zod/src/v4/locales/ja.ts +119 -0
  1277. package/node_modules/zod/src/v4/locales/ka.ts +128 -0
  1278. package/node_modules/zod/src/v4/locales/kh.ts +7 -0
  1279. package/node_modules/zod/src/v4/locales/km.ts +124 -0
  1280. package/node_modules/zod/src/v4/locales/kn.ts +126 -0
  1281. package/node_modules/zod/src/v4/locales/ko.ts +126 -0
  1282. package/node_modules/zod/src/v4/locales/lt.ts +243 -0
  1283. package/node_modules/zod/src/v4/locales/mk.ts +123 -0
  1284. package/node_modules/zod/src/v4/locales/ms.ts +120 -0
  1285. package/node_modules/zod/src/v4/locales/ne.ts +128 -0
  1286. package/node_modules/zod/src/v4/locales/nl.ts +126 -0
  1287. package/node_modules/zod/src/v4/locales/nn.ts +121 -0
  1288. package/node_modules/zod/src/v4/locales/no.ts +121 -0
  1289. package/node_modules/zod/src/v4/locales/ota.ts +122 -0
  1290. package/node_modules/zod/src/v4/locales/pl.ts +123 -0
  1291. package/node_modules/zod/src/v4/locales/ps.ts +131 -0
  1292. package/node_modules/zod/src/v4/locales/pt-BR.ts +161 -0
  1293. package/node_modules/zod/src/v4/locales/pt.ts +159 -0
  1294. package/node_modules/zod/src/v4/locales/ro.ts +132 -0
  1295. package/node_modules/zod/src/v4/locales/ru.ts +188 -0
  1296. package/node_modules/zod/src/v4/locales/sk.ts +123 -0
  1297. package/node_modules/zod/src/v4/locales/sl.ts +123 -0
  1298. package/node_modules/zod/src/v4/locales/sv.ts +124 -0
  1299. package/node_modules/zod/src/v4/locales/ta.ts +123 -0
  1300. package/node_modules/zod/src/v4/locales/tg.ts +135 -0
  1301. package/node_modules/zod/src/v4/locales/th.ts +124 -0
  1302. package/node_modules/zod/src/v4/locales/tk.ts +113 -0
  1303. package/node_modules/zod/src/v4/locales/tr.ts +116 -0
  1304. package/node_modules/zod/src/v4/locales/ua.ts +7 -0
  1305. package/node_modules/zod/src/v4/locales/uk.ts +122 -0
  1306. package/node_modules/zod/src/v4/locales/ur.ts +124 -0
  1307. package/node_modules/zod/src/v4/locales/uz.ts +120 -0
  1308. package/node_modules/zod/src/v4/locales/vi.ts +122 -0
  1309. package/node_modules/zod/src/v4/locales/yo.ts +129 -0
  1310. package/node_modules/zod/src/v4/locales/zh-CN.ts +121 -0
  1311. package/node_modules/zod/src/v4/locales/zh-TW.ts +120 -0
  1312. package/node_modules/zod/src/v4/mini/checks.ts +33 -0
  1313. package/node_modules/zod/src/v4/mini/coerce.ts +27 -0
  1314. package/node_modules/zod/src/v4/mini/deep-partial.ts +73 -0
  1315. package/node_modules/zod/src/v4/mini/external.ts +52 -0
  1316. package/node_modules/zod/src/v4/mini/in-out.ts +45 -0
  1317. package/node_modules/zod/src/v4/mini/index.ts +3 -0
  1318. package/node_modules/zod/src/v4/mini/iso.ts +66 -0
  1319. package/node_modules/zod/src/v4/mini/parse.ts +16 -0
  1320. package/node_modules/zod/src/v4/mini/schemas.ts +2051 -0
  1321. package/node_modules/zod/src/v4/mini/tests/apply.test.ts +57 -0
  1322. package/node_modules/zod/src/v4/mini/tests/assignability.test.ts +190 -0
  1323. package/node_modules/zod/src/v4/mini/tests/brand.test.ts +94 -0
  1324. package/node_modules/zod/src/v4/mini/tests/checks.test.ts +158 -0
  1325. package/node_modules/zod/src/v4/mini/tests/codec.test.ts +582 -0
  1326. package/node_modules/zod/src/v4/mini/tests/computed.test.ts +30 -0
  1327. package/node_modules/zod/src/v4/mini/tests/cyclic-data.test.ts +84 -0
  1328. package/node_modules/zod/src/v4/mini/tests/deep-partial.test.ts +54 -0
  1329. package/node_modules/zod/src/v4/mini/tests/error.test.ts +41 -0
  1330. package/node_modules/zod/src/v4/mini/tests/functions.test.ts +14 -0
  1331. package/node_modules/zod/src/v4/mini/tests/index.test.ts +1146 -0
  1332. package/node_modules/zod/src/v4/mini/tests/number.test.ts +109 -0
  1333. package/node_modules/zod/src/v4/mini/tests/object.test.ts +292 -0
  1334. package/node_modules/zod/src/v4/mini/tests/prototypes.test.ts +43 -0
  1335. package/node_modules/zod/src/v4/mini/tests/recursive-types.test.ts +341 -0
  1336. package/node_modules/zod/src/v4/mini/tests/standard-schema.test.ts +50 -0
  1337. package/node_modules/zod/src/v4/mini/tests/string.test.ts +396 -0
  1338. package/node_modules/zod/src/v4-mini/index.ts +3 -0
  1339. package/node_modules/zod/v3/ZodError.cjs +167 -0
  1340. package/node_modules/zod/v3/ZodError.d.cts +164 -0
  1341. package/node_modules/zod/v3/ZodError.d.ts +164 -0
  1342. package/node_modules/zod/v3/ZodError.js +143 -0
  1343. package/node_modules/zod/v3/errors.cjs +36 -0
  1344. package/node_modules/zod/v3/errors.d.cts +5 -0
  1345. package/node_modules/zod/v3/errors.d.ts +5 -0
  1346. package/node_modules/zod/v3/errors.js +9 -0
  1347. package/node_modules/zod/v3/external.cjs +41 -0
  1348. package/node_modules/zod/v3/external.d.cts +6 -0
  1349. package/node_modules/zod/v3/external.d.ts +6 -0
  1350. package/node_modules/zod/v3/external.js +6 -0
  1351. package/node_modules/zod/v3/helpers/enumUtil.cjs +21 -0
  1352. package/node_modules/zod/v3/helpers/enumUtil.d.cts +8 -0
  1353. package/node_modules/zod/v3/helpers/enumUtil.d.ts +8 -0
  1354. package/node_modules/zod/v3/helpers/enumUtil.js +1 -0
  1355. package/node_modules/zod/v3/helpers/errorUtil.cjs +28 -0
  1356. package/node_modules/zod/v3/helpers/errorUtil.d.cts +9 -0
  1357. package/node_modules/zod/v3/helpers/errorUtil.d.ts +9 -0
  1358. package/node_modules/zod/v3/helpers/errorUtil.js +6 -0
  1359. package/node_modules/zod/v3/helpers/parseUtil.cjs +143 -0
  1360. package/node_modules/zod/v3/helpers/parseUtil.d.cts +78 -0
  1361. package/node_modules/zod/v3/helpers/parseUtil.d.ts +78 -0
  1362. package/node_modules/zod/v3/helpers/parseUtil.js +109 -0
  1363. package/node_modules/zod/v3/helpers/partialUtil.cjs +21 -0
  1364. package/node_modules/zod/v3/helpers/partialUtil.d.cts +8 -0
  1365. package/node_modules/zod/v3/helpers/partialUtil.d.ts +8 -0
  1366. package/node_modules/zod/v3/helpers/partialUtil.js +1 -0
  1367. package/node_modules/zod/v3/helpers/typeAliases.cjs +21 -0
  1368. package/node_modules/zod/v3/helpers/typeAliases.d.cts +2 -0
  1369. package/node_modules/zod/v3/helpers/typeAliases.d.ts +2 -0
  1370. package/node_modules/zod/v3/helpers/typeAliases.js +1 -0
  1371. package/node_modules/zod/v3/helpers/util.cjs +156 -0
  1372. package/node_modules/zod/v3/helpers/util.d.cts +85 -0
  1373. package/node_modules/zod/v3/helpers/util.d.ts +85 -0
  1374. package/node_modules/zod/v3/helpers/util.js +133 -0
  1375. package/node_modules/zod/v3/index.cjs +52 -0
  1376. package/node_modules/zod/v3/index.d.cts +4 -0
  1377. package/node_modules/zod/v3/index.d.ts +4 -0
  1378. package/node_modules/zod/v3/index.js +4 -0
  1379. package/node_modules/zod/v3/locales/en.cjs +112 -0
  1380. package/node_modules/zod/v3/locales/en.d.cts +3 -0
  1381. package/node_modules/zod/v3/locales/en.d.ts +3 -0
  1382. package/node_modules/zod/v3/locales/en.js +109 -0
  1383. package/node_modules/zod/v3/package.json +7 -0
  1384. package/node_modules/zod/v3/standard-schema.cjs +21 -0
  1385. package/node_modules/zod/v3/standard-schema.d.cts +102 -0
  1386. package/node_modules/zod/v3/standard-schema.d.ts +102 -0
  1387. package/node_modules/zod/v3/standard-schema.js +1 -0
  1388. package/node_modules/zod/v3/types.cjs +3798 -0
  1389. package/node_modules/zod/v3/types.d.cts +1034 -0
  1390. package/node_modules/zod/v3/types.d.ts +1034 -0
  1391. package/node_modules/zod/v3/types.js +3697 -0
  1392. package/node_modules/zod/v4/classic/checks.cjs +53 -0
  1393. package/node_modules/zod/v4/classic/checks.d.cts +1 -0
  1394. package/node_modules/zod/v4/classic/checks.d.ts +1 -0
  1395. package/node_modules/zod/v4/classic/checks.js +1 -0
  1396. package/node_modules/zod/v4/classic/coerce.cjs +66 -0
  1397. package/node_modules/zod/v4/classic/coerce.d.cts +17 -0
  1398. package/node_modules/zod/v4/classic/coerce.d.ts +17 -0
  1399. package/node_modules/zod/v4/classic/coerce.js +17 -0
  1400. package/node_modules/zod/v4/classic/compat.cjs +80 -0
  1401. package/node_modules/zod/v4/classic/compat.d.cts +48 -0
  1402. package/node_modules/zod/v4/classic/compat.d.ts +48 -0
  1403. package/node_modules/zod/v4/classic/compat.js +31 -0
  1404. package/node_modules/zod/v4/classic/deep-partial.cjs +58 -0
  1405. package/node_modules/zod/v4/classic/deep-partial.d.cts +14 -0
  1406. package/node_modules/zod/v4/classic/deep-partial.d.ts +14 -0
  1407. package/node_modules/zod/v4/classic/deep-partial.js +13 -0
  1408. package/node_modules/zod/v4/classic/errors.cjs +102 -0
  1409. package/node_modules/zod/v4/classic/errors.d.cts +29 -0
  1410. package/node_modules/zod/v4/classic/errors.d.ts +29 -0
  1411. package/node_modules/zod/v4/classic/errors.js +57 -0
  1412. package/node_modules/zod/v4/classic/external.cjs +97 -0
  1413. package/node_modules/zod/v4/classic/external.d.cts +19 -0
  1414. package/node_modules/zod/v4/classic/external.d.ts +19 -0
  1415. package/node_modules/zod/v4/classic/external.js +17 -0
  1416. package/node_modules/zod/v4/classic/from-json-schema.cjs +926 -0
  1417. package/node_modules/zod/v4/classic/from-json-schema.d.cts +12 -0
  1418. package/node_modules/zod/v4/classic/from-json-schema.d.ts +12 -0
  1419. package/node_modules/zod/v4/classic/from-json-schema.js +881 -0
  1420. package/node_modules/zod/v4/classic/in-out.cjs +82 -0
  1421. package/node_modules/zod/v4/classic/in-out.d.cts +8 -0
  1422. package/node_modules/zod/v4/classic/in-out.d.ts +8 -0
  1423. package/node_modules/zod/v4/classic/in-out.js +36 -0
  1424. package/node_modules/zod/v4/classic/index.cjs +52 -0
  1425. package/node_modules/zod/v4/classic/index.d.cts +3 -0
  1426. package/node_modules/zod/v4/classic/index.d.ts +3 -0
  1427. package/node_modules/zod/v4/classic/index.js +4 -0
  1428. package/node_modules/zod/v4/classic/iso.cjs +68 -0
  1429. package/node_modules/zod/v4/classic/iso.d.cts +7 -0
  1430. package/node_modules/zod/v4/classic/iso.d.ts +7 -0
  1431. package/node_modules/zod/v4/classic/iso.js +15 -0
  1432. package/node_modules/zod/v4/classic/package.json +7 -0
  1433. package/node_modules/zod/v4/classic/parse.cjs +63 -0
  1434. package/node_modules/zod/v4/classic/parse.d.cts +44 -0
  1435. package/node_modules/zod/v4/classic/parse.d.ts +44 -0
  1436. package/node_modules/zod/v4/classic/parse.js +16 -0
  1437. package/node_modules/zod/v4/classic/schemas.cjs +1676 -0
  1438. package/node_modules/zod/v4/classic/schemas.d.cts +816 -0
  1439. package/node_modules/zod/v4/classic/schemas.d.ts +816 -0
  1440. package/node_modules/zod/v4/classic/schemas.js +1535 -0
  1441. package/node_modules/zod/v4/core/api.cjs +1269 -0
  1442. package/node_modules/zod/v4/core/api.d.cts +335 -0
  1443. package/node_modules/zod/v4/core/api.d.ts +335 -0
  1444. package/node_modules/zod/v4/core/api.js +1107 -0
  1445. package/node_modules/zod/v4/core/checks.cjs +561 -0
  1446. package/node_modules/zod/v4/core/checks.d.cts +294 -0
  1447. package/node_modules/zod/v4/core/checks.d.ts +294 -0
  1448. package/node_modules/zod/v4/core/checks.js +516 -0
  1449. package/node_modules/zod/v4/core/compile.cjs +1908 -0
  1450. package/node_modules/zod/v4/core/compile.d.cts +65 -0
  1451. package/node_modules/zod/v4/core/compile.d.ts +65 -0
  1452. package/node_modules/zod/v4/core/compile.js +1858 -0
  1453. package/node_modules/zod/v4/core/core.cjs +167 -0
  1454. package/node_modules/zod/v4/core/core.d.cts +82 -0
  1455. package/node_modules/zod/v4/core/core.d.ts +82 -0
  1456. package/node_modules/zod/v4/core/core.js +141 -0
  1457. package/node_modules/zod/v4/core/doc.cjs +61 -0
  1458. package/node_modules/zod/v4/core/doc.d.cts +16 -0
  1459. package/node_modules/zod/v4/core/doc.d.ts +16 -0
  1460. package/node_modules/zod/v4/core/doc.js +38 -0
  1461. package/node_modules/zod/v4/core/errors.cjs +317 -0
  1462. package/node_modules/zod/v4/core/errors.d.cts +225 -0
  1463. package/node_modules/zod/v4/core/errors.d.ts +225 -0
  1464. package/node_modules/zod/v4/core/errors.js +267 -0
  1465. package/node_modules/zod/v4/core/index.cjs +70 -0
  1466. package/node_modules/zod/v4/core/index.d.cts +19 -0
  1467. package/node_modules/zod/v4/core/index.d.ts +19 -0
  1468. package/node_modules/zod/v4/core/index.js +19 -0
  1469. package/node_modules/zod/v4/core/json-schema-generator.cjs +122 -0
  1470. package/node_modules/zod/v4/core/json-schema-generator.d.cts +65 -0
  1471. package/node_modules/zod/v4/core/json-schema-generator.d.ts +65 -0
  1472. package/node_modules/zod/v4/core/json-schema-generator.js +99 -0
  1473. package/node_modules/zod/v4/core/json-schema-processors.cjs +929 -0
  1474. package/node_modules/zod/v4/core/json-schema-processors.d.cts +65 -0
  1475. package/node_modules/zod/v4/core/json-schema-processors.d.ts +65 -0
  1476. package/node_modules/zod/v4/core/json-schema-processors.js +843 -0
  1477. package/node_modules/zod/v4/core/json-schema.cjs +21 -0
  1478. package/node_modules/zod/v4/core/json-schema.d.cts +89 -0
  1479. package/node_modules/zod/v4/core/json-schema.d.ts +89 -0
  1480. package/node_modules/zod/v4/core/json-schema.js +1 -0
  1481. package/node_modules/zod/v4/core/memoizer.cjs +360 -0
  1482. package/node_modules/zod/v4/core/memoizer.d.cts +15 -0
  1483. package/node_modules/zod/v4/core/memoizer.d.ts +15 -0
  1484. package/node_modules/zod/v4/core/memoizer.js +311 -0
  1485. package/node_modules/zod/v4/core/package.json +7 -0
  1486. package/node_modules/zod/v4/core/parse.cjs +233 -0
  1487. package/node_modules/zod/v4/core/parse.d.cts +52 -0
  1488. package/node_modules/zod/v4/core/parse.d.ts +52 -0
  1489. package/node_modules/zod/v4/core/parse.js +175 -0
  1490. package/node_modules/zod/v4/core/regexes.cjs +209 -0
  1491. package/node_modules/zod/v4/core/regexes.d.cts +90 -0
  1492. package/node_modules/zod/v4/core/regexes.d.ts +90 -0
  1493. package/node_modules/zod/v4/core/regexes.js +156 -0
  1494. package/node_modules/zod/v4/core/registries.cjs +75 -0
  1495. package/node_modules/zod/v4/core/registries.d.cts +35 -0
  1496. package/node_modules/zod/v4/core/registries.d.ts +35 -0
  1497. package/node_modules/zod/v4/core/registries.js +51 -0
  1498. package/node_modules/zod/v4/core/schemas.cjs +2669 -0
  1499. package/node_modules/zod/v4/core/schemas.d.cts +1273 -0
  1500. package/node_modules/zod/v4/core/schemas.d.ts +1273 -0
  1501. package/node_modules/zod/v4/core/schemas.js +2606 -0
  1502. package/node_modules/zod/v4/core/standard-schema.cjs +21 -0
  1503. package/node_modules/zod/v4/core/standard-schema.d.cts +126 -0
  1504. package/node_modules/zod/v4/core/standard-schema.d.ts +126 -0
  1505. package/node_modules/zod/v4/core/standard-schema.js +1 -0
  1506. package/node_modules/zod/v4/core/to-json-schema.cjs +681 -0
  1507. package/node_modules/zod/v4/core/to-json-schema.d.cts +154 -0
  1508. package/node_modules/zod/v4/core/to-json-schema.d.ts +154 -0
  1509. package/node_modules/zod/v4/core/to-json-schema.js +651 -0
  1510. package/node_modules/zod/v4/core/util.cjs +949 -0
  1511. package/node_modules/zod/v4/core/util.d.cts +278 -0
  1512. package/node_modules/zod/v4/core/util.d.ts +278 -0
  1513. package/node_modules/zod/v4/core/util.js +859 -0
  1514. package/node_modules/zod/v4/core/versions.cjs +27 -0
  1515. package/node_modules/zod/v4/core/versions.d.cts +5 -0
  1516. package/node_modules/zod/v4/core/versions.d.ts +5 -0
  1517. package/node_modules/zod/v4/core/versions.js +5 -0
  1518. package/node_modules/zod/v4/core/visit.cjs +214 -0
  1519. package/node_modules/zod/v4/core/visit.d.cts +30 -0
  1520. package/node_modules/zod/v4/core/visit.d.ts +30 -0
  1521. package/node_modules/zod/v4/core/visit.js +169 -0
  1522. package/node_modules/zod/v4/index.cjs +51 -0
  1523. package/node_modules/zod/v4/index.d.cts +3 -0
  1524. package/node_modules/zod/v4/index.d.ts +3 -0
  1525. package/node_modules/zod/v4/index.js +4 -0
  1526. package/node_modules/zod/v4/locales/ar.cjs +138 -0
  1527. package/node_modules/zod/v4/locales/ar.d.cts +5 -0
  1528. package/node_modules/zod/v4/locales/ar.d.ts +4 -0
  1529. package/node_modules/zod/v4/locales/ar.js +111 -0
  1530. package/node_modules/zod/v4/locales/az.cjs +137 -0
  1531. package/node_modules/zod/v4/locales/az.d.cts +5 -0
  1532. package/node_modules/zod/v4/locales/az.d.ts +4 -0
  1533. package/node_modules/zod/v4/locales/az.js +110 -0
  1534. package/node_modules/zod/v4/locales/be.cjs +195 -0
  1535. package/node_modules/zod/v4/locales/be.d.cts +5 -0
  1536. package/node_modules/zod/v4/locales/be.d.ts +4 -0
  1537. package/node_modules/zod/v4/locales/be.js +168 -0
  1538. package/node_modules/zod/v4/locales/bg.cjs +152 -0
  1539. package/node_modules/zod/v4/locales/bg.d.cts +5 -0
  1540. package/node_modules/zod/v4/locales/bg.d.ts +4 -0
  1541. package/node_modules/zod/v4/locales/bg.js +125 -0
  1542. package/node_modules/zod/v4/locales/bn.cjs +140 -0
  1543. package/node_modules/zod/v4/locales/bn.d.cts +5 -0
  1544. package/node_modules/zod/v4/locales/bn.d.ts +4 -0
  1545. package/node_modules/zod/v4/locales/bn.js +113 -0
  1546. package/node_modules/zod/v4/locales/ca.cjs +139 -0
  1547. package/node_modules/zod/v4/locales/ca.d.cts +5 -0
  1548. package/node_modules/zod/v4/locales/ca.d.ts +4 -0
  1549. package/node_modules/zod/v4/locales/ca.js +112 -0
  1550. package/node_modules/zod/v4/locales/ckb.cjs +160 -0
  1551. package/node_modules/zod/v4/locales/ckb.d.cts +5 -0
  1552. package/node_modules/zod/v4/locales/ckb.d.ts +4 -0
  1553. package/node_modules/zod/v4/locales/ckb.js +133 -0
  1554. package/node_modules/zod/v4/locales/cs.cjs +143 -0
  1555. package/node_modules/zod/v4/locales/cs.d.cts +5 -0
  1556. package/node_modules/zod/v4/locales/cs.d.ts +4 -0
  1557. package/node_modules/zod/v4/locales/cs.js +116 -0
  1558. package/node_modules/zod/v4/locales/da.cjs +147 -0
  1559. package/node_modules/zod/v4/locales/da.d.cts +5 -0
  1560. package/node_modules/zod/v4/locales/da.d.ts +4 -0
  1561. package/node_modules/zod/v4/locales/da.js +120 -0
  1562. package/node_modules/zod/v4/locales/de.cjs +140 -0
  1563. package/node_modules/zod/v4/locales/de.d.cts +5 -0
  1564. package/node_modules/zod/v4/locales/de.d.ts +4 -0
  1565. package/node_modules/zod/v4/locales/de.js +113 -0
  1566. package/node_modules/zod/v4/locales/el.cjs +139 -0
  1567. package/node_modules/zod/v4/locales/el.d.cts +5 -0
  1568. package/node_modules/zod/v4/locales/el.d.ts +4 -0
  1569. package/node_modules/zod/v4/locales/el.js +112 -0
  1570. package/node_modules/zod/v4/locales/en.cjs +152 -0
  1571. package/node_modules/zod/v4/locales/en.d.cts +5 -0
  1572. package/node_modules/zod/v4/locales/en.d.ts +4 -0
  1573. package/node_modules/zod/v4/locales/en.js +125 -0
  1574. package/node_modules/zod/v4/locales/eo.cjs +141 -0
  1575. package/node_modules/zod/v4/locales/eo.d.cts +5 -0
  1576. package/node_modules/zod/v4/locales/eo.d.ts +4 -0
  1577. package/node_modules/zod/v4/locales/eo.js +114 -0
  1578. package/node_modules/zod/v4/locales/es.cjs +163 -0
  1579. package/node_modules/zod/v4/locales/es.d.cts +5 -0
  1580. package/node_modules/zod/v4/locales/es.d.ts +4 -0
  1581. package/node_modules/zod/v4/locales/es.js +136 -0
  1582. package/node_modules/zod/v4/locales/fa.cjs +146 -0
  1583. package/node_modules/zod/v4/locales/fa.d.cts +5 -0
  1584. package/node_modules/zod/v4/locales/fa.d.ts +4 -0
  1585. package/node_modules/zod/v4/locales/fa.js +119 -0
  1586. package/node_modules/zod/v4/locales/fi.cjs +144 -0
  1587. package/node_modules/zod/v4/locales/fi.d.cts +5 -0
  1588. package/node_modules/zod/v4/locales/fi.d.ts +4 -0
  1589. package/node_modules/zod/v4/locales/fi.js +117 -0
  1590. package/node_modules/zod/v4/locales/fr-CA.cjs +139 -0
  1591. package/node_modules/zod/v4/locales/fr-CA.d.cts +5 -0
  1592. package/node_modules/zod/v4/locales/fr-CA.d.ts +4 -0
  1593. package/node_modules/zod/v4/locales/fr-CA.js +112 -0
  1594. package/node_modules/zod/v4/locales/fr.cjs +156 -0
  1595. package/node_modules/zod/v4/locales/fr.d.cts +5 -0
  1596. package/node_modules/zod/v4/locales/fr.d.ts +4 -0
  1597. package/node_modules/zod/v4/locales/fr.js +129 -0
  1598. package/node_modules/zod/v4/locales/gu.cjs +140 -0
  1599. package/node_modules/zod/v4/locales/gu.d.cts +5 -0
  1600. package/node_modules/zod/v4/locales/gu.d.ts +4 -0
  1601. package/node_modules/zod/v4/locales/gu.js +113 -0
  1602. package/node_modules/zod/v4/locales/he.cjs +248 -0
  1603. package/node_modules/zod/v4/locales/he.d.cts +5 -0
  1604. package/node_modules/zod/v4/locales/he.d.ts +4 -0
  1605. package/node_modules/zod/v4/locales/he.js +221 -0
  1606. package/node_modules/zod/v4/locales/hi.cjs +138 -0
  1607. package/node_modules/zod/v4/locales/hi.d.cts +5 -0
  1608. package/node_modules/zod/v4/locales/hi.d.ts +4 -0
  1609. package/node_modules/zod/v4/locales/hi.js +111 -0
  1610. package/node_modules/zod/v4/locales/hr.cjs +153 -0
  1611. package/node_modules/zod/v4/locales/hr.d.cts +5 -0
  1612. package/node_modules/zod/v4/locales/hr.d.ts +4 -0
  1613. package/node_modules/zod/v4/locales/hr.js +126 -0
  1614. package/node_modules/zod/v4/locales/hu.cjs +140 -0
  1615. package/node_modules/zod/v4/locales/hu.d.cts +5 -0
  1616. package/node_modules/zod/v4/locales/hu.d.ts +4 -0
  1617. package/node_modules/zod/v4/locales/hu.js +113 -0
  1618. package/node_modules/zod/v4/locales/hy.cjs +185 -0
  1619. package/node_modules/zod/v4/locales/hy.d.cts +5 -0
  1620. package/node_modules/zod/v4/locales/hy.d.ts +4 -0
  1621. package/node_modules/zod/v4/locales/hy.js +158 -0
  1622. package/node_modules/zod/v4/locales/id.cjs +138 -0
  1623. package/node_modules/zod/v4/locales/id.d.cts +5 -0
  1624. package/node_modules/zod/v4/locales/id.d.ts +4 -0
  1625. package/node_modules/zod/v4/locales/id.js +111 -0
  1626. package/node_modules/zod/v4/locales/index.cjs +152 -0
  1627. package/node_modules/zod/v4/locales/index.d.cts +63 -0
  1628. package/node_modules/zod/v4/locales/index.d.ts +63 -0
  1629. package/node_modules/zod/v4/locales/index.js +63 -0
  1630. package/node_modules/zod/v4/locales/is.cjs +141 -0
  1631. package/node_modules/zod/v4/locales/is.d.cts +5 -0
  1632. package/node_modules/zod/v4/locales/is.d.ts +4 -0
  1633. package/node_modules/zod/v4/locales/is.js +114 -0
  1634. package/node_modules/zod/v4/locales/it.cjs +140 -0
  1635. package/node_modules/zod/v4/locales/it.d.cts +5 -0
  1636. package/node_modules/zod/v4/locales/it.d.ts +4 -0
  1637. package/node_modules/zod/v4/locales/it.js +113 -0
  1638. package/node_modules/zod/v4/locales/ja.cjs +139 -0
  1639. package/node_modules/zod/v4/locales/ja.d.cts +5 -0
  1640. package/node_modules/zod/v4/locales/ja.d.ts +4 -0
  1641. package/node_modules/zod/v4/locales/ja.js +112 -0
  1642. package/node_modules/zod/v4/locales/ka.cjs +144 -0
  1643. package/node_modules/zod/v4/locales/ka.d.cts +5 -0
  1644. package/node_modules/zod/v4/locales/ka.d.ts +4 -0
  1645. package/node_modules/zod/v4/locales/ka.js +117 -0
  1646. package/node_modules/zod/v4/locales/kh.cjs +12 -0
  1647. package/node_modules/zod/v4/locales/kh.d.cts +5 -0
  1648. package/node_modules/zod/v4/locales/kh.d.ts +5 -0
  1649. package/node_modules/zod/v4/locales/kh.js +5 -0
  1650. package/node_modules/zod/v4/locales/km.cjs +142 -0
  1651. package/node_modules/zod/v4/locales/km.d.cts +5 -0
  1652. package/node_modules/zod/v4/locales/km.d.ts +4 -0
  1653. package/node_modules/zod/v4/locales/km.js +115 -0
  1654. package/node_modules/zod/v4/locales/kn.cjs +143 -0
  1655. package/node_modules/zod/v4/locales/kn.d.cts +5 -0
  1656. package/node_modules/zod/v4/locales/kn.d.ts +4 -0
  1657. package/node_modules/zod/v4/locales/kn.js +116 -0
  1658. package/node_modules/zod/v4/locales/ko.cjs +143 -0
  1659. package/node_modules/zod/v4/locales/ko.d.cts +5 -0
  1660. package/node_modules/zod/v4/locales/ko.d.ts +4 -0
  1661. package/node_modules/zod/v4/locales/ko.js +116 -0
  1662. package/node_modules/zod/v4/locales/lt.cjs +234 -0
  1663. package/node_modules/zod/v4/locales/lt.d.cts +5 -0
  1664. package/node_modules/zod/v4/locales/lt.d.ts +4 -0
  1665. package/node_modules/zod/v4/locales/lt.js +207 -0
  1666. package/node_modules/zod/v4/locales/mk.cjs +141 -0
  1667. package/node_modules/zod/v4/locales/mk.d.cts +5 -0
  1668. package/node_modules/zod/v4/locales/mk.d.ts +4 -0
  1669. package/node_modules/zod/v4/locales/mk.js +114 -0
  1670. package/node_modules/zod/v4/locales/ms.cjs +139 -0
  1671. package/node_modules/zod/v4/locales/ms.d.cts +5 -0
  1672. package/node_modules/zod/v4/locales/ms.d.ts +4 -0
  1673. package/node_modules/zod/v4/locales/ms.js +112 -0
  1674. package/node_modules/zod/v4/locales/ne.cjs +138 -0
  1675. package/node_modules/zod/v4/locales/ne.d.cts +5 -0
  1676. package/node_modules/zod/v4/locales/ne.d.ts +4 -0
  1677. package/node_modules/zod/v4/locales/ne.js +111 -0
  1678. package/node_modules/zod/v4/locales/nl.cjs +142 -0
  1679. package/node_modules/zod/v4/locales/nl.d.cts +5 -0
  1680. package/node_modules/zod/v4/locales/nl.d.ts +4 -0
  1681. package/node_modules/zod/v4/locales/nl.js +115 -0
  1682. package/node_modules/zod/v4/locales/nn.cjs +140 -0
  1683. package/node_modules/zod/v4/locales/nn.d.cts +5 -0
  1684. package/node_modules/zod/v4/locales/nn.d.ts +4 -0
  1685. package/node_modules/zod/v4/locales/nn.js +113 -0
  1686. package/node_modules/zod/v4/locales/no.cjs +140 -0
  1687. package/node_modules/zod/v4/locales/no.d.cts +5 -0
  1688. package/node_modules/zod/v4/locales/no.d.ts +4 -0
  1689. package/node_modules/zod/v4/locales/no.js +113 -0
  1690. package/node_modules/zod/v4/locales/ota.cjs +141 -0
  1691. package/node_modules/zod/v4/locales/ota.d.cts +5 -0
  1692. package/node_modules/zod/v4/locales/ota.d.ts +4 -0
  1693. package/node_modules/zod/v4/locales/ota.js +114 -0
  1694. package/node_modules/zod/v4/locales/package.json +7 -0
  1695. package/node_modules/zod/v4/locales/pl.cjs +141 -0
  1696. package/node_modules/zod/v4/locales/pl.d.cts +5 -0
  1697. package/node_modules/zod/v4/locales/pl.d.ts +4 -0
  1698. package/node_modules/zod/v4/locales/pl.js +114 -0
  1699. package/node_modules/zod/v4/locales/ps.cjs +146 -0
  1700. package/node_modules/zod/v4/locales/ps.d.cts +5 -0
  1701. package/node_modules/zod/v4/locales/ps.d.ts +4 -0
  1702. package/node_modules/zod/v4/locales/ps.js +119 -0
  1703. package/node_modules/zod/v4/locales/pt-BR.cjs +170 -0
  1704. package/node_modules/zod/v4/locales/pt-BR.d.cts +5 -0
  1705. package/node_modules/zod/v4/locales/pt-BR.d.ts +4 -0
  1706. package/node_modules/zod/v4/locales/pt-BR.js +143 -0
  1707. package/node_modules/zod/v4/locales/pt.cjs +169 -0
  1708. package/node_modules/zod/v4/locales/pt.d.cts +5 -0
  1709. package/node_modules/zod/v4/locales/pt.d.ts +4 -0
  1710. package/node_modules/zod/v4/locales/pt.js +142 -0
  1711. package/node_modules/zod/v4/locales/ro.cjs +149 -0
  1712. package/node_modules/zod/v4/locales/ro.d.cts +5 -0
  1713. package/node_modules/zod/v4/locales/ro.d.ts +4 -0
  1714. package/node_modules/zod/v4/locales/ro.js +122 -0
  1715. package/node_modules/zod/v4/locales/ru.cjs +195 -0
  1716. package/node_modules/zod/v4/locales/ru.d.cts +5 -0
  1717. package/node_modules/zod/v4/locales/ru.d.ts +4 -0
  1718. package/node_modules/zod/v4/locales/ru.js +168 -0
  1719. package/node_modules/zod/v4/locales/sk.cjs +143 -0
  1720. package/node_modules/zod/v4/locales/sk.d.cts +5 -0
  1721. package/node_modules/zod/v4/locales/sk.d.ts +4 -0
  1722. package/node_modules/zod/v4/locales/sk.js +116 -0
  1723. package/node_modules/zod/v4/locales/sl.cjs +141 -0
  1724. package/node_modules/zod/v4/locales/sl.d.cts +5 -0
  1725. package/node_modules/zod/v4/locales/sl.d.ts +4 -0
  1726. package/node_modules/zod/v4/locales/sl.js +114 -0
  1727. package/node_modules/zod/v4/locales/sv.cjs +142 -0
  1728. package/node_modules/zod/v4/locales/sv.d.cts +5 -0
  1729. package/node_modules/zod/v4/locales/sv.d.ts +4 -0
  1730. package/node_modules/zod/v4/locales/sv.js +115 -0
  1731. package/node_modules/zod/v4/locales/ta.cjs +142 -0
  1732. package/node_modules/zod/v4/locales/ta.d.cts +5 -0
  1733. package/node_modules/zod/v4/locales/ta.d.ts +4 -0
  1734. package/node_modules/zod/v4/locales/ta.js +115 -0
  1735. package/node_modules/zod/v4/locales/tg.cjs +144 -0
  1736. package/node_modules/zod/v4/locales/tg.d.cts +5 -0
  1737. package/node_modules/zod/v4/locales/tg.d.ts +4 -0
  1738. package/node_modules/zod/v4/locales/tg.js +117 -0
  1739. package/node_modules/zod/v4/locales/th.cjs +142 -0
  1740. package/node_modules/zod/v4/locales/th.d.cts +5 -0
  1741. package/node_modules/zod/v4/locales/th.d.ts +4 -0
  1742. package/node_modules/zod/v4/locales/th.js +115 -0
  1743. package/node_modules/zod/v4/locales/tk.cjs +134 -0
  1744. package/node_modules/zod/v4/locales/tk.d.cts +5 -0
  1745. package/node_modules/zod/v4/locales/tk.d.ts +4 -0
  1746. package/node_modules/zod/v4/locales/tk.js +107 -0
  1747. package/node_modules/zod/v4/locales/tr.cjs +137 -0
  1748. package/node_modules/zod/v4/locales/tr.d.cts +5 -0
  1749. package/node_modules/zod/v4/locales/tr.d.ts +4 -0
  1750. package/node_modules/zod/v4/locales/tr.js +110 -0
  1751. package/node_modules/zod/v4/locales/ua.cjs +12 -0
  1752. package/node_modules/zod/v4/locales/ua.d.cts +5 -0
  1753. package/node_modules/zod/v4/locales/ua.d.ts +5 -0
  1754. package/node_modules/zod/v4/locales/ua.js +5 -0
  1755. package/node_modules/zod/v4/locales/uk.cjs +140 -0
  1756. package/node_modules/zod/v4/locales/uk.d.cts +5 -0
  1757. package/node_modules/zod/v4/locales/uk.d.ts +4 -0
  1758. package/node_modules/zod/v4/locales/uk.js +113 -0
  1759. package/node_modules/zod/v4/locales/ur.cjs +142 -0
  1760. package/node_modules/zod/v4/locales/ur.d.cts +5 -0
  1761. package/node_modules/zod/v4/locales/ur.d.ts +4 -0
  1762. package/node_modules/zod/v4/locales/ur.js +115 -0
  1763. package/node_modules/zod/v4/locales/uz.cjs +140 -0
  1764. package/node_modules/zod/v4/locales/uz.d.cts +5 -0
  1765. package/node_modules/zod/v4/locales/uz.d.ts +4 -0
  1766. package/node_modules/zod/v4/locales/uz.js +113 -0
  1767. package/node_modules/zod/v4/locales/vi.cjs +140 -0
  1768. package/node_modules/zod/v4/locales/vi.d.cts +5 -0
  1769. package/node_modules/zod/v4/locales/vi.d.ts +4 -0
  1770. package/node_modules/zod/v4/locales/vi.js +113 -0
  1771. package/node_modules/zod/v4/locales/yo.cjs +139 -0
  1772. package/node_modules/zod/v4/locales/yo.d.cts +5 -0
  1773. package/node_modules/zod/v4/locales/yo.d.ts +4 -0
  1774. package/node_modules/zod/v4/locales/yo.js +112 -0
  1775. package/node_modules/zod/v4/locales/zh-CN.cjs +141 -0
  1776. package/node_modules/zod/v4/locales/zh-CN.d.cts +5 -0
  1777. package/node_modules/zod/v4/locales/zh-CN.d.ts +4 -0
  1778. package/node_modules/zod/v4/locales/zh-CN.js +114 -0
  1779. package/node_modules/zod/v4/locales/zh-TW.cjs +139 -0
  1780. package/node_modules/zod/v4/locales/zh-TW.d.cts +5 -0
  1781. package/node_modules/zod/v4/locales/zh-TW.d.ts +4 -0
  1782. package/node_modules/zod/v4/locales/zh-TW.js +112 -0
  1783. package/node_modules/zod/v4/mini/checks.cjs +54 -0
  1784. package/node_modules/zod/v4/mini/checks.d.cts +1 -0
  1785. package/node_modules/zod/v4/mini/checks.d.ts +1 -0
  1786. package/node_modules/zod/v4/mini/checks.js +1 -0
  1787. package/node_modules/zod/v4/mini/coerce.cjs +71 -0
  1788. package/node_modules/zod/v4/mini/coerce.d.cts +7 -0
  1789. package/node_modules/zod/v4/mini/coerce.d.ts +7 -0
  1790. package/node_modules/zod/v4/mini/coerce.js +22 -0
  1791. package/node_modules/zod/v4/mini/deep-partial.cjs +58 -0
  1792. package/node_modules/zod/v4/mini/deep-partial.d.cts +14 -0
  1793. package/node_modules/zod/v4/mini/deep-partial.d.ts +14 -0
  1794. package/node_modules/zod/v4/mini/deep-partial.js +13 -0
  1795. package/node_modules/zod/v4/mini/external.cjs +95 -0
  1796. package/node_modules/zod/v4/mini/external.d.cts +16 -0
  1797. package/node_modules/zod/v4/mini/external.d.ts +16 -0
  1798. package/node_modules/zod/v4/mini/external.js +16 -0
  1799. package/node_modules/zod/v4/mini/in-out.cjs +82 -0
  1800. package/node_modules/zod/v4/mini/in-out.d.cts +8 -0
  1801. package/node_modules/zod/v4/mini/in-out.d.ts +8 -0
  1802. package/node_modules/zod/v4/mini/in-out.js +36 -0
  1803. package/node_modules/zod/v4/mini/index.cjs +51 -0
  1804. package/node_modules/zod/v4/mini/index.d.cts +3 -0
  1805. package/node_modules/zod/v4/mini/index.d.ts +3 -0
  1806. package/node_modules/zod/v4/mini/index.js +3 -0
  1807. package/node_modules/zod/v4/mini/iso.cjs +83 -0
  1808. package/node_modules/zod/v4/mini/iso.d.cts +22 -0
  1809. package/node_modules/zod/v4/mini/iso.d.ts +22 -0
  1810. package/node_modules/zod/v4/mini/iso.js +34 -0
  1811. package/node_modules/zod/v4/mini/package.json +7 -0
  1812. package/node_modules/zod/v4/mini/parse.cjs +37 -0
  1813. package/node_modules/zod/v4/mini/parse.d.cts +1 -0
  1814. package/node_modules/zod/v4/mini/parse.d.ts +1 -0
  1815. package/node_modules/zod/v4/mini/parse.js +1 -0
  1816. package/node_modules/zod/v4/mini/schemas.cjs +1147 -0
  1817. package/node_modules/zod/v4/mini/schemas.d.cts +480 -0
  1818. package/node_modules/zod/v4/mini/schemas.d.ts +480 -0
  1819. package/node_modules/zod/v4/mini/schemas.js +1002 -0
  1820. package/node_modules/zod/v4/package.json +7 -0
  1821. package/node_modules/zod/v4-mini/index.cjs +51 -0
  1822. package/node_modules/zod/v4-mini/index.d.cts +3 -0
  1823. package/node_modules/zod/v4-mini/index.d.ts +3 -0
  1824. package/node_modules/zod/v4-mini/index.js +3 -0
  1825. package/node_modules/zod/v4-mini/package.json +7 -0
  1826. package/package.json +50 -0
@@ -0,0 +1,3746 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { promises as fsPromises } from "node:fs";
3
+ import { lstat, mkdir, mkdtemp, readFile, readdir, rm, stat, symlink, writeFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { execFile as execFileCallback, spawn } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ import { afterEach, describe, expect, it, vi } from "vitest";
9
+ import { resetLocalGitIndexToHead, runLocalGit, setExpensiveWorkspaceGitExecutor, WORKSPACE_GIT_SCAN_SATURATED_CODE, } from "./git-workspace-sync.js";
10
+ import { assertSyncOperationsConfined, escapeTarExcludeLiteral, mirrorDirectory, prepareSandboxManagedRuntime, REFERENCED_SOURCE_IGNORE_FAILURE_REASONS, resolveReferencedSourceIgnore, } from "./sandbox-managed-runtime.js";
11
+ import { prepareCommandManagedRuntime, } from "./command-managed-runtime.js";
12
+ import { SYNC_OPERATION_CONCURRENCY_LIMIT } from "./sync-operation-schedule.js";
13
+ import { createRuntimeSpanRunner, getActiveStepContext, measureStartupStep, } from "./acpx-engine/startup-timing.js";
14
+ function toArrayBuffer(bytes) {
15
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
16
+ }
17
+ // Sum the file sizes under a directory, recursively. Test-only: it stands in
18
+ // for a real provider's own byte count on a `kind: "directory"` mapping, so a
19
+ // fake `syncIn`/`syncOut` can report a real, non-zero `bytesTransferred`.
20
+ async function directoryByteSize(dir) {
21
+ let total = 0;
22
+ const entries = await readdir(dir, { withFileTypes: true });
23
+ for (const entry of entries) {
24
+ const entryPath = path.join(dir, entry.name);
25
+ if (entry.isDirectory()) {
26
+ total += await directoryByteSize(entryPath);
27
+ }
28
+ else if (entry.isFile()) {
29
+ total += (await stat(entryPath)).size;
30
+ }
31
+ }
32
+ return total;
33
+ }
34
+ // Give a bare fake client a `syncIn` that reproduces the non-native base64-tar
35
+ // FALLBACK (place each file mapping via `writeFile`, then run the operation's
36
+ // ordered `postUploadCommands` fail-fast via `run`) — byte-for-byte the prior
37
+ // inline `writeFile`+`run` sequence, exercised through the unified seam. Inbound
38
+ // staging uses only `kind: "file"` mappings.
39
+ function attachFallbackSyncIn(client, timeoutMs = 30_000) {
40
+ client.syncIn = async (operations) => {
41
+ const resultOperations = [];
42
+ for (const operation of operations) {
43
+ let filesTransferred = 0;
44
+ let bytesTransferred = 0;
45
+ for (const mapping of operation.files) {
46
+ const bytes = await readFile(mapping.sourcePath);
47
+ await client.makeDir(path.posix.dirname(mapping.targetPath));
48
+ if (mapping.mode != null) {
49
+ const staged = `${mapping.targetPath}.pcstage`;
50
+ await client.writeFile(staged, toArrayBuffer(bytes));
51
+ await client.run(`chmod ${(mapping.mode & 0o7777).toString(8)} '${staged}' && mv -f '${staged}' '${mapping.targetPath}'`, { timeoutMs });
52
+ }
53
+ else {
54
+ await client.writeFile(mapping.targetPath, toArrayBuffer(bytes));
55
+ }
56
+ filesTransferred += 1;
57
+ bytesTransferred += bytes.byteLength;
58
+ }
59
+ for (const command of operation.postUploadCommands ?? []) {
60
+ await client.run(command.command, { timeoutMs: command.timeoutMs ?? timeoutMs });
61
+ }
62
+ resultOperations.push({ operationId: operation.operationId, filesTransferred, bytesTransferred });
63
+ }
64
+ return { operations: resultOperations };
65
+ };
66
+ }
67
+ // A NATIVE-simulating `syncIn`: it records the operations for assertion and
68
+ // materializes files directly (never through the client's `writeFile`/`run`), so
69
+ // a test can prove the orchestrator delegates entirely to `syncIn` (0 direct
70
+ // `writeFile`/`run` execs). Post-upload commands still run in-sandbox (via `sh`),
71
+ // modeling a provider that honors `postUploadCommands` after `uploadFiles`.
72
+ function attachNativeRecordingSyncIn(client, captured) {
73
+ client.syncIn = async (operations) => {
74
+ const resultOperations = [];
75
+ for (const operation of operations) {
76
+ captured.push(operation);
77
+ let filesTransferred = 0;
78
+ let bytesTransferred = 0;
79
+ for (const mapping of operation.files) {
80
+ const bytes = await readFile(mapping.sourcePath);
81
+ await mkdir(path.posix.dirname(mapping.targetPath), { recursive: true });
82
+ await writeFile(mapping.targetPath, bytes);
83
+ if (mapping.mode != null)
84
+ await fsPromises.chmod(mapping.targetPath, mapping.mode);
85
+ filesTransferred += 1;
86
+ bytesTransferred += bytes.byteLength;
87
+ }
88
+ for (const command of operation.postUploadCommands ?? []) {
89
+ await execFile("sh", ["-c", command.command], { maxBuffer: 32 * 1024 * 1024 });
90
+ }
91
+ resultOperations.push({ operationId: operation.operationId, filesTransferred, bytesTransferred });
92
+ }
93
+ return { operations: resultOperations };
94
+ };
95
+ }
96
+ // A capturing `syncIn` that records every operation for assertion and
97
+ // materializes file AND directory mappings. A directory mapping (a referenced
98
+ // project) uses `mirrorDirectory`, so a test can assert the advisory `access`
99
+ // intent on directory mappings as well as file mappings. It reports a real
100
+ // `bytesTransferred` for a directory mapping too, from a post-mirror byte
101
+ // count, so a test can assert on the emitted progress line.
102
+ function attachCapturingSyncIn(client, captured) {
103
+ client.syncIn = async (operations) => {
104
+ const resultOperations = [];
105
+ for (const operation of operations) {
106
+ captured.push(operation);
107
+ let filesTransferred = 0;
108
+ let bytesTransferred = 0;
109
+ for (const mapping of operation.files) {
110
+ await mkdir(path.posix.dirname(mapping.targetPath), { recursive: true });
111
+ if (mapping.kind === "directory") {
112
+ await mirrorDirectory(mapping.sourcePath, mapping.targetPath);
113
+ bytesTransferred += await directoryByteSize(mapping.targetPath);
114
+ }
115
+ else {
116
+ const bytes = await readFile(mapping.sourcePath);
117
+ await writeFile(mapping.targetPath, bytes);
118
+ if (mapping.mode != null)
119
+ await fsPromises.chmod(mapping.targetPath, mapping.mode);
120
+ bytesTransferred += bytes.byteLength;
121
+ }
122
+ filesTransferred += 1;
123
+ }
124
+ for (const command of operation.postUploadCommands ?? []) {
125
+ await execFile("sh", ["-c", command.command], { maxBuffer: 32 * 1024 * 1024 });
126
+ }
127
+ resultOperations.push({ operationId: operation.operationId, filesTransferred, bytesTransferred });
128
+ }
129
+ return { operations: resultOperations };
130
+ };
131
+ }
132
+ vi.mock("node:fs", async (importOriginal) => {
133
+ const actual = await importOriginal();
134
+ return {
135
+ ...actual,
136
+ promises: {
137
+ ...actual.promises,
138
+ chmod: vi.fn(actual.promises.chmod),
139
+ rename: vi.fn(actual.promises.rename),
140
+ },
141
+ };
142
+ });
143
+ const execFile = promisify(execFileCallback);
144
+ async function git(cwd, args) {
145
+ const { stdout } = await execFile("git", ["-C", cwd, ...args], {
146
+ maxBuffer: 32 * 1024 * 1024,
147
+ });
148
+ return stdout.trim();
149
+ }
150
+ // A minimal committed Git repository, for the referenced-project ignore tests.
151
+ async function initGitRepo(repoDir) {
152
+ await mkdir(repoDir, { recursive: true });
153
+ await git(repoDir, ["init", "-q"]);
154
+ await git(repoDir, ["config", "user.name", "Paperclip Test"]);
155
+ await git(repoDir, ["config", "user.email", "test@paperclip.dev"]);
156
+ await writeFile(path.join(repoDir, "README.md"), "root\n", "utf8");
157
+ await git(repoDir, ["add", "README.md"]);
158
+ await git(repoDir, ["commit", "-qm", "base"]);
159
+ }
160
+ // A `CommandManagedRuntimeRunner` that runs real shell commands on the host
161
+ // filesystem (host FS stands in for the sandbox FS), exposing no native
162
+ // `syncIn` — staging rides the real base64/tar fallback, the same transport a
163
+ // provider without native sync uses. Unlike the in-file fake clients, this
164
+ // fallback DOES build a real tarball via `createTarballFromDirectory` for a
165
+ // `directory`-kind mapping, so it is the one that genuinely applies `exclude`.
166
+ function makeInlineSpawnRunner() {
167
+ return {
168
+ execute: (input) => new Promise((resolve) => {
169
+ const startedAt = new Date().toISOString();
170
+ const command = input.command === "sh" ? "/bin/sh" : input.command === "bash" ? "/bin/bash" : input.command;
171
+ const child = spawn(command, input.args ?? [], { cwd: input.cwd, env: { ...process.env, ...input.env } });
172
+ let stdout = "";
173
+ let stderr = "";
174
+ child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
175
+ child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
176
+ child.on("error", () => resolve({ exitCode: 127, signal: null, timedOut: false, stdout, stderr, pid: null, startedAt }));
177
+ child.on("close", (code) => resolve({ exitCode: code ?? 0, signal: null, timedOut: false, stdout, stderr, pid: child.pid ?? null, startedAt }));
178
+ if (input.stdin != null)
179
+ child.stdin.write(input.stdin);
180
+ child.stdin.end();
181
+ }),
182
+ };
183
+ }
184
+ // Stage ONE referenced project end-to-end (real tar fallback, real `exclude`
185
+ // filtering) through the full `prepareCommandManagedRuntime` seam, for the
186
+ // referenced-project ignore regression tests.
187
+ async function stageOneReferencedProject(referencedDir, ignoreResolution) {
188
+ const rootDir = path.dirname(referencedDir);
189
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
190
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
191
+ await mkdir(localWorkspaceDir, { recursive: true });
192
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "anchor\n", "utf8");
193
+ return await prepareCommandManagedRuntime({
194
+ runner: makeInlineSpawnRunner(),
195
+ spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
196
+ adapterKey: "test-adapter",
197
+ workspaceLocalDir: localWorkspaceDir,
198
+ additionalSources: [{ localPath: referencedDir, projectId: "proj", ignoreResolution }],
199
+ });
200
+ }
201
+ async function listTarMembers(rootDir, name, bytes) {
202
+ const tarPath = path.join(rootDir, name);
203
+ await writeFile(tarPath, bytes);
204
+ const { stdout } = await execFile("tar", ["-tf", tarPath], { maxBuffer: 32 * 1024 * 1024 });
205
+ return stdout.split("\n").map((line) => line.trim()).filter(Boolean);
206
+ }
207
+ // Build a filesystem-backed managed-runtime client. The host tarball path runs
208
+ // unchanged; the client just materializes the mappings on the local disk, so a
209
+ // pack-span test needs no provider.
210
+ function makeFilesystemClient() {
211
+ const client = {
212
+ makeDir: async (remotePath) => {
213
+ await mkdir(remotePath, { recursive: true });
214
+ },
215
+ writeFile: async (remotePath, bytes) => {
216
+ await mkdir(path.dirname(remotePath), { recursive: true });
217
+ await writeFile(remotePath, Buffer.from(bytes));
218
+ },
219
+ readFile: async (remotePath) => await readFile(remotePath),
220
+ listFiles: async (remotePath) => {
221
+ const entries = await readdir(remotePath, { withFileTypes: true }).catch(() => []);
222
+ return entries
223
+ .filter((entry) => entry.isFile())
224
+ .map((entry) => entry.name)
225
+ .sort((left, right) => left.localeCompare(right));
226
+ },
227
+ remove: async (remotePath) => {
228
+ await rm(remotePath, { recursive: true, force: true });
229
+ },
230
+ run: async (command) => {
231
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
232
+ },
233
+ };
234
+ attachFallbackSyncIn(client);
235
+ return client;
236
+ }
237
+ // A fake trace context that records every span and its parent by name. It
238
+ // satisfies the structural `StartupTraceContext` contract, so the real
239
+ // `createRuntimeSpanRunner` and `measureStartupStep` drive it unchanged. The
240
+ // opaque parent token is the parent's `RecordedSpan`, so a child span reads its
241
+ // parent name from the start context.
242
+ function createRecordingTraceContext() {
243
+ const spans = [];
244
+ const byHandle = new WeakMap();
245
+ const tracer = {
246
+ startSpan(name, options, context) {
247
+ const parent = context;
248
+ const record = {
249
+ name,
250
+ parentName: parent?.name ?? null,
251
+ ended: false,
252
+ attributes: { ...(options?.attributes ?? {}) },
253
+ };
254
+ spans.push(record);
255
+ const handle = {
256
+ setAttribute(key, value) {
257
+ record.attributes[key] = value;
258
+ },
259
+ setStatus() { },
260
+ end() {
261
+ record.ended = true;
262
+ },
263
+ };
264
+ byHandle.set(handle, record);
265
+ return handle;
266
+ },
267
+ };
268
+ const traceContext = {
269
+ tracer,
270
+ contextWithSpan: (span) => byHandle.get(span),
271
+ };
272
+ return { traceContext, spans };
273
+ }
274
+ describe("sandbox managed runtime", () => {
275
+ const cleanupDirs = [];
276
+ it.each(["host_current", "adopt_remote", "durable_seed"])("stages and restores both project repositories with independent Git histories (%s)", async (mode) => {
277
+ const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-multi-repo-"));
278
+ cleanupDirs.push(root);
279
+ const local = path.join(root, "local");
280
+ const remote = path.join(root, "remote");
281
+ const secondPath = ".paperclip-repositories/backend";
282
+ for (const [relative, contents] of [["", "frontend"], [secondPath, "backend"]]) {
283
+ const cwd = path.join(local, relative);
284
+ await mkdir(cwd, { recursive: true });
285
+ await git(cwd, ["init", "-b", "main"]);
286
+ await git(cwd, ["config", "user.name", "Test"]);
287
+ await git(cwd, ["config", "user.email", "test@example.com"]);
288
+ await writeFile(path.join(cwd, "README.md"), contents);
289
+ await writeFile(path.join(cwd, ".gitignore"), "secret.txt\n");
290
+ await git(cwd, ["add", "."]);
291
+ await git(cwd, ["commit", "-m", contents]);
292
+ await writeFile(path.join(cwd, "secret.txt"), "must stay local");
293
+ }
294
+ await writeFile(path.join(local, ".git/info/exclude"), ".paperclip-repositories/\n");
295
+ await writeFile(path.join(local, secondPath, "dirty.txt"), "local edit");
296
+ await writeFile(path.join(local, secondPath, "host-config.txt"), "excluded by operator");
297
+ const seed = { workspaceArchivePath: path.join(root, "workspace.tar"), gitArchivePath: path.join(root, "git.tar") };
298
+ const input = {
299
+ spec: { transport: "sandbox", provider: "test", sandboxId: "two-repos", remoteCwd: remote, timeoutMs: 30_000, apiKey: null },
300
+ client: makeFilesystemClient(), adapterKey: "test", workspaceLocalDir: local,
301
+ workspaceDurableSeed: seed,
302
+ workspaceExclude: [`${secondPath}/host-config.txt`],
303
+ };
304
+ let prepared = await prepareSandboxManagedRuntime(input);
305
+ if (mode !== "host_current") {
306
+ if (mode === "durable_seed")
307
+ await rm(remote, { recursive: true, force: true });
308
+ await writeFile(path.join(local, secondPath, "host-only.txt"), "concurrent host work");
309
+ prepared = await prepareSandboxManagedRuntime({
310
+ ...input, workspaceInboundMode: mode,
311
+ workspaceBaseline: prepared.workspaceSyncSnapshot.baseline,
312
+ workspaceGitSnapshot: prepared.workspaceSyncSnapshot.gitSnapshot,
313
+ });
314
+ }
315
+ for (const relative of ["", secondPath]) {
316
+ const cwd = path.join(remote, relative);
317
+ expect((await lstat(path.join(cwd, ".git"))).isDirectory()).toBe(true);
318
+ await expect(stat(path.join(cwd, "secret.txt"))).rejects.toMatchObject({ code: "ENOENT" });
319
+ await writeFile(path.join(cwd, "README.md"), `updated ${relative}`);
320
+ await git(cwd, ["-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-am", "remote change"]);
321
+ }
322
+ expect(await readFile(path.join(remote, secondPath, "dirty.txt"), "utf8")).toBe("local edit");
323
+ await expect(stat(path.join(remote, secondPath, "host-config.txt"))).rejects.toMatchObject({ code: "ENOENT" });
324
+ await writeFile(path.join(remote, secondPath, "host-config.txt"), "remote must not replace host config");
325
+ await prepared.restoreWorkspace();
326
+ expect(await readFile(path.join(local, secondPath, "host-config.txt"), "utf8")).toBe("excluded by operator");
327
+ if (mode !== "host_current")
328
+ expect(await readFile(path.join(local, secondPath, "host-only.txt"), "utf8")).toBe("concurrent host work");
329
+ for (const relative of ["", secondPath]) {
330
+ expect(await readFile(path.join(local, relative, "README.md"), "utf8")).toBe(`updated ${relative}`);
331
+ expect(await git(path.join(local, relative), ["log", "-1", "--format=%s"])).toBe("remote change");
332
+ expect(await readFile(path.join(local, relative, "secret.txt"), "utf8")).toBe("must stay local");
333
+ }
334
+ }, 30_000);
335
+ afterEach(async () => {
336
+ while (cleanupDirs.length > 0) {
337
+ const dir = cleanupDirs.pop();
338
+ if (!dir)
339
+ continue;
340
+ await rm(dir, { recursive: true, force: true }).catch(() => undefined);
341
+ }
342
+ });
343
+ it("adopts a warm remote workspace without inbound overwrite and still merges outbound changes", async () => {
344
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-adopt-"));
345
+ cleanupDirs.push(rootDir);
346
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
347
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
348
+ await mkdir(localWorkspaceDir, { recursive: true });
349
+ await mkdir(remoteWorkspaceDir, { recursive: true });
350
+ await writeFile(path.join(localWorkspaceDir, "continuity.txt"), "host baseline\n", "utf8");
351
+ await writeFile(path.join(remoteWorkspaceDir, "continuity.txt"), "remote retained\n", "utf8");
352
+ const client = makeFilesystemClient();
353
+ const syncIn = vi.spyOn(client, "syncIn");
354
+ const prepared = await prepareSandboxManagedRuntime({
355
+ spec: {
356
+ transport: "sandbox",
357
+ provider: "test",
358
+ sandboxId: "sandbox-warm",
359
+ remoteCwd: remoteWorkspaceDir,
360
+ timeoutMs: 30_000,
361
+ apiKey: null,
362
+ },
363
+ adapterKey: "test-adapter",
364
+ client,
365
+ workspaceLocalDir: localWorkspaceDir,
366
+ workspaceInboundMode: "adopt_remote",
367
+ });
368
+ expect(syncIn).not.toHaveBeenCalled();
369
+ await expect(readFile(path.join(remoteWorkspaceDir, "continuity.txt"), "utf8")).resolves.toBe("remote retained\n");
370
+ expect(prepared.workspaceSyncSnapshot).not.toBeNull();
371
+ await writeFile(path.join(remoteWorkspaceDir, "continuity.txt"), "remote finalized\n", "utf8");
372
+ await prepared.restoreWorkspace();
373
+ await expect(readFile(path.join(localWorkspaceDir, "continuity.txt"), "utf8")).resolves.toBe("remote finalized\n");
374
+ });
375
+ it("reconstructs a replacement workspace from the exact durable pre-turn seed", async () => {
376
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-durable-seed-"));
377
+ cleanupDirs.push(rootDir);
378
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
379
+ const firstRemoteDir = path.join(rootDir, "first-remote");
380
+ const replacementRemoteDir = path.join(rootDir, "replacement-remote");
381
+ const durableSeed = {
382
+ workspaceArchivePath: path.join(rootDir, "state", "workspace.tar"),
383
+ gitArchivePath: path.join(rootDir, "state", "git.tar"),
384
+ };
385
+ await mkdir(localWorkspaceDir, { recursive: true });
386
+ await writeFile(path.join(localWorkspaceDir, "continuity.txt"), "durable pre-turn bytes\n", "utf8");
387
+ const first = await prepareSandboxManagedRuntime({
388
+ spec: {
389
+ transport: "sandbox",
390
+ provider: "test",
391
+ sandboxId: "sandbox-first",
392
+ remoteCwd: firstRemoteDir,
393
+ timeoutMs: 30_000,
394
+ apiKey: null,
395
+ },
396
+ adapterKey: "test-adapter",
397
+ client: makeFilesystemClient(),
398
+ workspaceLocalDir: localWorkspaceDir,
399
+ workspaceInboundMode: "host_current",
400
+ workspaceDurableSeed: durableSeed,
401
+ });
402
+ expect(first.workspaceSyncSnapshot).not.toBeNull();
403
+ await expect(stat(durableSeed.workspaceArchivePath)).resolves.toMatchObject({
404
+ mode: expect.any(Number),
405
+ });
406
+ await writeFile(path.join(localWorkspaceDir, "continuity.txt"), "concurrent host edit must not enter replacement\n", "utf8");
407
+ await prepareSandboxManagedRuntime({
408
+ spec: {
409
+ transport: "sandbox",
410
+ provider: "test",
411
+ sandboxId: "sandbox-replacement",
412
+ remoteCwd: replacementRemoteDir,
413
+ timeoutMs: 30_000,
414
+ apiKey: null,
415
+ },
416
+ adapterKey: "test-adapter",
417
+ client: makeFilesystemClient(),
418
+ workspaceLocalDir: localWorkspaceDir,
419
+ workspaceInboundMode: "durable_seed",
420
+ workspaceDurableSeed: durableSeed,
421
+ workspaceBaseline: first.workspaceSyncSnapshot.baseline,
422
+ workspaceGitSnapshot: first.workspaceSyncSnapshot.gitSnapshot,
423
+ });
424
+ await expect(readFile(path.join(replacementRemoteDir, "continuity.txt"), "utf8")).resolves.toBe("durable pre-turn bytes\n");
425
+ });
426
+ it("preserves excluded local workspace artifacts during restore mirroring", async () => {
427
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-restore-"));
428
+ cleanupDirs.push(rootDir);
429
+ const sourceDir = path.join(rootDir, "source");
430
+ const targetDir = path.join(rootDir, "target");
431
+ await mkdir(path.join(sourceDir, "src"), { recursive: true });
432
+ await mkdir(path.join(targetDir, ".claude"), { recursive: true });
433
+ await mkdir(path.join(targetDir, ".paperclip-runtime"), { recursive: true });
434
+ await writeFile(path.join(sourceDir, "src", "app.ts"), "export const value = 2;\n", "utf8");
435
+ await writeFile(path.join(targetDir, "stale.txt"), "remove me\n", "utf8");
436
+ await writeFile(path.join(targetDir, ".claude", "settings.json"), "{\"keep\":true}\n", "utf8");
437
+ await writeFile(path.join(targetDir, ".claude.json"), "{\"keep\":true}\n", "utf8");
438
+ await writeFile(path.join(targetDir, ".paperclip-runtime", "state.json"), "{}\n", "utf8");
439
+ await mirrorDirectory(sourceDir, targetDir, {
440
+ preserveAbsent: [".paperclip-runtime", ".claude", ".claude.json"],
441
+ });
442
+ await expect(readFile(path.join(targetDir, "src", "app.ts"), "utf8")).resolves.toBe("export const value = 2;\n");
443
+ await expect(readFile(path.join(targetDir, ".claude", "settings.json"), "utf8")).resolves.toBe("{\"keep\":true}\n");
444
+ await expect(readFile(path.join(targetDir, ".claude.json"), "utf8")).resolves.toBe("{\"keep\":true}\n");
445
+ await expect(readFile(path.join(targetDir, ".paperclip-runtime", "state.json"), "utf8")).resolves.toBe("{}\n");
446
+ await expect(readFile(path.join(targetDir, "stale.txt"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
447
+ });
448
+ it("applies file mode on a staged sibling before renaming into place", async () => {
449
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-copy-mode-"));
450
+ cleanupDirs.push(rootDir);
451
+ const sourceDir = path.join(rootDir, "source");
452
+ const targetDir = path.join(rootDir, "target");
453
+ const relativePath = path.join("nested", "script.sh");
454
+ const sourcePath = path.join(sourceDir, relativePath);
455
+ const targetPath = path.join(targetDir, relativePath);
456
+ await mkdir(path.dirname(sourcePath), { recursive: true });
457
+ await writeFile(sourcePath, "#!/bin/sh\necho hello\n", { mode: 0o600 });
458
+ await mkdir(targetDir, { recursive: true });
459
+ const chmodMock = vi.mocked(fsPromises.chmod);
460
+ const renameMock = vi.mocked(fsPromises.rename);
461
+ chmodMock.mockClear();
462
+ renameMock.mockClear();
463
+ await mirrorDirectory(sourceDir, targetDir);
464
+ await expect(readFile(targetPath, "utf8")).resolves.toBe("#!/bin/sh\necho hello\n");
465
+ expect(chmodMock).toHaveBeenCalledTimes(1);
466
+ expect(renameMock).toHaveBeenCalledTimes(1);
467
+ expect(chmodMock.mock.calls[0]?.[0]).toContain(".paperclip-copy.");
468
+ expect(chmodMock.mock.calls[0]?.[0]).not.toBe(targetPath);
469
+ expect(chmodMock.mock.invocationCallOrder[0]).toBeLessThan(renameMock.mock.invocationCallOrder[0]);
470
+ });
471
+ it("cleans up a staged sibling when chmod fails before rename", async () => {
472
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-copy-cleanup-"));
473
+ cleanupDirs.push(rootDir);
474
+ const sourceDir = path.join(rootDir, "source");
475
+ const targetDir = path.join(rootDir, "target");
476
+ const relativePath = path.join("nested", "script.sh");
477
+ const sourcePath = path.join(sourceDir, relativePath);
478
+ const targetPath = path.join(targetDir, relativePath);
479
+ await mkdir(path.dirname(sourcePath), { recursive: true });
480
+ await writeFile(sourcePath, "#!/bin/sh\necho hello\n", { mode: 0o600 });
481
+ await mkdir(targetDir, { recursive: true });
482
+ const chmodMock = vi.mocked(fsPromises.chmod);
483
+ const renameMock = vi.mocked(fsPromises.rename);
484
+ chmodMock.mockClear();
485
+ renameMock.mockClear();
486
+ chmodMock.mockImplementationOnce(async () => {
487
+ throw new Error("chmod failed");
488
+ });
489
+ await expect(mirrorDirectory(sourceDir, targetDir)).rejects.toThrow(/chmod failed/);
490
+ expect(chmodMock).toHaveBeenCalledTimes(1);
491
+ expect(renameMock).not.toHaveBeenCalled();
492
+ const stagedPath = chmodMock.mock.calls[0]?.[0];
493
+ expect(stagedPath).toContain(".paperclip-copy.");
494
+ await expect(readFile(stagedPath, "utf8")).rejects.toMatchObject({ code: "ENOENT" });
495
+ await expect(readFile(targetPath, "utf8")).rejects.toMatchObject({ code: "ENOENT" });
496
+ });
497
+ it("syncs workspace and assets through a provider-neutral sandbox client", async () => {
498
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-managed-"));
499
+ cleanupDirs.push(rootDir);
500
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
501
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
502
+ const localAssetsDir = path.join(rootDir, "local-assets");
503
+ const linkedAssetPath = path.join(rootDir, "linked-skill.md");
504
+ await mkdir(path.join(localWorkspaceDir, ".claude"), { recursive: true });
505
+ await mkdir(localAssetsDir, { recursive: true });
506
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "local workspace\n", "utf8");
507
+ await writeFile(path.join(localWorkspaceDir, "._README.md"), "appledouble\n", "utf8");
508
+ await writeFile(path.join(localWorkspaceDir, ".claude", "settings.json"), "{\"local\":true}\n", "utf8");
509
+ await writeFile(linkedAssetPath, "skill body\n", "utf8");
510
+ await symlink(linkedAssetPath, path.join(localAssetsDir, "skill.md"));
511
+ const client = {
512
+ makeDir: async (remotePath) => {
513
+ await mkdir(remotePath, { recursive: true });
514
+ },
515
+ writeFile: async (remotePath, bytes) => {
516
+ await mkdir(path.dirname(remotePath), { recursive: true });
517
+ await writeFile(remotePath, Buffer.from(bytes));
518
+ },
519
+ readFile: async (remotePath) => await readFile(remotePath),
520
+ listFiles: async (remotePath) => {
521
+ const entries = await readdir(remotePath, { withFileTypes: true }).catch(() => []);
522
+ return entries
523
+ .filter((entry) => entry.isFile())
524
+ .map((entry) => entry.name)
525
+ .sort((left, right) => left.localeCompare(right));
526
+ },
527
+ remove: async (remotePath) => {
528
+ await rm(remotePath, { recursive: true, force: true });
529
+ },
530
+ run: async (command) => {
531
+ await execFile("sh", ["-c", command], {
532
+ maxBuffer: 32 * 1024 * 1024,
533
+ });
534
+ },
535
+ };
536
+ const runtimeStatuses = [];
537
+ attachFallbackSyncIn(client);
538
+ const prepared = await prepareSandboxManagedRuntime({
539
+ spec: {
540
+ transport: "sandbox",
541
+ provider: "test",
542
+ sandboxId: "sandbox-1",
543
+ remoteCwd: remoteWorkspaceDir,
544
+ timeoutMs: 30_000,
545
+ apiKey: null,
546
+ },
547
+ adapterKey: "test-adapter",
548
+ client,
549
+ workspaceLocalDir: localWorkspaceDir,
550
+ workspaceExclude: [".claude"],
551
+ preserveAbsentOnRestore: [".claude"],
552
+ onRuntimeProgress: async (status) => {
553
+ runtimeStatuses.push(`${status.phase}:${status.message}`);
554
+ },
555
+ assets: [{
556
+ key: "skills",
557
+ localDir: localAssetsDir,
558
+ followSymlinks: true,
559
+ }],
560
+ });
561
+ await expect(readFile(path.join(remoteWorkspaceDir, "README.md"), "utf8")).resolves.toBe("local workspace\n");
562
+ await expect(readFile(path.join(remoteWorkspaceDir, "._README.md"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
563
+ await expect(readFile(path.join(remoteWorkspaceDir, ".claude", "settings.json"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
564
+ await expect(readFile(path.join(prepared.assetDirs.skills, "skill.md"), "utf8")).resolves.toBe("skill body\n");
565
+ expect((await lstat(path.join(prepared.assetDirs.skills, "skill.md"))).isFile()).toBe(true);
566
+ await writeFile(path.join(remoteWorkspaceDir, "README.md"), "remote workspace\n", "utf8");
567
+ await writeFile(path.join(remoteWorkspaceDir, "remote-only.txt"), "sync back\n", "utf8");
568
+ await mkdir(path.join(localWorkspaceDir, ".paperclip-runtime"), { recursive: true });
569
+ await writeFile(path.join(localWorkspaceDir, ".paperclip-runtime", "state.json"), "{}\n", "utf8");
570
+ await writeFile(path.join(localWorkspaceDir, "local-stale.txt"), "remove\n", "utf8");
571
+ await prepared.restoreWorkspace();
572
+ await expect(readFile(path.join(localWorkspaceDir, "README.md"), "utf8")).resolves.toBe("remote workspace\n");
573
+ await expect(readFile(path.join(localWorkspaceDir, "remote-only.txt"), "utf8")).resolves.toBe("sync back\n");
574
+ await expect(readFile(path.join(localWorkspaceDir, "local-stale.txt"), "utf8")).resolves.toBe("remove\n");
575
+ await expect(readFile(path.join(localWorkspaceDir, ".claude", "settings.json"), "utf8")).resolves.toBe("{\"local\":true}\n");
576
+ await expect(readFile(path.join(localWorkspaceDir, ".paperclip-runtime", "state.json"), "utf8")).resolves.toBe("{}\n");
577
+ expect(runtimeStatuses).toEqual(expect.arrayContaining([
578
+ "config_sync:Syncing workspace to environment",
579
+ "config_sync:Syncing runtime assets to environment",
580
+ "restore:Restoring workspace from environment",
581
+ "finalize:Finalizing workspace",
582
+ ]));
583
+ expect(runtimeStatuses).toEqual(expect.arrayContaining([
584
+ expect.stringMatching(/^config_sync:Syncing workspace to environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/),
585
+ expect.stringMatching(/^config_sync:Syncing skills to environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/),
586
+ expect.stringMatching(/^restore:Restoring workspace from environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/),
587
+ ]));
588
+ expect(runtimeStatuses.at(-1)).toBe("finalize:Finalizing workspace");
589
+ });
590
+ it("falls back to the host-known byte count when the provider reports 0 for inbound workspace sync", async () => {
591
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-zero-report-"));
592
+ cleanupDirs.push(rootDir);
593
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
594
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
595
+ await mkdir(localWorkspaceDir, { recursive: true });
596
+ // Sizeable content, well above the 0.1 MB rounding step, so a fallback bug
597
+ // that stays at 0 is distinguishable from a small transfer that would
598
+ // still round down to "0.0 MB".
599
+ await writeFile(path.join(localWorkspaceDir, "large.bin"), Buffer.alloc(300 * 1024, "a"));
600
+ const client = makeFilesystemClient();
601
+ const realSyncIn = client.syncIn;
602
+ // Simulate a provider that under-reports: it moves the real bytes but
603
+ // returns 0 in `bytesTransferred`, exactly like a buggy or minimal plugin.
604
+ client.syncIn = async (operations) => {
605
+ const result = await realSyncIn(operations);
606
+ return {
607
+ operations: result.operations.map((operation) => ({ ...operation, bytesTransferred: 0 })),
608
+ };
609
+ };
610
+ const lines = [];
611
+ await prepareSandboxManagedRuntime({
612
+ spec: {
613
+ transport: "sandbox",
614
+ provider: "test",
615
+ sandboxId: "sandbox-1",
616
+ remoteCwd: remoteWorkspaceDir,
617
+ timeoutMs: 30_000,
618
+ apiKey: null,
619
+ },
620
+ adapterKey: "test-adapter",
621
+ client,
622
+ workspaceLocalDir: localWorkspaceDir,
623
+ onProgress: (line) => { lines.push(line); },
624
+ });
625
+ await expect(readFile(path.join(remoteWorkspaceDir, "large.bin"))).resolves.toHaveLength(300 * 1024);
626
+ const workspaceLines = lines.filter((line) => line.includes("Syncing workspace to environment"));
627
+ expect(workspaceLines.length).toBeGreaterThan(0);
628
+ // Even though the provider reported 0, the line still shows the real,
629
+ // host-known workspace size, from the caller-supplied fallback.
630
+ expect(workspaceLines.some((line) => /\(\d+\.\d\/\d+\.\d MB\)/.test(line))).toBe(true);
631
+ expect(workspaceLines.some((line) => line.includes("(0.0/0.0 MB)"))).toBe(false);
632
+ });
633
+ it.each(["workspace", "git-workspace"])("rejects an asset key that collides with the reserved %s archive name", async (reservedKey) => {
634
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-asset-key-"));
635
+ cleanupDirs.push(rootDir);
636
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
637
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
638
+ const localAssetsDir = path.join(rootDir, "local-assets");
639
+ await mkdir(localWorkspaceDir, { recursive: true });
640
+ await mkdir(localAssetsDir, { recursive: true });
641
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
642
+ const client = makeFilesystemClient();
643
+ await expect(prepareSandboxManagedRuntime({
644
+ spec: {
645
+ transport: "sandbox",
646
+ provider: "test",
647
+ sandboxId: "sandbox-1",
648
+ remoteCwd: remoteWorkspaceDir,
649
+ timeoutMs: 30_000,
650
+ apiKey: null,
651
+ },
652
+ adapterKey: "test-adapter",
653
+ client,
654
+ workspaceLocalDir: localWorkspaceDir,
655
+ assets: [{ key: reservedKey, localDir: localAssetsDir }],
656
+ })).rejects.toThrow(/collides with a reserved runtime archive name/);
657
+ // The reserved-key guard fails before any workspace or asset archive is
658
+ // built, so nothing lands in the remote workspace directory.
659
+ await expect(readdir(remoteWorkspaceDir)).rejects.toMatchObject({ code: "ENOENT" });
660
+ });
661
+ it.each(["skills/nested", "skills\\nested", "..", "../escape"])("rejects an asset key that is not a simple path segment: %s", async (unsafeKey) => {
662
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-asset-key-"));
663
+ cleanupDirs.push(rootDir);
664
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
665
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
666
+ const localAssetsDir = path.join(rootDir, "local-assets");
667
+ await mkdir(localWorkspaceDir, { recursive: true });
668
+ await mkdir(localAssetsDir, { recursive: true });
669
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
670
+ const client = makeFilesystemClient();
671
+ await expect(prepareSandboxManagedRuntime({
672
+ spec: {
673
+ transport: "sandbox",
674
+ provider: "test",
675
+ sandboxId: "sandbox-1",
676
+ remoteCwd: remoteWorkspaceDir,
677
+ timeoutMs: 30_000,
678
+ apiKey: null,
679
+ },
680
+ adapterKey: "test-adapter",
681
+ client,
682
+ workspaceLocalDir: localWorkspaceDir,
683
+ assets: [{ key: unsafeKey, localDir: localAssetsDir }],
684
+ })).rejects.toThrow(/is not a simple path segment/);
685
+ });
686
+ it("syncs git-backed workspaces through a shallow standalone clone and keeps .git out of archives", async () => {
687
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-git-"));
688
+ cleanupDirs.push(rootDir);
689
+ const sourceRepoDir = path.join(rootDir, "source-repo");
690
+ const localWorkspaceDir = path.join(rootDir, "local-worktree");
691
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
692
+ await mkdir(sourceRepoDir, { recursive: true });
693
+ await git(sourceRepoDir, ["init"]);
694
+ await git(sourceRepoDir, ["checkout", "-b", "main"]);
695
+ await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
696
+ await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
697
+ await writeFile(path.join(sourceRepoDir, ".gitignore"), "node_modules/\n", "utf8");
698
+ await writeFile(path.join(sourceRepoDir, "tracked.txt"), "base\n", "utf8");
699
+ await writeFile(path.join(sourceRepoDir, "clean.txt"), "from git\n", "utf8");
700
+ await writeFile(path.join(sourceRepoDir, "deleted.txt"), "delete me\n", "utf8");
701
+ await git(sourceRepoDir, ["add", ".gitignore", "tracked.txt", "clean.txt", "deleted.txt"]);
702
+ await git(sourceRepoDir, ["commit", "-m", "base"]);
703
+ await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
704
+ expect((await lstat(path.join(localWorkspaceDir, ".git"))).isFile()).toBe(true);
705
+ await mkdir(path.join(localWorkspaceDir, "node_modules"), { recursive: true });
706
+ await writeFile(path.join(localWorkspaceDir, "tracked.txt"), "dirty local\n", "utf8");
707
+ await writeFile(path.join(localWorkspaceDir, "untracked.txt"), "from local\n", "utf8");
708
+ await writeFile(path.join(localWorkspaceDir, "node_modules", "cache.bin"), "do not upload\n", "utf8");
709
+ await rm(path.join(localWorkspaceDir, "deleted.txt"));
710
+ const uploadedTars = [];
711
+ const downloadedTars = [];
712
+ const driveProgress = async (total, onProgress) => {
713
+ if (!onProgress)
714
+ return;
715
+ await onProgress(Math.max(1, Math.floor(total / 2)), total);
716
+ await onProgress(total, total);
717
+ };
718
+ const client = {
719
+ makeDir: async (remotePath) => {
720
+ await mkdir(remotePath, { recursive: true });
721
+ },
722
+ writeFile: async (remotePath, bytes, options) => {
723
+ await mkdir(path.dirname(remotePath), { recursive: true });
724
+ const buffer = Buffer.from(bytes);
725
+ if (remotePath.endsWith("-upload.tar"))
726
+ uploadedTars.push({ remotePath, bytes: buffer });
727
+ await writeFile(remotePath, buffer);
728
+ await driveProgress(buffer.byteLength, options?.onProgress);
729
+ },
730
+ readFile: async (remotePath, options) => {
731
+ const buffer = await readFile(remotePath);
732
+ if (remotePath.endsWith("workspace-download.tar"))
733
+ downloadedTars.push({ remotePath, bytes: buffer });
734
+ await driveProgress(buffer.byteLength, options?.onProgress);
735
+ return buffer;
736
+ },
737
+ listFiles: async () => [],
738
+ remove: async (remotePath) => {
739
+ await rm(remotePath, { recursive: true, force: true });
740
+ },
741
+ run: async (command) => {
742
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
743
+ },
744
+ };
745
+ const runtimeStatuses = [];
746
+ attachFallbackSyncIn(client);
747
+ const prepared = await prepareSandboxManagedRuntime({
748
+ spec: {
749
+ transport: "sandbox",
750
+ provider: "test",
751
+ sandboxId: "sandbox-1",
752
+ remoteCwd: remoteWorkspaceDir,
753
+ timeoutMs: 30_000,
754
+ apiKey: null,
755
+ },
756
+ adapterKey: "test-adapter",
757
+ client,
758
+ workspaceLocalDir: localWorkspaceDir,
759
+ onRuntimeProgress: async (status) => {
760
+ runtimeStatuses.push({ phase: status.phase, message: status.message });
761
+ },
762
+ });
763
+ expect((await lstat(path.join(remoteWorkspaceDir, ".git"))).isDirectory()).toBe(true);
764
+ await expect(readFile(path.join(remoteWorkspaceDir, ".git", "shallow"), "utf8")).resolves.toContain(await git(localWorkspaceDir, ["rev-parse", "HEAD"]));
765
+ expect(await git(remoteWorkspaceDir, ["rev-list", "--count", "HEAD"])).toBe("1");
766
+ expect(await git(remoteWorkspaceDir, ["status", "--short"])).toContain("M tracked.txt");
767
+ expect(await git(remoteWorkspaceDir, ["status", "--short"])).toContain("?? untracked.txt");
768
+ await expect(readFile(path.join(remoteWorkspaceDir, "deleted.txt"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
769
+ const gitUpload = uploadedTars.find((entry) => path.posix.basename(entry.remotePath) === "git-workspace-upload.tar");
770
+ const workspaceUpload = uploadedTars.find((entry) => path.posix.basename(entry.remotePath) === "workspace-upload.tar");
771
+ expect(gitUpload).toBeDefined();
772
+ expect(workspaceUpload).toBeDefined();
773
+ const gitMembers = await listTarMembers(rootDir, "git-upload-list.tar", gitUpload.bytes);
774
+ const workspaceMembers = await listTarMembers(rootDir, "workspace-upload-list.tar", workspaceUpload.bytes);
775
+ expect(gitMembers.some((entry) => entry === ".git" || entry.startsWith(".git/"))).toBe(true);
776
+ expect(workspaceMembers.some((entry) => entry === ".git" || entry.startsWith(".git/"))).toBe(false);
777
+ expect(workspaceMembers).toContain("tracked.txt");
778
+ expect(workspaceMembers).toContain("untracked.txt");
779
+ expect(workspaceMembers).not.toContain("clean.txt");
780
+ expect(workspaceMembers.some((entry) => entry === "node_modules" || entry.startsWith("node_modules/"))).toBe(false);
781
+ await git(remoteWorkspaceDir, ["config", "user.name", "Paperclip Sandbox"]);
782
+ await git(remoteWorkspaceDir, ["config", "user.email", "sandbox@paperclip.dev"]);
783
+ await git(remoteWorkspaceDir, ["add", "-A"]);
784
+ await git(remoteWorkspaceDir, ["commit", "-m", "sandbox update"]);
785
+ await writeFile(path.join(remoteWorkspaceDir, "tracked.txt"), "remote dirty\n", "utf8");
786
+ await writeFile(path.join(remoteWorkspaceDir, "remote-only.txt"), "from sandbox\n", "utf8");
787
+ await prepared.restoreWorkspace();
788
+ expect((await lstat(path.join(localWorkspaceDir, ".git"))).isFile()).toBe(true);
789
+ expect(await git(localWorkspaceDir, ["log", "-1", "--pretty=%s"])).toBe("sandbox update");
790
+ await expect(readFile(path.join(localWorkspaceDir, "tracked.txt"), "utf8")).resolves.toBe("remote dirty\n");
791
+ await expect(readFile(path.join(localWorkspaceDir, "remote-only.txt"), "utf8")).resolves.toBe("from sandbox\n");
792
+ await expect(readFile(path.join(localWorkspaceDir, "deleted.txt"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
793
+ await expect(readFile(path.join(localWorkspaceDir, "node_modules", "cache.bin"), "utf8")).resolves.toBe("do not upload\n");
794
+ expect(downloadedTars).toHaveLength(1);
795
+ const downloadMembers = await listTarMembers(rootDir, "workspace-download-list.tar", downloadedTars[0].bytes);
796
+ expect(downloadMembers.some((entry) => entry === ".git" || entry.startsWith(".git/"))).toBe(false);
797
+ expect(downloadMembers.some((entry) => entry === "node_modules" || entry.startsWith("node_modules/"))).toBe(false);
798
+ expect(runtimeStatuses.map((status) => status.phase)).toEqual(expect.arrayContaining([
799
+ "git_sync",
800
+ "config_sync",
801
+ "export",
802
+ "restore",
803
+ "finalize",
804
+ ]));
805
+ // Git history and workspace overlay sync as ONE merged operation, so a single
806
+ // transfer-progress event rides the config_sync (workspace) phase. The git_sync
807
+ // phase still emits its plain status message (asserted by the arrayContaining
808
+ // check above).
809
+ expect(runtimeStatuses.some((status) => (status.phase === "config_sync" &&
810
+ /^Syncing workspace to environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/.test(status.message)))).toBe(true);
811
+ expect(runtimeStatuses.some((status) => (status.phase === "export" &&
812
+ /^Exporting git history from environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/.test(status.message)))).toBe(true);
813
+ });
814
+ it("repairs stale host index deletions when the sandbox restores a clean git worktree", async () => {
815
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-clean-restore-"));
816
+ cleanupDirs.push(rootDir);
817
+ const sourceRepoDir = path.join(rootDir, "source-repo");
818
+ const localWorkspaceDir = path.join(rootDir, "local-worktree");
819
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
820
+ await mkdir(sourceRepoDir, { recursive: true });
821
+ await git(sourceRepoDir, ["init"]);
822
+ await git(sourceRepoDir, ["checkout", "-b", "main"]);
823
+ await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
824
+ await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
825
+ await writeFile(path.join(sourceRepoDir, "kept.txt"), "kept\n", "utf8");
826
+ await writeFile(path.join(sourceRepoDir, "restored.txt"), "restored\n", "utf8");
827
+ await git(sourceRepoDir, ["add", "kept.txt", "restored.txt"]);
828
+ await git(sourceRepoDir, ["commit", "-m", "base"]);
829
+ await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
830
+ await git(localWorkspaceDir, ["rm", "restored.txt"]);
831
+ expect(await git(localWorkspaceDir, ["status", "--short"])).toContain("D restored.txt");
832
+ const missingStatusReads = [];
833
+ const client = {
834
+ makeDir: async (remotePath) => {
835
+ await mkdir(remotePath, { recursive: true });
836
+ },
837
+ writeFile: async (remotePath, bytes) => {
838
+ await mkdir(path.dirname(remotePath), { recursive: true });
839
+ await writeFile(remotePath, Buffer.from(bytes));
840
+ },
841
+ readFile: async (remotePath) => {
842
+ if (remotePath.endsWith("workspace-status.txt")) {
843
+ missingStatusReads.push(remotePath);
844
+ throw new Error("status file unavailable");
845
+ }
846
+ return await readFile(remotePath);
847
+ },
848
+ listFiles: async () => [],
849
+ remove: async (remotePath) => {
850
+ await rm(remotePath, { recursive: true, force: true });
851
+ },
852
+ run: async (command) => {
853
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
854
+ },
855
+ };
856
+ attachFallbackSyncIn(client);
857
+ const prepared = await prepareSandboxManagedRuntime({
858
+ spec: {
859
+ transport: "sandbox",
860
+ provider: "test",
861
+ sandboxId: "sandbox-1",
862
+ remoteCwd: remoteWorkspaceDir,
863
+ timeoutMs: 30_000,
864
+ apiKey: null,
865
+ },
866
+ adapterKey: "test-adapter",
867
+ client,
868
+ workspaceLocalDir: localWorkspaceDir,
869
+ });
870
+ expect(await git(remoteWorkspaceDir, ["status", "--short"])).toContain("D restored.txt");
871
+ await git(remoteWorkspaceDir, ["reset", "--hard", "HEAD"]);
872
+ expect(await git(remoteWorkspaceDir, ["status", "--short"])).toBe("");
873
+ await prepared.restoreWorkspace();
874
+ await expect(readFile(path.join(localWorkspaceDir, "restored.txt"), "utf8")).resolves.toBe("restored\n");
875
+ expect(await git(localWorkspaceDir, ["ls-files", "restored.txt"])).toBe("restored.txt");
876
+ expect(await git(localWorkspaceDir, ["status", "--short"])).toBe("");
877
+ expect(await git(localWorkspaceDir, ["diff", "--name-status", "HEAD", "--"])).toBe("");
878
+ expect(await git(localWorkspaceDir, ["diff", "--cached", "--name-status", "HEAD", "--"])).toBe("");
879
+ expect(missingStatusReads).toHaveLength(1);
880
+ });
881
+ it("does not fail clean restore checks when local working tree changes survive", async () => {
882
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-preserved-local-"));
883
+ cleanupDirs.push(rootDir);
884
+ const sourceRepoDir = path.join(rootDir, "source-repo");
885
+ const localWorkspaceDir = path.join(rootDir, "local-worktree");
886
+ await mkdir(sourceRepoDir, { recursive: true });
887
+ await git(sourceRepoDir, ["init"]);
888
+ await git(sourceRepoDir, ["checkout", "-b", "main"]);
889
+ await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
890
+ await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
891
+ await writeFile(path.join(sourceRepoDir, "kept.txt"), "base\n", "utf8");
892
+ await git(sourceRepoDir, ["add", "kept.txt"]);
893
+ await git(sourceRepoDir, ["commit", "-m", "base"]);
894
+ await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
895
+ await writeFile(path.join(localWorkspaceDir, "kept.txt"), "local user change\n", "utf8");
896
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
897
+ try {
898
+ await resetLocalGitIndexToHead({
899
+ localDir: localWorkspaceDir,
900
+ checkWorkingTreeClean: true,
901
+ });
902
+ expect(warnSpy).toHaveBeenCalledWith("[paperclip] Workspace restore preserved local working tree changes after clean sandbox restore.");
903
+ }
904
+ finally {
905
+ warnSpy.mockRestore();
906
+ }
907
+ await expect(readFile(path.join(localWorkspaceDir, "kept.txt"), "utf8")).resolves.toBe("local user change\n");
908
+ expect(await git(localWorkspaceDir, ["diff", "--cached", "--name-status", "HEAD", "--"])).toBe("");
909
+ expect(await git(localWorkspaceDir, ["status", "--short"])).toContain("M kept.txt");
910
+ });
911
+ it("excludes unignored dependency trees from git-backed workspace overlay archives", async () => {
912
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-unignored-deps-"));
913
+ cleanupDirs.push(rootDir);
914
+ const sourceRepoDir = path.join(rootDir, "source-repo");
915
+ const localWorkspaceDir = path.join(rootDir, "local-worktree");
916
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
917
+ await mkdir(sourceRepoDir, { recursive: true });
918
+ await git(sourceRepoDir, ["init"]);
919
+ await git(sourceRepoDir, ["checkout", "-b", "main"]);
920
+ await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
921
+ await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
922
+ await mkdir(path.join(sourceRepoDir, "src"), { recursive: true });
923
+ await writeFile(path.join(sourceRepoDir, "src", "tracked.ts"), "export const tracked = true;\n", "utf8");
924
+ await git(sourceRepoDir, ["add", "src/tracked.ts"]);
925
+ await git(sourceRepoDir, ["commit", "-m", "base"]);
926
+ await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
927
+ await mkdir(path.join(localWorkspaceDir, "node_modules", "root-package"), { recursive: true });
928
+ await mkdir(path.join(localWorkspaceDir, "packages", "ui", "node_modules", "nested-package"), { recursive: true });
929
+ await writeFile(path.join(localWorkspaceDir, "node_modules", "root-package", "cache.bin"), "root dependency\n", "utf8");
930
+ await writeFile(path.join(localWorkspaceDir, "packages", "ui", "node_modules", "nested-package", "cache.bin"), "nested dependency\n", "utf8");
931
+ await writeFile(path.join(localWorkspaceDir, "src", "local-only.ts"), "export const local = true;\n", "utf8");
932
+ const uploadedTars = [];
933
+ const downloadedTars = [];
934
+ const client = {
935
+ makeDir: async (remotePath) => {
936
+ await mkdir(remotePath, { recursive: true });
937
+ },
938
+ writeFile: async (remotePath, bytes) => {
939
+ await mkdir(path.dirname(remotePath), { recursive: true });
940
+ const buffer = Buffer.from(bytes);
941
+ if (remotePath.endsWith("-upload.tar"))
942
+ uploadedTars.push({ remotePath, bytes: buffer });
943
+ await writeFile(remotePath, buffer);
944
+ },
945
+ readFile: async (remotePath) => {
946
+ const buffer = await readFile(remotePath);
947
+ if (remotePath.endsWith("workspace-download.tar"))
948
+ downloadedTars.push({ remotePath, bytes: buffer });
949
+ return buffer;
950
+ },
951
+ listFiles: async () => [],
952
+ remove: async (remotePath) => {
953
+ await rm(remotePath, { recursive: true, force: true });
954
+ },
955
+ run: async (command) => {
956
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
957
+ },
958
+ };
959
+ attachFallbackSyncIn(client);
960
+ const prepared = await prepareSandboxManagedRuntime({
961
+ spec: {
962
+ transport: "sandbox",
963
+ provider: "test",
964
+ sandboxId: "sandbox-1",
965
+ remoteCwd: remoteWorkspaceDir,
966
+ timeoutMs: 30_000,
967
+ apiKey: null,
968
+ },
969
+ adapterKey: "test-adapter",
970
+ client,
971
+ workspaceLocalDir: localWorkspaceDir,
972
+ });
973
+ const workspaceUpload = uploadedTars.find((entry) => path.posix.basename(entry.remotePath) === "workspace-upload.tar");
974
+ expect(workspaceUpload).toBeDefined();
975
+ const workspaceMembers = await listTarMembers(rootDir, "unignored-deps-workspace-upload.tar", workspaceUpload.bytes);
976
+ expect(workspaceMembers).toContain("src/local-only.ts");
977
+ expect(workspaceMembers.some((entry) => entry === "node_modules" || entry.startsWith("node_modules/"))).toBe(false);
978
+ expect(workspaceMembers.some((entry) => entry.includes("/node_modules/") || entry.endsWith("/node_modules"))).toBe(false);
979
+ await expect(readFile(path.join(remoteWorkspaceDir, "src", "local-only.ts"), "utf8")).resolves.toBe("export const local = true;\n");
980
+ await expect(readFile(path.join(remoteWorkspaceDir, "node_modules", "root-package", "cache.bin"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
981
+ await expect(readFile(path.join(remoteWorkspaceDir, "packages", "ui", "node_modules", "nested-package", "cache.bin"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
982
+ await mkdir(path.join(remoteWorkspaceDir, "node_modules", "sandbox-package"), { recursive: true });
983
+ await mkdir(path.join(remoteWorkspaceDir, "packages", "ui", "node_modules", "sandbox-package"), { recursive: true });
984
+ await writeFile(path.join(remoteWorkspaceDir, "node_modules", "sandbox-package", "cache.bin"), "sandbox root dependency\n", "utf8");
985
+ await writeFile(path.join(remoteWorkspaceDir, "packages", "ui", "node_modules", "sandbox-package", "cache.bin"), "sandbox nested dependency\n", "utf8");
986
+ await writeFile(path.join(remoteWorkspaceDir, "src", "remote-only.ts"), "export const remote = true;\n", "utf8");
987
+ await prepared.restoreWorkspace();
988
+ await expect(readFile(path.join(localWorkspaceDir, "node_modules", "root-package", "cache.bin"), "utf8")).resolves.toBe("root dependency\n");
989
+ await expect(readFile(path.join(localWorkspaceDir, "packages", "ui", "node_modules", "nested-package", "cache.bin"), "utf8")).resolves.toBe("nested dependency\n");
990
+ await expect(readFile(path.join(localWorkspaceDir, "src", "remote-only.ts"), "utf8")).resolves.toBe("export const remote = true;\n");
991
+ await expect(readFile(path.join(localWorkspaceDir, "node_modules", "sandbox-package", "cache.bin"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
992
+ expect(downloadedTars).toHaveLength(1);
993
+ const downloadMembers = await listTarMembers(rootDir, "unignored-deps-workspace-download.tar", downloadedTars[0].bytes);
994
+ expect(downloadMembers.some((entry) => entry === ".git" || entry.startsWith(".git/"))).toBe(false);
995
+ expect(downloadMembers.some((entry) => entry === "node_modules" || entry.startsWith("node_modules/"))).toBe(false);
996
+ expect(downloadMembers.some((entry) => entry.includes("/node_modules/") || entry.endsWith("/node_modules"))).toBe(false);
997
+ });
998
+ it("excludes an anchor-workspace ignored file whose name has leading and trailing whitespace from the staged tree", async () => {
999
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-ignored-whitespace-"));
1000
+ cleanupDirs.push(rootDir);
1001
+ const workspaceLocalDir = path.join(rootDir, "workspace");
1002
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1003
+ await initGitRepo(workspaceLocalDir);
1004
+ // A double-wildcard pattern avoids the separate rule that Git trims an
1005
+ // unescaped trailing space in a .gitignore PATTERN itself; the padding
1006
+ // under test lives in the matched FILE name, proving the anchor `splitNul`
1007
+ // parser keeps it instead of trimming it away and missing the exclude.
1008
+ const ignoredName = " ignored padded ";
1009
+ await writeFile(path.join(workspaceLocalDir, ".gitignore"), "*ignored*padded*\n", "utf8");
1010
+ await writeFile(path.join(workspaceLocalDir, ignoredName), "TOKEN=abc\n", "utf8");
1011
+ await writeFile(path.join(workspaceLocalDir, "kept.txt"), "kept\n", "utf8");
1012
+ const uploadedTars = [];
1013
+ const client = {
1014
+ makeDir: async (remotePath) => {
1015
+ await mkdir(remotePath, { recursive: true });
1016
+ },
1017
+ writeFile: async (remotePath, bytes) => {
1018
+ await mkdir(path.dirname(remotePath), { recursive: true });
1019
+ const buffer = Buffer.from(bytes);
1020
+ if (remotePath.endsWith("-upload.tar"))
1021
+ uploadedTars.push({ remotePath, bytes: buffer });
1022
+ await writeFile(remotePath, buffer);
1023
+ },
1024
+ readFile: async (remotePath) => await readFile(remotePath),
1025
+ listFiles: async () => [],
1026
+ remove: async (remotePath) => {
1027
+ await rm(remotePath, { recursive: true, force: true });
1028
+ },
1029
+ run: async (command) => {
1030
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1031
+ },
1032
+ };
1033
+ attachFallbackSyncIn(client);
1034
+ await prepareSandboxManagedRuntime({
1035
+ spec: {
1036
+ transport: "sandbox",
1037
+ provider: "test",
1038
+ sandboxId: "sandbox-1",
1039
+ remoteCwd: remoteWorkspaceDir,
1040
+ timeoutMs: 30_000,
1041
+ apiKey: null,
1042
+ },
1043
+ adapterKey: "test-adapter",
1044
+ client,
1045
+ workspaceLocalDir,
1046
+ });
1047
+ const workspaceUpload = uploadedTars.find((entry) => path.posix.basename(entry.remotePath) === "workspace-upload.tar");
1048
+ expect(workspaceUpload).toBeDefined();
1049
+ const members = await listTarMembers(rootDir, "ignored-whitespace-workspace-upload.tar", workspaceUpload.bytes);
1050
+ expect(members).not.toContain(ignoredName);
1051
+ expect(members).toContain("kept.txt");
1052
+ });
1053
+ it("builds workspace/asset tarballs without a './' self-entry (so untar does not chmod/utime an unowned target dir)", async () => {
1054
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-tarself-"));
1055
+ cleanupDirs.push(rootDir);
1056
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
1057
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1058
+ const localAssetsDir = path.join(rootDir, "local-assets");
1059
+ await mkdir(path.join(localWorkspaceDir, "src"), { recursive: true });
1060
+ await mkdir(localAssetsDir, { recursive: true });
1061
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "ws\n", "utf8");
1062
+ await writeFile(path.join(localWorkspaceDir, "src", "main.ts"), "x\n", "utf8");
1063
+ await writeFile(path.join(localAssetsDir, "asset.txt"), "a\n", "utf8");
1064
+ // Capture every tar uploaded/downloaded through the sandbox so we can inspect its members.
1065
+ const uploadedTars = [];
1066
+ const downloadedTars = [];
1067
+ const client = {
1068
+ makeDir: async (remotePath) => {
1069
+ await mkdir(remotePath, { recursive: true });
1070
+ },
1071
+ writeFile: async (remotePath, bytes) => {
1072
+ await mkdir(path.dirname(remotePath), { recursive: true });
1073
+ const buffer = Buffer.from(bytes);
1074
+ if (remotePath.endsWith("-upload.tar"))
1075
+ uploadedTars.push({ remotePath, bytes: buffer });
1076
+ await writeFile(remotePath, buffer);
1077
+ },
1078
+ readFile: async (remotePath) => {
1079
+ const buffer = await readFile(remotePath);
1080
+ if (remotePath.endsWith("workspace-download.tar"))
1081
+ downloadedTars.push({ remotePath, bytes: buffer });
1082
+ return buffer;
1083
+ },
1084
+ listFiles: async () => [],
1085
+ remove: async (remotePath) => {
1086
+ await rm(remotePath, { recursive: true, force: true });
1087
+ },
1088
+ run: async (command) => {
1089
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1090
+ },
1091
+ };
1092
+ attachFallbackSyncIn(client);
1093
+ const prepared = await prepareSandboxManagedRuntime({
1094
+ spec: {
1095
+ transport: "sandbox",
1096
+ provider: "test",
1097
+ sandboxId: "sandbox-1",
1098
+ remoteCwd: remoteWorkspaceDir,
1099
+ timeoutMs: 30_000,
1100
+ apiKey: null,
1101
+ },
1102
+ adapterKey: "test-adapter",
1103
+ client,
1104
+ workspaceLocalDir: localWorkspaceDir,
1105
+ assets: [{ key: "skills", localDir: localAssetsDir }],
1106
+ });
1107
+ expect(uploadedTars.length).toBeGreaterThanOrEqual(2);
1108
+ for (const { remotePath, bytes } of uploadedTars) {
1109
+ const listPath = path.join(rootDir, `list-${path.basename(remotePath)}`);
1110
+ await writeFile(listPath, bytes);
1111
+ const { stdout } = await execFile("tar", ["-tf", listPath], { maxBuffer: 32 * 1024 * 1024 });
1112
+ const members = stdout.split("\n").map((line) => line.trim()).filter(Boolean);
1113
+ // The archive must NOT contain a self-entry for the root directory; that is
1114
+ // what makes tar try to mutate the (possibly unowned) extraction target.
1115
+ expect(members).not.toContain(".");
1116
+ expect(members).not.toContain("./");
1117
+ }
1118
+ // And the workspace still extracts correctly into an existing target dir.
1119
+ await expect(readFile(path.join(remoteWorkspaceDir, "README.md"), "utf8")).resolves.toBe("ws\n");
1120
+ await expect(readFile(path.join(remoteWorkspaceDir, "src", "main.ts"), "utf8")).resolves.toBe("x\n");
1121
+ await prepared.restoreWorkspace();
1122
+ expect(downloadedTars).toHaveLength(1);
1123
+ const downloadMembers = await listTarMembers(rootDir, "workspace-download-list.tar", downloadedTars[0].bytes);
1124
+ expect(downloadMembers).not.toContain(".");
1125
+ expect(downloadMembers).not.toContain("./");
1126
+ });
1127
+ it("excludes transient symlinked home dirs from the asset tar while keeping required content", async () => {
1128
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-home-tmp-"));
1129
+ cleanupDirs.push(rootDir);
1130
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
1131
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1132
+ const homeDir = path.join(rootDir, "codex-home");
1133
+ await mkdir(localWorkspaceDir, { recursive: true });
1134
+ // Simulate a host Codex binary that a stale `tmp/arg0` symlink points at.
1135
+ // With followSymlinks the archive would otherwise inline this whole file.
1136
+ const hostBinary = path.join(rootDir, "codex-host-binary");
1137
+ const binaryMarker = "HOST_CODEX_BINARY_BYTES";
1138
+ await writeFile(hostBinary, `${binaryMarker}\n`.repeat(4096), "utf8");
1139
+ // Required managed-home content that MUST still reach the sandbox.
1140
+ await mkdir(path.join(homeDir, "skills"), { recursive: true });
1141
+ await writeFile(path.join(homeDir, "auth.json"), "{\"OPENAI_API_KEY\":\"sk-test\"}\n", "utf8");
1142
+ await writeFile(path.join(homeDir, "config.toml"), "model = \"gpt\"\n", "utf8");
1143
+ await writeFile(path.join(homeDir, "skills", "demo.md"), "skill body\n", "utf8");
1144
+ // Transient dirs holding symlinks to the host binary (the bloat source).
1145
+ await mkdir(path.join(homeDir, "tmp", "arg0"), { recursive: true });
1146
+ await mkdir(path.join(homeDir, ".tmp"), { recursive: true });
1147
+ await symlink(hostBinary, path.join(homeDir, "tmp", "arg0", "codex"));
1148
+ await symlink(hostBinary, path.join(homeDir, ".tmp", "codex"));
1149
+ const uploadedTars = [];
1150
+ const client = {
1151
+ makeDir: async (remotePath) => {
1152
+ await mkdir(remotePath, { recursive: true });
1153
+ },
1154
+ writeFile: async (remotePath, bytes) => {
1155
+ await mkdir(path.dirname(remotePath), { recursive: true });
1156
+ const buffer = Buffer.from(bytes);
1157
+ if (remotePath.endsWith("-upload.tar"))
1158
+ uploadedTars.push({ remotePath, bytes: buffer });
1159
+ await writeFile(remotePath, buffer);
1160
+ },
1161
+ readFile: async (remotePath) => await readFile(remotePath),
1162
+ listFiles: async () => [],
1163
+ remove: async (remotePath) => {
1164
+ await rm(remotePath, { recursive: true, force: true });
1165
+ },
1166
+ run: async (command) => {
1167
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1168
+ },
1169
+ };
1170
+ attachFallbackSyncIn(client);
1171
+ const prepared = await prepareSandboxManagedRuntime({
1172
+ spec: {
1173
+ transport: "sandbox",
1174
+ provider: "test",
1175
+ sandboxId: "sandbox-1",
1176
+ remoteCwd: remoteWorkspaceDir,
1177
+ timeoutMs: 30_000,
1178
+ apiKey: null,
1179
+ },
1180
+ adapterKey: "codex",
1181
+ client,
1182
+ workspaceLocalDir: localWorkspaceDir,
1183
+ assets: [{
1184
+ key: "home",
1185
+ localDir: homeDir,
1186
+ followSymlinks: true,
1187
+ exclude: ["tmp", ".tmp"],
1188
+ }],
1189
+ });
1190
+ const homeTar = uploadedTars.find(({ remotePath }) => path.basename(remotePath) === "home-upload.tar");
1191
+ expect(homeTar).toBeDefined();
1192
+ const members = await listTarMembers(rootDir, "home-members.tar", homeTar.bytes);
1193
+ // Transient symlink trees must be filtered out entirely.
1194
+ expect(members.some((entry) => entry === "tmp" || entry.startsWith("tmp/"))).toBe(false);
1195
+ expect(members.some((entry) => entry === ".tmp" || entry.startsWith(".tmp/"))).toBe(false);
1196
+ // Required managed-home content must survive.
1197
+ expect(members).toContain("auth.json");
1198
+ expect(members).toContain("config.toml");
1199
+ expect(members.some((entry) => entry === "skills/demo.md")).toBe(true);
1200
+ // The host binary bytes must not have been inlined into the upload.
1201
+ expect(homeTar.bytes.includes(Buffer.from(binaryMarker))).toBe(false);
1202
+ // The extracted sandbox home keeps required content and omits the transient dirs.
1203
+ await expect(readFile(path.join(prepared.assetDirs.home, "auth.json"), "utf8"))
1204
+ .resolves.toBe("{\"OPENAI_API_KEY\":\"sk-test\"}\n");
1205
+ await expect(readFile(path.join(prepared.assetDirs.home, "skills", "demo.md"), "utf8"))
1206
+ .resolves.toBe("skill body\n");
1207
+ await expect(lstat(path.join(prepared.assetDirs.home, "tmp"))).rejects.toMatchObject({ code: "ENOENT" });
1208
+ await expect(lstat(path.join(prepared.assetDirs.home, ".tmp"))).rejects.toMatchObject({ code: "ENOENT" });
1209
+ });
1210
+ it("emits throttled, labeled upload and restore progress with direction and percentages", async () => {
1211
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-progress-"));
1212
+ cleanupDirs.push(rootDir);
1213
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
1214
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1215
+ const localAssetsDir = path.join(rootDir, "local-assets");
1216
+ await mkdir(localWorkspaceDir, { recursive: true });
1217
+ await mkdir(localAssetsDir, { recursive: true });
1218
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
1219
+ await writeFile(path.join(localAssetsDir, "skill.md"), "skill\n", "utf8");
1220
+ // Drive byte progress in 100 fine (1%) increments so the throttle has many
1221
+ // chances to emit; the reporter must collapse them to ~one line per 10% step.
1222
+ const driveProgress = async (total, onProgress) => {
1223
+ if (!onProgress)
1224
+ return;
1225
+ for (let i = 1; i <= 100; i++) {
1226
+ await onProgress(Math.floor((total * i) / 100), total);
1227
+ }
1228
+ };
1229
+ const client = {
1230
+ makeDir: async (remotePath) => {
1231
+ await mkdir(remotePath, { recursive: true });
1232
+ },
1233
+ writeFile: async (remotePath, bytes, options) => {
1234
+ await mkdir(path.dirname(remotePath), { recursive: true });
1235
+ const buffer = Buffer.from(bytes);
1236
+ await writeFile(remotePath, buffer);
1237
+ await driveProgress(buffer.byteLength, options?.onProgress);
1238
+ },
1239
+ readFile: async (remotePath, options) => {
1240
+ const buffer = await readFile(remotePath);
1241
+ await driveProgress(buffer.byteLength, options?.onProgress);
1242
+ return buffer;
1243
+ },
1244
+ listFiles: async () => [],
1245
+ remove: async (remotePath) => {
1246
+ await rm(remotePath, { recursive: true, force: true });
1247
+ },
1248
+ run: async (command) => {
1249
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1250
+ },
1251
+ };
1252
+ const lines = [];
1253
+ attachFallbackSyncIn(client);
1254
+ const prepared = await prepareSandboxManagedRuntime({
1255
+ spec: {
1256
+ transport: "sandbox",
1257
+ provider: "test",
1258
+ sandboxId: "sandbox-1",
1259
+ remoteCwd: remoteWorkspaceDir,
1260
+ timeoutMs: 30_000,
1261
+ apiKey: null,
1262
+ },
1263
+ adapterKey: "test-adapter",
1264
+ client,
1265
+ workspaceLocalDir: localWorkspaceDir,
1266
+ assets: [{ key: "skills", localDir: localAssetsDir }],
1267
+ onProgress: (line) => {
1268
+ lines.push(line);
1269
+ },
1270
+ });
1271
+ const uploadWorkspaceLines = lines.filter((line) => line.includes("Syncing workspace to environment"));
1272
+ const uploadAssetLines = lines.filter((line) => line.includes("Syncing skills to environment"));
1273
+ expect(uploadWorkspaceLines.length).toBeGreaterThan(0);
1274
+ expect(uploadAssetLines.length).toBeGreaterThan(0);
1275
+ // 100 reported increments must be throttled to at most ~one line per 10% step.
1276
+ expect(uploadWorkspaceLines.length).toBeLessThanOrEqual(11);
1277
+ // Reaches 100% and shows the MB breakdown.
1278
+ expect(uploadWorkspaceLines.some((line) => line.includes("100%"))).toBe(true);
1279
+ expect(uploadWorkspaceLines.every((line) => /\(\d+\.\d\/\d+\.\d MB\)/.test(line))).toBe(true);
1280
+ await prepared.restoreWorkspace();
1281
+ const restoreLines = lines.filter((line) => line.includes("Restoring workspace from environment"));
1282
+ expect(restoreLines.length).toBeGreaterThan(0);
1283
+ expect(restoreLines.length).toBeLessThanOrEqual(11);
1284
+ expect(restoreLines.some((line) => line.includes("100%"))).toBe(true);
1285
+ });
1286
+ it("creates valid empty workspace tarballs when the workspace is empty", async () => {
1287
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-empty-"));
1288
+ cleanupDirs.push(rootDir);
1289
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
1290
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1291
+ await mkdir(localWorkspaceDir, { recursive: true });
1292
+ const downloadedTars = [];
1293
+ const runCommands = [];
1294
+ const client = {
1295
+ makeDir: async (remotePath) => {
1296
+ await mkdir(remotePath, { recursive: true });
1297
+ },
1298
+ writeFile: async (remotePath, bytes) => {
1299
+ await mkdir(path.dirname(remotePath), { recursive: true });
1300
+ await writeFile(remotePath, Buffer.from(bytes));
1301
+ },
1302
+ readFile: async (remotePath) => {
1303
+ const buffer = await readFile(remotePath);
1304
+ if (remotePath.endsWith("workspace-download.tar"))
1305
+ downloadedTars.push({ remotePath, bytes: buffer });
1306
+ return buffer;
1307
+ },
1308
+ listFiles: async () => [],
1309
+ remove: async (remotePath) => {
1310
+ await rm(remotePath, { recursive: true, force: true });
1311
+ },
1312
+ run: async (command) => {
1313
+ runCommands.push(command);
1314
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1315
+ },
1316
+ };
1317
+ attachFallbackSyncIn(client);
1318
+ const prepared = await prepareSandboxManagedRuntime({
1319
+ spec: {
1320
+ transport: "sandbox",
1321
+ provider: "test",
1322
+ sandboxId: "sandbox-1",
1323
+ remoteCwd: remoteWorkspaceDir,
1324
+ timeoutMs: 30_000,
1325
+ apiKey: null,
1326
+ },
1327
+ adapterKey: "test-adapter",
1328
+ client,
1329
+ workspaceLocalDir: localWorkspaceDir,
1330
+ });
1331
+ await prepared.restoreWorkspace();
1332
+ expect(downloadedTars).toHaveLength(1);
1333
+ const members = await listTarMembers(rootDir, "empty-workspace-download.tar", downloadedTars[0].bytes);
1334
+ expect(members).toEqual([]);
1335
+ const emptyArchiveCommand = runCommands.find((command) => command.includes("dd if=/dev/zero"));
1336
+ expect(emptyArchiveCommand).toBeDefined();
1337
+ expect(emptyArchiveCommand).not.toContain("/dev/null");
1338
+ });
1339
+ it("provisions a contribution-less asset via a plain tar extract and restores it as a no-op", async () => {
1340
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-default-asset-"));
1341
+ cleanupDirs.push(rootDir);
1342
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
1343
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1344
+ const localAssetsDir = path.join(rootDir, "local-assets");
1345
+ await mkdir(localWorkspaceDir, { recursive: true });
1346
+ await mkdir(localAssetsDir, { recursive: true });
1347
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
1348
+ await writeFile(path.join(localAssetsDir, "plain.txt"), "plain asset\n", "utf8");
1349
+ const stagedWrites = [];
1350
+ const runCommands = [];
1351
+ const client = {
1352
+ makeDir: async (remotePath) => {
1353
+ await mkdir(remotePath, { recursive: true });
1354
+ },
1355
+ writeFile: async (remotePath, bytes) => {
1356
+ await mkdir(path.dirname(remotePath), { recursive: true });
1357
+ if (!remotePath.endsWith("-upload.tar"))
1358
+ stagedWrites.push(path.basename(remotePath));
1359
+ await writeFile(remotePath, Buffer.from(bytes));
1360
+ },
1361
+ readFile: async (remotePath) => await readFile(remotePath),
1362
+ listFiles: async () => [],
1363
+ remove: async (remotePath) => {
1364
+ await rm(remotePath, { recursive: true, force: true });
1365
+ },
1366
+ run: async (command) => {
1367
+ runCommands.push(command);
1368
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1369
+ },
1370
+ };
1371
+ attachFallbackSyncIn(client);
1372
+ const prepared = await prepareSandboxManagedRuntime({
1373
+ spec: {
1374
+ transport: "sandbox",
1375
+ provider: "test",
1376
+ sandboxId: "sandbox-1",
1377
+ remoteCwd: remoteWorkspaceDir,
1378
+ timeoutMs: 30_000,
1379
+ apiKey: null,
1380
+ },
1381
+ adapterKey: "test-adapter",
1382
+ client,
1383
+ workspaceLocalDir: localWorkspaceDir,
1384
+ // No `provision` / `restore` on the asset: it must ride the default path.
1385
+ assets: [{ key: "plain", localDir: localAssetsDir }],
1386
+ });
1387
+ // Extracted through the default `tar -xf` path.
1388
+ await expect(readFile(path.join(prepared.assetDirs.plain, "plain.txt"), "utf8")).resolves.toBe("plain asset\n");
1389
+ // A contribution-less asset stages no extra files beyond its own tar.
1390
+ expect(stagedWrites.filter((name) => name.includes("plain"))).toEqual([]);
1391
+ // The extract command is the generic tar path, not an adapter-specific script.
1392
+ const assetExtract = runCommands.find((command) => command.includes(`${path.posix.basename(prepared.assetDirs.plain)}-upload.tar`));
1393
+ expect(assetExtract).toBeDefined();
1394
+ expect(assetExtract).toContain("tar -xf");
1395
+ expect(assetExtract).not.toMatch(/\.sh|\.cjs/);
1396
+ // Restore is a clean no-op for a contribution-less asset (no throw, asset dir untouched).
1397
+ await expect(prepared.restoreWorkspace()).resolves.toBeUndefined();
1398
+ await expect(readFile(path.join(prepared.assetDirs.plain, "plain.txt"), "utf8")).resolves.toBe("plain asset\n");
1399
+ });
1400
+ it("round-trips a non-codex asset through generic provision + restore contributions", async () => {
1401
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-seam-"));
1402
+ cleanupDirs.push(rootDir);
1403
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
1404
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1405
+ const localAssetsDir = path.join(rootDir, "local-assets");
1406
+ await mkdir(localWorkspaceDir, { recursive: true });
1407
+ await mkdir(localAssetsDir, { recursive: true });
1408
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
1409
+ await writeFile(path.join(localAssetsDir, "seed.txt"), "seed\n", "utf8");
1410
+ const client = {
1411
+ makeDir: async (remotePath) => {
1412
+ await mkdir(remotePath, { recursive: true });
1413
+ },
1414
+ writeFile: async (remotePath, bytes) => {
1415
+ await mkdir(path.dirname(remotePath), { recursive: true });
1416
+ await writeFile(remotePath, Buffer.from(bytes));
1417
+ },
1418
+ readFile: async (remotePath) => await readFile(remotePath),
1419
+ listFiles: async () => [],
1420
+ remove: async (remotePath) => {
1421
+ await rm(remotePath, { recursive: true, force: true });
1422
+ },
1423
+ run: async (command) => {
1424
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1425
+ },
1426
+ };
1427
+ // A minimal shell quoter local to the test's custom extract command; the seam
1428
+ // itself carries no adapter knowledge — the fake asset supplies everything.
1429
+ const q = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
1430
+ const restored = [];
1431
+ const stagedContentSeen = [];
1432
+ attachFallbackSyncIn(client);
1433
+ const prepared = await prepareSandboxManagedRuntime({
1434
+ spec: {
1435
+ transport: "sandbox",
1436
+ provider: "test",
1437
+ sandboxId: "sandbox-1",
1438
+ remoteCwd: remoteWorkspaceDir,
1439
+ timeoutMs: 30_000,
1440
+ apiKey: null,
1441
+ },
1442
+ adapterKey: "generic-adapter",
1443
+ client,
1444
+ workspaceLocalDir: localWorkspaceDir,
1445
+ assets: [{
1446
+ key: "widget",
1447
+ localDir: localAssetsDir,
1448
+ provision: {
1449
+ stageFiles: [{ name: "widget-helper.txt", contents: "helper-bytes\n" }],
1450
+ // Extract the asset AND consume the staged helper file, proving both
1451
+ // stageFiles and postUploadCommand flow through the core generically.
1452
+ postUploadCommand: ({ assetTarPath, assetDir, runtimeRootDir }) => `rm -rf ${q(assetDir)} && mkdir -p ${q(assetDir)} && ` +
1453
+ `tar -xf ${q(assetTarPath)} -C ${q(assetDir)} && rm -f ${q(assetTarPath)} && ` +
1454
+ `cp ${q(path.posix.join(runtimeRootDir, "widget-helper.txt"))} ${q(path.posix.join(assetDir, "helper.copied.txt"))}`,
1455
+ },
1456
+ restore: async ({ assetDir, readFile: readRemote }) => {
1457
+ const bytes = await readRemote(path.posix.join(assetDir, "refreshed.txt"));
1458
+ restored.push(bytes.toString("utf8"));
1459
+ },
1460
+ }],
1461
+ });
1462
+ // provision: the asset's own content extracted...
1463
+ await expect(readFile(path.join(prepared.assetDirs.widget, "seed.txt"), "utf8")).resolves.toBe("seed\n");
1464
+ // ...the staged helper file was written to the runtime root and consumed by the custom extract command.
1465
+ await expect(readFile(path.join(prepared.assetDirs.widget, "helper.copied.txt"), "utf8")).resolves.toBe("helper-bytes\n");
1466
+ stagedContentSeen.push("provisioned");
1467
+ // Simulate the sandbox refreshing a file inside the asset dir, then restore.
1468
+ await writeFile(path.join(prepared.assetDirs.widget, "refreshed.txt"), "refreshed-by-sandbox\n", "utf8");
1469
+ await prepared.restoreWorkspace();
1470
+ // restore contribution was invoked with a working remote readFile against assetDir.
1471
+ expect(restored).toEqual(["refreshed-by-sandbox\n"]);
1472
+ expect(stagedContentSeen).toEqual(["provisioned"]);
1473
+ });
1474
+ it("rejects a provision stageFile.name that is not a simple basename", async () => {
1475
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-traversal-"));
1476
+ cleanupDirs.push(rootDir);
1477
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
1478
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1479
+ const localAssetsDir = path.join(rootDir, "local-assets");
1480
+ await mkdir(localWorkspaceDir, { recursive: true });
1481
+ await mkdir(localAssetsDir, { recursive: true });
1482
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
1483
+ await writeFile(path.join(localAssetsDir, "seed.txt"), "seed\n", "utf8");
1484
+ const writtenPaths = [];
1485
+ const client = {
1486
+ makeDir: async (remotePath) => {
1487
+ await mkdir(remotePath, { recursive: true });
1488
+ },
1489
+ writeFile: async (remotePath, bytes) => {
1490
+ writtenPaths.push(remotePath);
1491
+ await mkdir(path.dirname(remotePath), { recursive: true });
1492
+ await writeFile(remotePath, Buffer.from(bytes));
1493
+ },
1494
+ readFile: async (remotePath) => await readFile(remotePath),
1495
+ listFiles: async () => [],
1496
+ remove: async (remotePath) => {
1497
+ await rm(remotePath, { recursive: true, force: true });
1498
+ },
1499
+ run: async (command) => {
1500
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1501
+ },
1502
+ };
1503
+ attachFallbackSyncIn(client);
1504
+ // A compromised adapter supplying a traversal name must be rejected before
1505
+ // the core ever writes outside the runtime root.
1506
+ for (const maliciousName of ["../evil.txt", "..", "nested/child.txt", "back\\slash.txt", "../../etc/passwd"]) {
1507
+ writtenPaths.length = 0;
1508
+ await expect(prepareSandboxManagedRuntime({
1509
+ spec: {
1510
+ transport: "sandbox",
1511
+ provider: "test",
1512
+ sandboxId: "sandbox-1",
1513
+ remoteCwd: remoteWorkspaceDir,
1514
+ timeoutMs: 30_000,
1515
+ apiKey: null,
1516
+ },
1517
+ adapterKey: "generic-adapter",
1518
+ client,
1519
+ workspaceLocalDir: localWorkspaceDir,
1520
+ assets: [{
1521
+ key: "widget",
1522
+ localDir: localAssetsDir,
1523
+ provision: {
1524
+ stageFiles: [{ name: maliciousName, contents: "payload\n" }],
1525
+ },
1526
+ }],
1527
+ })).rejects.toThrow(/must be a simple basename/);
1528
+ // The guard fires before the offending write, so nothing landed under the runtime root.
1529
+ expect(writtenPaths.some((p) => p.endsWith("evil.txt") || p.endsWith("passwd") || p.endsWith("child.txt"))).toBe(false);
1530
+ }
1531
+ });
1532
+ it("routes a custom-provisioned asset through a single syncIn operation with its post-upload command (native runner → 0 direct writeFile/run)", async () => {
1533
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-native-asset-"));
1534
+ cleanupDirs.push(rootDir);
1535
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
1536
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1537
+ const localAssetsDir = path.join(rootDir, "local-assets");
1538
+ await mkdir(localWorkspaceDir, { recursive: true });
1539
+ await mkdir(localAssetsDir, { recursive: true });
1540
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
1541
+ await writeFile(path.join(localAssetsDir, "seed.txt"), "seed\n", "utf8");
1542
+ // A native runner delegates every staging step to `syncIn`; the orchestrator
1543
+ // must make NO direct `writeFile`/`run` exec. These record any leak.
1544
+ const directWrites = [];
1545
+ const directRuns = [];
1546
+ const client = {
1547
+ makeDir: async (remotePath) => {
1548
+ await mkdir(remotePath, { recursive: true });
1549
+ },
1550
+ writeFile: async (remotePath, bytes) => {
1551
+ directWrites.push(remotePath);
1552
+ await mkdir(path.dirname(remotePath), { recursive: true });
1553
+ await writeFile(remotePath, Buffer.from(bytes));
1554
+ },
1555
+ readFile: async (remotePath) => await readFile(remotePath),
1556
+ listFiles: async () => [],
1557
+ remove: async (remotePath) => {
1558
+ await rm(remotePath, { recursive: true, force: true });
1559
+ },
1560
+ run: async (command) => {
1561
+ directRuns.push(command);
1562
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1563
+ },
1564
+ };
1565
+ const captured = [];
1566
+ attachNativeRecordingSyncIn(client, captured);
1567
+ const q = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
1568
+ const prepared = await prepareSandboxManagedRuntime({
1569
+ spec: {
1570
+ transport: "sandbox",
1571
+ provider: "test",
1572
+ sandboxId: "sandbox-1",
1573
+ remoteCwd: remoteWorkspaceDir,
1574
+ timeoutMs: 30_000,
1575
+ apiKey: null,
1576
+ },
1577
+ adapterKey: "generic-adapter",
1578
+ client,
1579
+ workspaceLocalDir: localWorkspaceDir,
1580
+ assets: [{
1581
+ key: "widget",
1582
+ localDir: localAssetsDir,
1583
+ provision: {
1584
+ stageFiles: [{ name: "widget-helper.sh", contents: "#!/bin/sh\ntar -xf \"$2\" -C \"$1\"\n" }],
1585
+ // A bespoke post-upload command that consumes the staged helper — proves
1586
+ // the custom command (not a plain default `tar -xf`) rides syncIn.
1587
+ postUploadCommand: ({ assetTarPath, assetDir, runtimeRootDir }) => `rm -rf ${q(assetDir)} && mkdir -p ${q(assetDir)} && ` +
1588
+ `sh ${q(path.posix.join(runtimeRootDir, "widget-helper.sh"))} ${q(assetDir)} ${q(assetTarPath)} && ` +
1589
+ `rm -f ${q(assetTarPath)}`,
1590
+ },
1591
+ }],
1592
+ });
1593
+ // The orchestrator delegated everything to syncIn: no direct exec/writeFile.
1594
+ expect(directWrites).toEqual([]);
1595
+ expect(directRuns).toEqual([]);
1596
+ // Exactly one operation carries the asset: the asset tar + the staged helper
1597
+ // as `files`, and the bespoke command as the ordered post-upload command.
1598
+ const assetOp = captured.find((op) => op.files.some((mapping) => mapping.targetPath.endsWith("widget-upload.tar")));
1599
+ expect(assetOp).toBeDefined();
1600
+ const targets = assetOp.files.map((mapping) => path.posix.basename(mapping.targetPath)).sort();
1601
+ expect(targets).toEqual(["widget-helper.sh", "widget-upload.tar"]);
1602
+ expect(assetOp.files.every((mapping) => mapping.kind === "file")).toBe(true);
1603
+ expect(assetOp.postUploadCommands).toHaveLength(1);
1604
+ expect(assetOp.postUploadCommands[0].command).toContain("widget-helper.sh");
1605
+ expect(assetOp.postUploadCommands[0].command).not.toBe(`rm -rf ${q(path.posix.join(prepared.runtimeRootDir, "widget"))} && mkdir -p ${q(path.posix.join(prepared.runtimeRootDir, "widget"))}`);
1606
+ // The asset tar carries the asset directory as its read-write destination,
1607
+ // because the extract command fills that directory, not the staging archive.
1608
+ const assetTarMapping = assetOp.files.find((mapping) => mapping.targetPath.endsWith("widget-upload.tar"));
1609
+ expect(assetTarMapping?.access).toBe("rw");
1610
+ expect(assetTarMapping?.writablePath).toBe(prepared.assetDirs.widget);
1611
+ // The staged helper file is a read-only input that the command consumes, so
1612
+ // it is `access: "ro"` and never joins the writable set.
1613
+ const stageMapping = assetOp.files.find((mapping) => mapping.targetPath.endsWith("widget-helper.sh"));
1614
+ expect(stageMapping?.access).toBe("ro");
1615
+ expect(stageMapping?.writablePath).toBeUndefined();
1616
+ // The asset actually materialized through the native seam.
1617
+ await expect(readFile(path.join(prepared.assetDirs.widget, "seed.txt"), "utf8")).resolves.toBe("seed\n");
1618
+ });
1619
+ it("stages git and workspace via syncIn preserving .paperclip-runtime (native runner → 0 direct writeFile/run)", async () => {
1620
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-native-git-"));
1621
+ cleanupDirs.push(rootDir);
1622
+ const sourceRepoDir = path.join(rootDir, "source-repo");
1623
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
1624
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1625
+ await mkdir(sourceRepoDir, { recursive: true });
1626
+ await git(sourceRepoDir, ["init"]);
1627
+ await git(sourceRepoDir, ["checkout", "-b", "main"]);
1628
+ await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
1629
+ await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
1630
+ await writeFile(path.join(sourceRepoDir, "tracked.txt"), "tracked\n", "utf8");
1631
+ await git(sourceRepoDir, ["add", "tracked.txt"]);
1632
+ await git(sourceRepoDir, ["commit", "-m", "base"]);
1633
+ await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
1634
+ // Pre-seed the sandbox with a `.paperclip-runtime` dir that MUST survive.
1635
+ await mkdir(path.join(remoteWorkspaceDir, ".paperclip-runtime"), { recursive: true });
1636
+ await writeFile(path.join(remoteWorkspaceDir, ".paperclip-runtime", "keep.txt"), "keep\n", "utf8");
1637
+ const directWrites = [];
1638
+ const directRuns = [];
1639
+ const client = {
1640
+ makeDir: async (remotePath) => {
1641
+ await mkdir(remotePath, { recursive: true });
1642
+ },
1643
+ writeFile: async (remotePath, bytes) => {
1644
+ directWrites.push(remotePath);
1645
+ await mkdir(path.dirname(remotePath), { recursive: true });
1646
+ await writeFile(remotePath, Buffer.from(bytes));
1647
+ },
1648
+ readFile: async (remotePath) => await readFile(remotePath),
1649
+ listFiles: async () => [],
1650
+ remove: async (remotePath) => {
1651
+ await rm(remotePath, { recursive: true, force: true });
1652
+ },
1653
+ run: async (command) => {
1654
+ directRuns.push(command);
1655
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1656
+ },
1657
+ };
1658
+ const captured = [];
1659
+ attachNativeRecordingSyncIn(client, captured);
1660
+ const prepared = await prepareSandboxManagedRuntime({
1661
+ spec: {
1662
+ transport: "sandbox",
1663
+ provider: "test",
1664
+ sandboxId: "sandbox-1",
1665
+ remoteCwd: remoteWorkspaceDir,
1666
+ timeoutMs: 30_000,
1667
+ apiKey: null,
1668
+ },
1669
+ adapterKey: "test-adapter",
1670
+ client,
1671
+ workspaceLocalDir: localWorkspaceDir,
1672
+ });
1673
+ // Delegated entirely to syncIn.
1674
+ expect(directWrites).toEqual([]);
1675
+ expect(directRuns).toEqual([]);
1676
+ // One merged operation carries BOTH the git-history and workspace-overlay tars
1677
+ // as two `file` mappings, each with its extract as an ordered post-command.
1678
+ expect(captured).toHaveLength(1);
1679
+ const op = captured[0];
1680
+ const byBase = (base) => op.files.find((mapping) => path.posix.basename(mapping.targetPath) === base);
1681
+ expect(byBase("git-workspace-upload.tar")).toBeDefined();
1682
+ expect(byBase("workspace-upload.tar")).toBeDefined();
1683
+ expect(op.files.every((mapping) => mapping.kind === "file")).toBe(true);
1684
+ // The first post-upload command extracts the git history and preserves
1685
+ // `.paperclip-runtime` while replacing the rest of the tree (wipe-except-preserved).
1686
+ const gitCommand = op.postUploadCommands[0].command;
1687
+ expect(gitCommand).toContain(".paperclip-runtime");
1688
+ expect(gitCommand).toContain("tar -xf");
1689
+ // The pre-seeded runtime dir survived the git+workspace staging.
1690
+ await expect(readFile(path.join(remoteWorkspaceDir, ".paperclip-runtime", "keep.txt"), "utf8")).resolves.toBe("keep\n");
1691
+ await expect(readFile(path.join(remoteWorkspaceDir, "tracked.txt"), "utf8")).resolves.toBe("tracked\n");
1692
+ expect(prepared.workspaceRemoteDir).toBe(remoteWorkspaceDir);
1693
+ });
1694
+ it("the workspace wipe command preserves in-flight sync scratch tarballs (.paperclip-upload-*)", async () => {
1695
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-scratch-shape-"));
1696
+ cleanupDirs.push(rootDir);
1697
+ const sourceRepoDir = path.join(rootDir, "source-repo");
1698
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
1699
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1700
+ await mkdir(sourceRepoDir, { recursive: true });
1701
+ await git(sourceRepoDir, ["init"]);
1702
+ await git(sourceRepoDir, ["checkout", "-b", "main"]);
1703
+ await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
1704
+ await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
1705
+ await writeFile(path.join(sourceRepoDir, "tracked.txt"), "tracked\n", "utf8");
1706
+ await git(sourceRepoDir, ["add", "tracked.txt"]);
1707
+ await git(sourceRepoDir, ["commit", "-m", "base"]);
1708
+ await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
1709
+ const client = {
1710
+ makeDir: async (remotePath) => {
1711
+ await mkdir(remotePath, { recursive: true });
1712
+ },
1713
+ writeFile: async (remotePath, bytes) => {
1714
+ await mkdir(path.dirname(remotePath), { recursive: true });
1715
+ await writeFile(remotePath, Buffer.from(bytes));
1716
+ },
1717
+ readFile: async (remotePath) => await readFile(remotePath),
1718
+ listFiles: async () => [],
1719
+ remove: async (remotePath) => {
1720
+ await rm(remotePath, { recursive: true, force: true });
1721
+ },
1722
+ run: async (command) => {
1723
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1724
+ },
1725
+ };
1726
+ const captured = [];
1727
+ attachNativeRecordingSyncIn(client, captured);
1728
+ await prepareSandboxManagedRuntime({
1729
+ spec: {
1730
+ transport: "sandbox",
1731
+ provider: "test",
1732
+ sandboxId: "sandbox-1",
1733
+ remoteCwd: remoteWorkspaceDir,
1734
+ timeoutMs: 30_000,
1735
+ apiKey: null,
1736
+ },
1737
+ adapterKey: "test-adapter",
1738
+ client,
1739
+ workspaceLocalDir: localWorkspaceDir,
1740
+ });
1741
+ // The wipe `find` runs before the extract. It must preserve the daytona
1742
+ // scratch prefix so a concurrent referenced-project upload survives the wipe.
1743
+ const wipeCommand = captured[0].postUploadCommands[0].command;
1744
+ expect(wipeCommand).toContain("find ");
1745
+ expect(wipeCommand).toContain("! -name '.paperclip-upload-*'");
1746
+ });
1747
+ it("the workspace wipe keeps an in-flight scratch tarball at the root but removes a stale sibling", async () => {
1748
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-scratch-race-"));
1749
+ cleanupDirs.push(rootDir);
1750
+ const sourceRepoDir = path.join(rootDir, "source-repo");
1751
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
1752
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1753
+ await mkdir(sourceRepoDir, { recursive: true });
1754
+ await git(sourceRepoDir, ["init"]);
1755
+ await git(sourceRepoDir, ["checkout", "-b", "main"]);
1756
+ await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
1757
+ await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
1758
+ await writeFile(path.join(sourceRepoDir, "tracked.txt"), "tracked\n", "utf8");
1759
+ await git(sourceRepoDir, ["add", "tracked.txt"]);
1760
+ await git(sourceRepoDir, ["commit", "-m", "base"]);
1761
+ await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
1762
+ // Pre-seed the sandbox root. `.paperclip-upload-test.tar` simulates a
1763
+ // concurrent referenced-project scratch tarball in flight; `stale-junk.txt`
1764
+ // is an unrelated child that the wipe must remove.
1765
+ await mkdir(remoteWorkspaceDir, { recursive: true });
1766
+ await writeFile(path.join(remoteWorkspaceDir, ".paperclip-upload-test.tar"), "scratch\n", "utf8");
1767
+ await writeFile(path.join(remoteWorkspaceDir, "stale-junk.txt"), "junk\n", "utf8");
1768
+ const client = {
1769
+ makeDir: async (remotePath) => {
1770
+ await mkdir(remotePath, { recursive: true });
1771
+ },
1772
+ writeFile: async (remotePath, bytes) => {
1773
+ await mkdir(path.dirname(remotePath), { recursive: true });
1774
+ await writeFile(remotePath, Buffer.from(bytes));
1775
+ },
1776
+ readFile: async (remotePath) => await readFile(remotePath),
1777
+ listFiles: async () => [],
1778
+ remove: async (remotePath) => {
1779
+ await rm(remotePath, { recursive: true, force: true });
1780
+ },
1781
+ run: async (command) => {
1782
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1783
+ },
1784
+ };
1785
+ const captured = [];
1786
+ attachNativeRecordingSyncIn(client, captured);
1787
+ await prepareSandboxManagedRuntime({
1788
+ spec: {
1789
+ transport: "sandbox",
1790
+ provider: "test",
1791
+ sandboxId: "sandbox-1",
1792
+ remoteCwd: remoteWorkspaceDir,
1793
+ timeoutMs: 30_000,
1794
+ apiKey: null,
1795
+ },
1796
+ adapterKey: "test-adapter",
1797
+ client,
1798
+ workspaceLocalDir: localWorkspaceDir,
1799
+ });
1800
+ // The real `find` wipe ran through `sh -c`. The scratch tarball survived and
1801
+ // the unrelated sibling did not.
1802
+ await expect(readFile(path.join(remoteWorkspaceDir, ".paperclip-upload-test.tar"), "utf8")).resolves.toBe("scratch\n");
1803
+ await expect(readFile(path.join(remoteWorkspaceDir, "stale-junk.txt"), "utf8")).rejects.toThrow();
1804
+ });
1805
+ it("issues one merged syncIn operation for a git-backed workspace stage-sync with two ordered extract commands", async () => {
1806
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-merged-git-"));
1807
+ cleanupDirs.push(rootDir);
1808
+ const sourceRepoDir = path.join(rootDir, "source-repo");
1809
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
1810
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1811
+ await mkdir(sourceRepoDir, { recursive: true });
1812
+ await git(sourceRepoDir, ["init"]);
1813
+ await git(sourceRepoDir, ["checkout", "-b", "main"]);
1814
+ await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
1815
+ await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
1816
+ await writeFile(path.join(sourceRepoDir, "tracked.txt"), "tracked\n", "utf8");
1817
+ await git(sourceRepoDir, ["add", "tracked.txt"]);
1818
+ await git(sourceRepoDir, ["commit", "-m", "base"]);
1819
+ await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
1820
+ // Pre-seed the sandbox with a `.paperclip-runtime` dir that MUST survive.
1821
+ await mkdir(path.join(remoteWorkspaceDir, ".paperclip-runtime"), { recursive: true });
1822
+ await writeFile(path.join(remoteWorkspaceDir, ".paperclip-runtime", "keep.txt"), "keep\n", "utf8");
1823
+ const client = {
1824
+ makeDir: async (remotePath) => {
1825
+ await mkdir(remotePath, { recursive: true });
1826
+ },
1827
+ writeFile: async (remotePath, bytes) => {
1828
+ await mkdir(path.dirname(remotePath), { recursive: true });
1829
+ await writeFile(remotePath, Buffer.from(bytes));
1830
+ },
1831
+ readFile: async (remotePath) => await readFile(remotePath),
1832
+ listFiles: async () => [],
1833
+ remove: async (remotePath) => {
1834
+ await rm(remotePath, { recursive: true, force: true });
1835
+ },
1836
+ run: async (command) => {
1837
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1838
+ },
1839
+ };
1840
+ const captured = [];
1841
+ attachNativeRecordingSyncIn(client, captured);
1842
+ // Count how many times `syncIn` is invoked so the merge collapses the two
1843
+ // workspace staging steps into a single native round trip.
1844
+ let syncInCallCount = 0;
1845
+ const recordingSyncIn = client.syncIn;
1846
+ client.syncIn = async (operations) => {
1847
+ syncInCallCount += 1;
1848
+ return recordingSyncIn(operations);
1849
+ };
1850
+ await prepareSandboxManagedRuntime({
1851
+ spec: {
1852
+ transport: "sandbox",
1853
+ provider: "test",
1854
+ sandboxId: "sandbox-1",
1855
+ remoteCwd: remoteWorkspaceDir,
1856
+ timeoutMs: 30_000,
1857
+ apiKey: null,
1858
+ },
1859
+ adapterKey: "test-adapter",
1860
+ client,
1861
+ workspaceLocalDir: localWorkspaceDir,
1862
+ });
1863
+ // One `syncIn` call carrying exactly one operation for the whole workspace.
1864
+ expect(syncInCallCount).toBe(1);
1865
+ expect(captured).toHaveLength(1);
1866
+ const op = captured[0];
1867
+ // Both host tars ride the one operation as two `file` mappings.
1868
+ const byBase = (base) => op.files.find((mapping) => path.posix.basename(mapping.targetPath) === base);
1869
+ const gitMapping = byBase("git-workspace-upload.tar");
1870
+ const overlayMapping = byBase("workspace-upload.tar");
1871
+ expect(gitMapping).toBeDefined();
1872
+ expect(overlayMapping).toBeDefined();
1873
+ expect(op.files).toHaveLength(2);
1874
+ expect(op.files.every((mapping) => mapping.kind === "file")).toBe(true);
1875
+ // Both tar targets live under `.paperclip-runtime`, so the git extract's wipe
1876
+ // (which preserves `.paperclip-runtime`) cannot delete the overlay tar before
1877
+ // the overlay extract runs.
1878
+ for (const mapping of op.files) {
1879
+ expect(mapping.targetPath).toContain("/.paperclip-runtime/");
1880
+ }
1881
+ // Two ordered extract commands: git history first (wipe-except-preserved),
1882
+ // overlay second (merge, no wipe). No deleted paths in this clean worktree.
1883
+ const commands = op.postUploadCommands ?? [];
1884
+ expect(commands).toHaveLength(2);
1885
+ expect(commands[0].command).toContain("git-workspace-upload.tar");
1886
+ expect(commands[0].command).toContain(".paperclip-runtime");
1887
+ expect(commands[0].command).toContain("find ");
1888
+ expect(commands[1].command).toContain("workspace-upload.tar");
1889
+ expect(commands[1].command).not.toContain("git-workspace-upload.tar");
1890
+ expect(commands[1].command).not.toContain("find ");
1891
+ // The pre-seeded runtime dir survived and the workspace overlay applied.
1892
+ await expect(readFile(path.join(remoteWorkspaceDir, ".paperclip-runtime", "keep.txt"), "utf8")).resolves.toBe("keep\n");
1893
+ await expect(readFile(path.join(remoteWorkspaceDir, "tracked.txt"), "utf8")).resolves.toBe("tracked\n");
1894
+ });
1895
+ it("the merged workspace confine guard covers both tar mappings (escape in either trips it)", () => {
1896
+ const runtimeRoot = "/home/daytona/paperclip-workspace/.paperclip-runtime/test-adapter";
1897
+ const tempRoot = "/tmp/paperclip-sandbox-sync-abc";
1898
+ const gitMapping = {
1899
+ sourcePath: `${tempRoot}/git-workspace.tar`,
1900
+ targetPath: `${runtimeRoot}/git-workspace-upload.tar`,
1901
+ kind: "file",
1902
+ };
1903
+ const overlayMapping = {
1904
+ sourcePath: `${tempRoot}/workspace.tar`,
1905
+ targetPath: `${runtimeRoot}/workspace-upload.tar`,
1906
+ kind: "file",
1907
+ };
1908
+ const roots = { sourceRoots: [tempRoot], targetRoots: [runtimeRoot] };
1909
+ // A confined merged operation with both tar mappings passes the guard.
1910
+ expect(() => assertSyncOperationsConfined([{ operationId: "merged", files: [gitMapping, overlayMapping] }], roots)).not.toThrow();
1911
+ // A `..` target escape in the overlay mapping trips the guard, so the whole
1912
+ // merged operation is rejected before any transfer.
1913
+ expect(() => assertSyncOperationsConfined([{
1914
+ operationId: "merged",
1915
+ files: [
1916
+ gitMapping,
1917
+ { ...overlayMapping, targetPath: `${runtimeRoot}/../../etc/workspace-upload.tar` },
1918
+ ],
1919
+ }], roots)).toThrow(/escapes its confinement root|not a confined absolute path/);
1920
+ // An absolute-path source escape in the git mapping trips the guard too.
1921
+ expect(() => assertSyncOperationsConfined([{
1922
+ operationId: "merged",
1923
+ files: [{ ...gitMapping, sourcePath: "/etc/passwd" }, overlayMapping],
1924
+ }], roots)).toThrow(/escapes its confinement root|not a confined absolute path/);
1925
+ });
1926
+ // Regression lock: a representative `codex_local` start stages its inbound bytes
1927
+ // as TWO `syncIn` operations. The git-history and workspace-overlay tars share
1928
+ // ONE merged operation (one native `uploadFiles` round-trip that carries both
1929
+ // tars, with the two extract commands as ordered `postUploadCommands`); the
1930
+ // managed Codex `home` asset (auth.json merge) is the second operation. Every
1931
+ // inbound step is routed through `client.syncIn`, with no separate
1932
+ // custom-provision diversion. Assert the collapsed round-trip count so a future
1933
+ // change that re-inlines a `writeFile`+`run` sequence — or splits the merged
1934
+ // workspace operation back into two — fails loudly here instead of silently
1935
+ // regressing the start path.
1936
+ it("collapses a representative codex_local start to two syncIn round-trips: one merged workspace op plus the asset op", async () => {
1937
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-codex-roundtrip-"));
1938
+ cleanupDirs.push(rootDir);
1939
+ const sourceRepoDir = path.join(rootDir, "source-repo");
1940
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
1941
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
1942
+ const homeDir = path.join(rootDir, "codex-home");
1943
+ // Git-backed workspace → git history + workspace overlay share one merged op.
1944
+ await mkdir(sourceRepoDir, { recursive: true });
1945
+ await git(sourceRepoDir, ["init"]);
1946
+ await git(sourceRepoDir, ["checkout", "-b", "main"]);
1947
+ await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
1948
+ await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
1949
+ await writeFile(path.join(sourceRepoDir, "tracked.txt"), "tracked\n", "utf8");
1950
+ await git(sourceRepoDir, ["add", "tracked.txt"]);
1951
+ await git(sourceRepoDir, ["commit", "-m", "base"]);
1952
+ await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
1953
+ // Managed Codex home with an auth.json that a custom post-upload command
1954
+ // merges in-sandbox — the credential path is routed onto native uploadFiles.
1955
+ await mkdir(homeDir, { recursive: true });
1956
+ await writeFile(path.join(homeDir, "auth.json"), "{\"OPENAI_API_KEY\":\"sk-test\"}\n", "utf8");
1957
+ await writeFile(path.join(homeDir, "config.toml"), "model = \"gpt\"\n", "utf8");
1958
+ // A native runner delegates every staging step to `syncIn`; ANY direct
1959
+ // writeFile/run exec is a collapse regression.
1960
+ const directWrites = [];
1961
+ const directRuns = [];
1962
+ const client = {
1963
+ makeDir: async (remotePath) => {
1964
+ await mkdir(remotePath, { recursive: true });
1965
+ },
1966
+ writeFile: async (remotePath, bytes) => {
1967
+ directWrites.push(remotePath);
1968
+ await mkdir(path.dirname(remotePath), { recursive: true });
1969
+ await writeFile(remotePath, Buffer.from(bytes));
1970
+ },
1971
+ readFile: async (remotePath) => await readFile(remotePath),
1972
+ listFiles: async () => [],
1973
+ remove: async (remotePath) => {
1974
+ await rm(remotePath, { recursive: true, force: true });
1975
+ },
1976
+ run: async (command) => {
1977
+ directRuns.push(command);
1978
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
1979
+ },
1980
+ };
1981
+ const captured = [];
1982
+ attachNativeRecordingSyncIn(client, captured);
1983
+ const q = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
1984
+ const prepared = await prepareSandboxManagedRuntime({
1985
+ spec: {
1986
+ transport: "sandbox",
1987
+ provider: "test",
1988
+ sandboxId: "sandbox-1",
1989
+ remoteCwd: remoteWorkspaceDir,
1990
+ timeoutMs: 30_000,
1991
+ apiKey: null,
1992
+ },
1993
+ adapterKey: "codex",
1994
+ client,
1995
+ workspaceLocalDir: localWorkspaceDir,
1996
+ assets: [{
1997
+ key: "home",
1998
+ localDir: homeDir,
1999
+ provision: {
2000
+ stageFiles: [{ name: "home-merge.sh", contents: "#!/bin/sh\ntar -xf \"$2\" -C \"$1\"\n" }],
2001
+ postUploadCommand: ({ assetTarPath, assetDir, runtimeRootDir }) => `mkdir -p ${q(assetDir)} && ` +
2002
+ `sh ${q(path.posix.join(runtimeRootDir, "home-merge.sh"))} ${q(assetDir)} ${q(assetTarPath)} && ` +
2003
+ `rm -f ${q(assetTarPath)}`,
2004
+ },
2005
+ }],
2006
+ });
2007
+ // The orchestrator delegated everything to syncIn: no re-inlined writeFile/run.
2008
+ expect(directWrites).toEqual([]);
2009
+ expect(directRuns).toEqual([]);
2010
+ // The collapsed count: exactly two inbound round-trips — the merged workspace
2011
+ // op (git history + overlay) and the home asset op.
2012
+ expect(captured).toHaveLength(2);
2013
+ const hasBase = (op, base) => op.files.some((mapping) => path.posix.basename(mapping.targetPath) === base);
2014
+ const workspaceOp = captured.find((op) => hasBase(op, "workspace-upload.tar"));
2015
+ const homeOp = captured.find((op) => hasBase(op, "home-upload.tar"));
2016
+ expect(workspaceOp).toBeDefined();
2017
+ expect(homeOp).toBeDefined();
2018
+ // The merged workspace op carries BOTH the git-history and overlay tars, with
2019
+ // both extract commands as ordered post-upload commands (git first, overlay
2020
+ // second). The two tars ride one native uploadFiles round-trip.
2021
+ expect(hasBase(workspaceOp, "git-workspace-upload.tar")).toBe(true);
2022
+ expect(workspaceOp.files).toHaveLength(2);
2023
+ expect((workspaceOp.postUploadCommands ?? []).length).toBeGreaterThanOrEqual(2);
2024
+ // Every operation is a native uploadFiles (all `file` mappings) whose
2025
+ // extract/merge rides as an ordered provider-executed post-upload command.
2026
+ for (const op of captured) {
2027
+ expect(op.files.length).toBeGreaterThanOrEqual(1);
2028
+ expect(op.files.every((mapping) => mapping.kind === "file")).toBe(true);
2029
+ expect(op.postUploadCommands ?? []).not.toHaveLength(0);
2030
+ }
2031
+ // Operation ids are distinct, so "2 operations" is 2 real round-trips.
2032
+ expect(new Set(captured.map((op) => op.operationId)).size).toBe(2);
2033
+ // The credential asset actually materialized through the native seam.
2034
+ await expect(readFile(path.join(prepared.assetDirs.home, "auth.json"), "utf8"))
2035
+ .resolves.toBe("{\"OPENAI_API_KEY\":\"sk-test\"}\n");
2036
+ });
2037
+ it("authors the advisory access intent rw on workspace, git, and asset inbound mappings", async () => {
2038
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-access-rw-"));
2039
+ cleanupDirs.push(rootDir);
2040
+ const sourceRepoDir = path.join(rootDir, "source-repo");
2041
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
2042
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
2043
+ const assetDir = path.join(rootDir, "asset-home");
2044
+ // A git-backed workspace produces both a git-history tar and an overlay tar.
2045
+ await mkdir(sourceRepoDir, { recursive: true });
2046
+ await git(sourceRepoDir, ["init"]);
2047
+ await git(sourceRepoDir, ["checkout", "-b", "main"]);
2048
+ await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
2049
+ await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
2050
+ await writeFile(path.join(sourceRepoDir, "tracked.txt"), "tracked\n", "utf8");
2051
+ await git(sourceRepoDir, ["add", "tracked.txt"]);
2052
+ await git(sourceRepoDir, ["commit", "-m", "base"]);
2053
+ await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
2054
+ await mkdir(assetDir, { recursive: true });
2055
+ await writeFile(path.join(assetDir, "config.toml"), "model = \"gpt\"\n", "utf8");
2056
+ const client = {
2057
+ makeDir: async (remotePath) => { await mkdir(remotePath, { recursive: true }); },
2058
+ writeFile: async (remotePath, bytes) => {
2059
+ await mkdir(path.dirname(remotePath), { recursive: true });
2060
+ await writeFile(remotePath, Buffer.from(bytes));
2061
+ },
2062
+ readFile: async (remotePath) => await readFile(remotePath),
2063
+ listFiles: async () => [],
2064
+ remove: async (remotePath) => { await rm(remotePath, { recursive: true, force: true }); },
2065
+ run: async (command) => { await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 }); },
2066
+ };
2067
+ const captured = [];
2068
+ attachCapturingSyncIn(client, captured);
2069
+ await prepareSandboxManagedRuntime({
2070
+ spec: {
2071
+ transport: "sandbox",
2072
+ provider: "test",
2073
+ sandboxId: "sandbox-1",
2074
+ remoteCwd: remoteWorkspaceDir,
2075
+ timeoutMs: 30_000,
2076
+ apiKey: null,
2077
+ },
2078
+ adapterKey: "test-adapter",
2079
+ client,
2080
+ workspaceLocalDir: localWorkspaceDir,
2081
+ assets: [{ key: "home", localDir: assetDir }],
2082
+ });
2083
+ const findMapping = (base) => captured
2084
+ .flatMap((op) => op.files)
2085
+ .find((mapping) => path.posix.basename(mapping.targetPath) === base);
2086
+ // The workspace, git-history, and asset destinations receive read-write bytes,
2087
+ // so the author marks each mapping `access: "rw"`.
2088
+ expect(findMapping("workspace-upload.tar")?.access).toBe("rw");
2089
+ expect(findMapping("git-workspace-upload.tar")?.access).toBe("rw");
2090
+ expect(findMapping("home-upload.tar")?.access).toBe("rw");
2091
+ // Each tar mapping uploads a staging archive under the runtime root, so its
2092
+ // `targetPath` is not the read-write destination. `writablePath` names the
2093
+ // directory that the post-upload extract command fills: the workspace
2094
+ // directory for the workspace and git tars, and the asset directory for the
2095
+ // asset tar.
2096
+ const remoteAssetDir = path.posix.join(remoteWorkspaceDir, ".paperclip-runtime", "test-adapter", "home");
2097
+ expect(findMapping("workspace-upload.tar")?.writablePath).toBe(remoteWorkspaceDir);
2098
+ expect(findMapping("git-workspace-upload.tar")?.writablePath).toBe(remoteWorkspaceDir);
2099
+ expect(findMapping("home-upload.tar")?.writablePath).toBe(remoteAssetDir);
2100
+ });
2101
+ it("authors the advisory access intent ro on referenced-project inbound mappings", async () => {
2102
+ const flagKey = "PAPERCLIP_MULTI_PROJECT_WORKSPACE_SYNC";
2103
+ const priorFlag = process.env[flagKey];
2104
+ process.env[flagKey] = "1";
2105
+ try {
2106
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-access-ro-"));
2107
+ cleanupDirs.push(rootDir);
2108
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
2109
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
2110
+ const referencedDir = path.join(rootDir, "referenced-project");
2111
+ await mkdir(localWorkspaceDir, { recursive: true });
2112
+ await mkdir(referencedDir, { recursive: true });
2113
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "anchor\n", "utf8");
2114
+ await writeFile(path.join(referencedDir, "notes.md"), "reference\n", "utf8");
2115
+ const client = {
2116
+ makeDir: async (remotePath) => { await mkdir(remotePath, { recursive: true }); },
2117
+ writeFile: async (remotePath, bytes) => {
2118
+ await mkdir(path.dirname(remotePath), { recursive: true });
2119
+ await writeFile(remotePath, Buffer.from(bytes));
2120
+ },
2121
+ readFile: async (remotePath) => await readFile(remotePath),
2122
+ listFiles: async () => [],
2123
+ remove: async (remotePath) => { await rm(remotePath, { recursive: true, force: true }); },
2124
+ run: async (command) => { await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 }); },
2125
+ };
2126
+ const captured = [];
2127
+ attachCapturingSyncIn(client, captured);
2128
+ await prepareSandboxManagedRuntime({
2129
+ spec: {
2130
+ transport: "sandbox",
2131
+ provider: "test",
2132
+ sandboxId: "sandbox-1",
2133
+ remoteCwd: remoteWorkspaceDir,
2134
+ timeoutMs: 30_000,
2135
+ apiKey: null,
2136
+ },
2137
+ adapterKey: "test-adapter",
2138
+ client,
2139
+ workspaceLocalDir: localWorkspaceDir,
2140
+ additionalSources: [{ localPath: referencedDir, projectId: "proj-first", ignoreResolution: { kind: "other" } }],
2141
+ });
2142
+ const referencedMapping = captured
2143
+ .flatMap((op) => op.files)
2144
+ .find((mapping) => path.posix.basename(mapping.targetPath) === "project-proj-first");
2145
+ // A referenced project is a read-only tree, so the author marks it `access: "ro"`.
2146
+ expect(referencedMapping).toBeDefined();
2147
+ expect(referencedMapping?.kind).toBe("directory");
2148
+ expect(referencedMapping?.access).toBe("ro");
2149
+ }
2150
+ finally {
2151
+ if (priorFlag === undefined)
2152
+ delete process.env[flagKey];
2153
+ else
2154
+ process.env[flagKey] = priorFlag;
2155
+ }
2156
+ });
2157
+ it("reports the real transferred bytes for a referenced project's inbound staging", async () => {
2158
+ const flagKey = "PAPERCLIP_MULTI_PROJECT_WORKSPACE_SYNC";
2159
+ const priorFlag = process.env[flagKey];
2160
+ process.env[flagKey] = "1";
2161
+ try {
2162
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-project-bytes-"));
2163
+ cleanupDirs.push(rootDir);
2164
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
2165
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
2166
+ const referencedDir = path.join(rootDir, "referenced-project");
2167
+ await mkdir(localWorkspaceDir, { recursive: true });
2168
+ await mkdir(referencedDir, { recursive: true });
2169
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "anchor\n", "utf8");
2170
+ // A referenced project has no host tarball to `fs.stat`, so its progress
2171
+ // line depends entirely on the transport's own `bytesTransferred`. Give it
2172
+ // real, sizeable content (well above the 0.1 MB rounding step), so a bug
2173
+ // that keeps the line at 0 stays distinguishable from a correctly-reported
2174
+ // small transfer that would still round down to "0.0 MB".
2175
+ await writeFile(path.join(referencedDir, "notes.md"), Buffer.alloc(300 * 1024, "a"));
2176
+ const client = {
2177
+ makeDir: async (remotePath) => { await mkdir(remotePath, { recursive: true }); },
2178
+ writeFile: async (remotePath, bytes) => {
2179
+ await mkdir(path.dirname(remotePath), { recursive: true });
2180
+ await writeFile(remotePath, Buffer.from(bytes));
2181
+ },
2182
+ readFile: async (remotePath) => await readFile(remotePath),
2183
+ listFiles: async () => [],
2184
+ remove: async (remotePath) => { await rm(remotePath, { recursive: true, force: true }); },
2185
+ run: async (command) => { await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 }); },
2186
+ };
2187
+ attachCapturingSyncIn(client, []);
2188
+ const lines = [];
2189
+ await prepareSandboxManagedRuntime({
2190
+ spec: {
2191
+ transport: "sandbox",
2192
+ provider: "test",
2193
+ sandboxId: "sandbox-1",
2194
+ remoteCwd: remoteWorkspaceDir,
2195
+ timeoutMs: 30_000,
2196
+ apiKey: null,
2197
+ },
2198
+ adapterKey: "test-adapter",
2199
+ client,
2200
+ workspaceLocalDir: localWorkspaceDir,
2201
+ additionalSources: [{ localPath: referencedDir, projectId: "proj-first", ignoreResolution: { kind: "other" } }],
2202
+ onProgress: (line) => { lines.push(line); },
2203
+ });
2204
+ const projectLines = lines.filter((line) => line.includes("Syncing project-proj-first to environment"));
2205
+ expect(projectLines.length).toBeGreaterThan(0);
2206
+ // The transfer landed real bytes, so the terminal line carries the actual
2207
+ // byte total and a 100% completion, not the "0.0 MB" that a discarded
2208
+ // sync result would show.
2209
+ expect(projectLines.some((line) => line.includes("100%"))).toBe(true);
2210
+ expect(projectLines.every((line) => /\(\d+\.\d\/\d+\.\d MB\)/.test(line))).toBe(true);
2211
+ expect(projectLines.some((line) => line.includes("(0.0/0.0 MB)"))).toBe(false);
2212
+ }
2213
+ finally {
2214
+ if (priorFlag === undefined)
2215
+ delete process.env[flagKey];
2216
+ else
2217
+ process.env[flagKey] = priorFlag;
2218
+ }
2219
+ });
2220
+ it("keeps the sandbox runtime core free of Codex-specific string literals", async () => {
2221
+ const coreSource = await readFile(new URL("./sandbox-managed-runtime.ts", import.meta.url), "utf8");
2222
+ // The seam must be generic: no adapter (Codex) knowledge may live in the core.
2223
+ expect(coreSource).not.toMatch(/codex/i);
2224
+ expect(coreSource).not.toMatch(/auth\.json/i);
2225
+ expect(coreSource).not.toMatch(/merge-extract|merge-decision/i);
2226
+ });
2227
+ // End-to-end: drive the WHOLE prepare path — `prepareCommandManagedRuntime`
2228
+ // building the client, syncing the anchor workspace, then staging the
2229
+ // referenced projects — through a runner that runs real shell commands on the
2230
+ // host filesystem (host FS stands in for the sandbox FS). The runner exposes no
2231
+ // native syncIn, so staging rides the base64/tar fallback, the same transport a
2232
+ // provider without native sync uses. In production the kill-switch
2233
+ // `PAPERCLIP_MULTI_PROJECT_WORKSPACE_SYNC` gates whether run prep resolves any
2234
+ // referenced projects (OFF ⇒ none reach this layer). Enable it in-test only to
2235
+ // model the ON scenario, and prove multi-project isolation plus one-failure
2236
+ // isolation end-to-end.
2237
+ it("stages multiple referenced projects into isolated sandbox dirs end-to-end, skipping a failing source", async () => {
2238
+ const flagKey = "PAPERCLIP_MULTI_PROJECT_WORKSPACE_SYNC";
2239
+ const priorFlag = process.env[flagKey];
2240
+ process.env[flagKey] = "1";
2241
+ try {
2242
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-e2e-additional-"));
2243
+ cleanupDirs.push(rootDir);
2244
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
2245
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
2246
+ await mkdir(localWorkspaceDir, { recursive: true });
2247
+ await mkdir(remoteWorkspaceDir, { recursive: true });
2248
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "anchor content\n", "utf8");
2249
+ // Two real referenced-project checkouts (one with a nested file) plus a
2250
+ // deliberately-missing source between them.
2251
+ const first = path.join(rootDir, "referenced-first");
2252
+ const second = path.join(rootDir, "referenced-second");
2253
+ await mkdir(path.join(first, "docs"), { recursive: true });
2254
+ await mkdir(second, { recursive: true });
2255
+ await writeFile(path.join(first, "docs", "guide.md"), "first guide\n", "utf8");
2256
+ await writeFile(path.join(second, "notes.md"), "second notes\n", "utf8");
2257
+ const runner = {
2258
+ execute: (input) => new Promise((resolve) => {
2259
+ const startedAt = new Date().toISOString();
2260
+ const command = input.command === "sh" ? "/bin/sh" : input.command === "bash" ? "/bin/bash" : input.command;
2261
+ const child = spawn(command, input.args ?? [], { cwd: input.cwd, env: { ...process.env, ...input.env } });
2262
+ let stdout = "";
2263
+ let stderr = "";
2264
+ child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
2265
+ child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
2266
+ child.on("error", () => resolve({ exitCode: 127, signal: null, timedOut: false, stdout, stderr, pid: null, startedAt }));
2267
+ child.on("close", (code) => resolve({ exitCode: code ?? 0, signal: null, timedOut: false, stdout, stderr, pid: child.pid ?? null, startedAt }));
2268
+ if (input.stdin != null)
2269
+ child.stdin.write(input.stdin);
2270
+ child.stdin.end();
2271
+ }),
2272
+ };
2273
+ const prepared = await prepareCommandManagedRuntime({
2274
+ runner,
2275
+ spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
2276
+ adapterKey: "test-adapter",
2277
+ workspaceLocalDir: localWorkspaceDir,
2278
+ additionalSources: [
2279
+ { localPath: first, projectId: "proj-first", ignoreResolution: { kind: "other" } },
2280
+ { localPath: path.join(rootDir, "referenced-missing"), projectId: "proj-missing", ignoreResolution: { kind: "other" } },
2281
+ { localPath: second, projectId: "proj-second", ignoreResolution: { kind: "other" } },
2282
+ ],
2283
+ });
2284
+ const runtimeRootDir = path.posix.join(remoteWorkspaceDir, ".paperclip-runtime", "test-adapter");
2285
+ // The anchor workspace synced normally and stays byte-identical.
2286
+ await expect(readFile(path.join(remoteWorkspaceDir, "README.md"), "utf8")).resolves.toBe("anchor content\n");
2287
+ // Each healthy referenced project landed in its OWN isolated dir; the
2288
+ // missing one is skipped, not fatal.
2289
+ expect(Object.keys(prepared.additionalSourceDirs).sort()).toEqual(["proj-first", "proj-second"]);
2290
+ expect(prepared.additionalSourceDirs["proj-first"]).toBe(path.posix.join(runtimeRootDir, "project-proj-first"));
2291
+ expect(prepared.additionalSourceDirs["proj-second"]).toBe(path.posix.join(runtimeRootDir, "project-proj-second"));
2292
+ expect(prepared.additionalSourceDirs["proj-missing"]).toBeUndefined();
2293
+ // The skipped project is a first-class per-project failure outcome, not only a warning, so the
2294
+ // run can count it in the requested-vs-synced accounting. The two healthy projects do not
2295
+ // appear as failures.
2296
+ expect(prepared.additionalSourceFailures.map((failure) => failure.projectId)).toEqual([
2297
+ "proj-missing",
2298
+ ]);
2299
+ expect(prepared.additionalSourceFailures[0].error.length).toBeGreaterThan(0);
2300
+ await expect(readFile(path.join(prepared.additionalSourceDirs["proj-first"], "docs", "guide.md"), "utf8"))
2301
+ .resolves.toBe("first guide\n");
2302
+ await expect(readFile(path.join(prepared.additionalSourceDirs["proj-second"], "notes.md"), "utf8"))
2303
+ .resolves.toBe("second notes\n");
2304
+ // Neither project's tree leaked into the anchor workspace or into the other
2305
+ // project's dir.
2306
+ await expect(readFile(path.join(remoteWorkspaceDir, "notes.md"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
2307
+ await expect(readFile(path.join(prepared.additionalSourceDirs["proj-first"], "notes.md"), "utf8")).rejects
2308
+ .toMatchObject({ code: "ENOENT" });
2309
+ await expect(readFile(path.join(runtimeRootDir, "project-proj-missing"), "utf8")).rejects
2310
+ .toMatchObject({ code: "ENOENT" });
2311
+ }
2312
+ finally {
2313
+ if (priorFlag === undefined)
2314
+ delete process.env[flagKey];
2315
+ else
2316
+ process.env[flagKey] = priorFlag;
2317
+ }
2318
+ });
2319
+ describe("resolveReferencedSourceIgnore", () => {
2320
+ it("re-relativizes root-relative ignored paths to a nested localPath", async () => {
2321
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-nested-"));
2322
+ cleanupDirs.push(rootDir);
2323
+ const repo = path.join(rootDir, "repo");
2324
+ await initGitRepo(repo);
2325
+ // An ignored entry OUTSIDE the referenced project's localPath, and two
2326
+ // ignored entries inside it (one top-level, one nested).
2327
+ await writeFile(path.join(repo, ".gitignore"), "outside-secret.env\npackages/app/secret.env\npackages/app/build/\n", "utf8");
2328
+ await writeFile(path.join(repo, "outside-secret.env"), "outside\n", "utf8");
2329
+ const localPath = path.join(repo, "packages", "app");
2330
+ // Commit a tracked file under `localPath` first. Otherwise the whole
2331
+ // `packages/` directory is untracked, and `git status` collapses it to
2332
+ // one `packages/` line instead of reporting entries inside it
2333
+ // individually — the fixture needs the individual entries.
2334
+ await mkdir(localPath, { recursive: true });
2335
+ await writeFile(path.join(localPath, "index.ts"), "export {};\n", "utf8");
2336
+ await git(repo, ["add", "packages/app/index.ts"]);
2337
+ await git(repo, ["commit", "-qm", "add app"]);
2338
+ await mkdir(path.join(localPath, "build"), { recursive: true });
2339
+ await writeFile(path.join(localPath, "secret.env"), "TOKEN=abc\n", "utf8");
2340
+ await writeFile(path.join(localPath, "build", "out.js"), "artifact\n", "utf8");
2341
+ const resolution = await resolveReferencedSourceIgnore(localPath);
2342
+ // Only the entries under `localPath` apply, re-relativized to it — the
2343
+ // sibling `outside-secret.env` never appears, and the prefix
2344
+ // `packages/app/` is stripped.
2345
+ expect(resolution).toEqual({ kind: "git", ignoredPaths: ["build", "secret.env"] });
2346
+ });
2347
+ it("keeps today's fixed excludes for a non-Git source", async () => {
2348
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-nongit-"));
2349
+ cleanupDirs.push(rootDir);
2350
+ const plainDir = path.join(rootDir, "plain-project");
2351
+ await mkdir(plainDir, { recursive: true });
2352
+ await writeFile(path.join(plainDir, "file.txt"), "body\n", "utf8");
2353
+ await expect(resolveReferencedSourceIgnore(plainDir)).resolves.toEqual({ kind: "other" });
2354
+ });
2355
+ it("fails closed on a real Git error instead of returning an unfiltered result", async () => {
2356
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-fail-"));
2357
+ cleanupDirs.push(rootDir);
2358
+ const repo = path.join(rootDir, "repo");
2359
+ await initGitRepo(repo);
2360
+ // Corrupt the index so `git rev-parse --show-toplevel` still succeeds but
2361
+ // `git status --ignored` fails with a real error (not "not a git repository").
2362
+ await writeFile(path.join(repo, ".git", "index"), "not a valid index\n", "utf8");
2363
+ const resolution = await resolveReferencedSourceIgnore(repo);
2364
+ // The reason is the fixed category, never the caught error's own message
2365
+ // (which would embed `repo`, an absolute host path).
2366
+ expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.scanFailed });
2367
+ });
2368
+ // Nadia's required probes: none of these three example absolute paths —
2369
+ // an ordinary POSIX path, a home directory, and a Windows path — may ever
2370
+ // reach `reason`, however they arrive (a caught Git error, or a raw
2371
+ // toplevel string that makes `localPath` a non-descendant).
2372
+ const SENSITIVE_PATH_PROBES = ["/srv/alice/project", "/home/alice/project", "C:\\Users\\alice\\project"];
2373
+ for (const sensitivePath of SENSITIVE_PATH_PROBES) {
2374
+ it(`redacts a caught Git error embedding ${sensitivePath} to the fixed category`, async () => {
2375
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-redact-caught-"));
2376
+ cleanupDirs.push(rootDir);
2377
+ const repo = path.join(rootDir, "repo");
2378
+ await initGitRepo(repo);
2379
+ try {
2380
+ setExpensiveWorkspaceGitExecutor(async (input) => {
2381
+ if (input.operation === "referenced_source.ignored_files") {
2382
+ throw Object.assign(new Error(`fatal: unable to read tree object for ${sensitivePath}, pid 4242`), { stderr: `fatal: unable to read tree object for ${sensitivePath}, pid 4242` });
2383
+ }
2384
+ return await runLocalGit(input.localDir, [...input.args], {
2385
+ timeout: input.timeout,
2386
+ maxBuffer: input.maxBuffer,
2387
+ env: input.env,
2388
+ });
2389
+ });
2390
+ const resolution = await resolveReferencedSourceIgnore(repo);
2391
+ expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.scanFailed });
2392
+ }
2393
+ finally {
2394
+ setExpensiveWorkspaceGitExecutor(null);
2395
+ }
2396
+ });
2397
+ it(`redacts a non-descendant toplevel embedding ${sensitivePath} to the fixed category`, async () => {
2398
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-redact-nondescendant-"));
2399
+ cleanupDirs.push(rootDir);
2400
+ const localPath = path.join(rootDir, "referenced");
2401
+ await mkdir(localPath, { recursive: true });
2402
+ try {
2403
+ // A toplevel string with no relation to `localPath` — the resolver
2404
+ // must treat it as a non-descendant and fail closed with the fixed
2405
+ // category, never a message built from `sensitivePath` or `localPath`.
2406
+ setExpensiveWorkspaceGitExecutor(async (input) => {
2407
+ if (input.operation === "referenced_source.toplevel") {
2408
+ return { stdout: `${sensitivePath}\n`, stderr: "" };
2409
+ }
2410
+ return { stdout: "", stderr: "" };
2411
+ });
2412
+ const resolution = await resolveReferencedSourceIgnore(localPath);
2413
+ expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.toplevelNotDescendant });
2414
+ }
2415
+ finally {
2416
+ setExpensiveWorkspaceGitExecutor(null);
2417
+ }
2418
+ });
2419
+ }
2420
+ it("fails closed with a fixed category when the parsed ignored-entry count exceeds the bound", async () => {
2421
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-bound-count-"));
2422
+ cleanupDirs.push(rootDir);
2423
+ const repo = path.join(rootDir, "repo");
2424
+ await initGitRepo(repo);
2425
+ const overLimitCount = 10_001;
2426
+ const syntheticIgnored = `${Array.from({ length: overLimitCount }, (_, index) => `!! entry-${index}`).join("\0")}\0`;
2427
+ try {
2428
+ setExpensiveWorkspaceGitExecutor(async (input) => {
2429
+ if (input.operation === "referenced_source.ignored_files") {
2430
+ return { stdout: syntheticIgnored, stderr: "" };
2431
+ }
2432
+ return await runLocalGit(input.localDir, [...input.args], {
2433
+ timeout: input.timeout,
2434
+ maxBuffer: input.maxBuffer,
2435
+ env: input.env,
2436
+ });
2437
+ });
2438
+ const resolution = await resolveReferencedSourceIgnore(repo);
2439
+ expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.limitExceeded });
2440
+ }
2441
+ finally {
2442
+ setExpensiveWorkspaceGitExecutor(null);
2443
+ }
2444
+ });
2445
+ it("fails closed with a fixed category when the total UTF-8 byte size of ignored paths exceeds the bound", async () => {
2446
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-bound-bytes-"));
2447
+ cleanupDirs.push(rootDir);
2448
+ const repo = path.join(rootDir, "repo");
2449
+ await initGitRepo(repo);
2450
+ // One entry alone exceeds the 2 MiB bound, well under the entry-count bound.
2451
+ const hugeEntry = "a".repeat(3 * 1024 * 1024);
2452
+ const syntheticIgnored = `!! ${hugeEntry}\0`;
2453
+ try {
2454
+ setExpensiveWorkspaceGitExecutor(async (input) => {
2455
+ if (input.operation === "referenced_source.ignored_files") {
2456
+ return { stdout: syntheticIgnored, stderr: "" };
2457
+ }
2458
+ return await runLocalGit(input.localDir, [...input.args], {
2459
+ timeout: input.timeout,
2460
+ maxBuffer: input.maxBuffer,
2461
+ env: input.env,
2462
+ });
2463
+ });
2464
+ const resolution = await resolveReferencedSourceIgnore(repo);
2465
+ expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.limitExceeded });
2466
+ }
2467
+ finally {
2468
+ setExpensiveWorkspaceGitExecutor(null);
2469
+ }
2470
+ });
2471
+ it("stages no bytes for either a count-breach or a byte-breach project, and stages a healthy sibling", async () => {
2472
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-bound-staging-"));
2473
+ cleanupDirs.push(rootDir);
2474
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
2475
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
2476
+ const healthyDir = path.join(rootDir, "referenced-healthy");
2477
+ const countBreachDir = path.join(rootDir, "referenced-count-breach");
2478
+ const byteBreachDir = path.join(rootDir, "referenced-byte-breach");
2479
+ await mkdir(localWorkspaceDir, { recursive: true });
2480
+ await mkdir(healthyDir, { recursive: true });
2481
+ await mkdir(countBreachDir, { recursive: true });
2482
+ await mkdir(byteBreachDir, { recursive: true });
2483
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "anchor\n", "utf8");
2484
+ await writeFile(path.join(healthyDir, "notes.md"), "healthy\n", "utf8");
2485
+ await writeFile(path.join(countBreachDir, "should-never-ship.txt"), "must not stage\n", "utf8");
2486
+ await writeFile(path.join(byteBreachDir, "should-never-ship.txt"), "must not stage\n", "utf8");
2487
+ const countBreachReason = { kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.limitExceeded };
2488
+ const byteBreachReason = { kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.limitExceeded };
2489
+ const prepared = await prepareCommandManagedRuntime({
2490
+ runner: makeInlineSpawnRunner(),
2491
+ spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
2492
+ adapterKey: "test-adapter",
2493
+ workspaceLocalDir: localWorkspaceDir,
2494
+ additionalSources: [
2495
+ { localPath: healthyDir, projectId: "healthy", ignoreResolution: { kind: "other" } },
2496
+ { localPath: countBreachDir, projectId: "count-breach", ignoreResolution: countBreachReason },
2497
+ { localPath: byteBreachDir, projectId: "byte-breach", ignoreResolution: byteBreachReason },
2498
+ ],
2499
+ });
2500
+ expect(Object.keys(prepared.additionalSourceDirs).sort()).toEqual(["healthy"]);
2501
+ expect(prepared.additionalSourceFailures.map((failure) => failure.projectId).sort()).toEqual([
2502
+ "byte-breach",
2503
+ "count-breach",
2504
+ ]);
2505
+ const runtimeRootDir = path.posix.join(remoteWorkspaceDir, ".paperclip-runtime", "test-adapter");
2506
+ await expect(readFile(path.join(runtimeRootDir, "project-count-breach", "should-never-ship.txt"), "utf8")).rejects
2507
+ .toMatchObject({ code: "ENOENT" });
2508
+ await expect(readFile(path.join(runtimeRootDir, "project-byte-breach", "should-never-ship.txt"), "utf8")).rejects
2509
+ .toMatchObject({ code: "ENOENT" });
2510
+ });
2511
+ describe("saturation retry", () => {
2512
+ afterEach(() => {
2513
+ vi.useRealTimers();
2514
+ setExpensiveWorkspaceGitExecutor(null);
2515
+ });
2516
+ function throwSaturated() {
2517
+ throw Object.assign(new Error("Changed files are temporarily unavailable because the Git scan queue is full"), { code: WORKSPACE_GIT_SCAN_SATURATED_CODE });
2518
+ }
2519
+ // Every case here synthesizes BOTH scan operations at the executor seam
2520
+ // instead of spawning real `git` — the property under test is the
2521
+ // retry's own timing and attempt count, and a fake-timer-driven test
2522
+ // must not also depend on a real child process's independent, real-time
2523
+ // completion. `toplevel` need not exist on disk: `resolveReferencedSourceIgnore`
2524
+ // falls back to a plain string compare when `fs.realpath` fails, and the
2525
+ // fixture path is used unchanged on both sides of that compare.
2526
+ const repo = "/fixture/referenced-project";
2527
+ it("retries a saturated scan up to two times and succeeds on the third attempt", async () => {
2528
+ let ignoredCallCount = 0;
2529
+ setExpensiveWorkspaceGitExecutor(async (input) => {
2530
+ if (input.operation === "referenced_source.toplevel") {
2531
+ return { stdout: `${repo}\n`, stderr: "" };
2532
+ }
2533
+ ignoredCallCount += 1;
2534
+ if (ignoredCallCount <= 2)
2535
+ throwSaturated();
2536
+ return { stdout: "", stderr: "" };
2537
+ });
2538
+ vi.useFakeTimers();
2539
+ const resolutionPromise = resolveReferencedSourceIgnore(repo);
2540
+ // Bounded backoff: none before attempt 1, 1 s before attempt 2, 2 s
2541
+ // before attempt 3.
2542
+ await vi.advanceTimersByTimeAsync(1_000);
2543
+ await vi.advanceTimersByTimeAsync(2_000);
2544
+ const resolution = await resolutionPromise;
2545
+ expect(resolution).toEqual({ kind: "git", ignoredPaths: [] });
2546
+ expect(ignoredCallCount).toBe(3);
2547
+ });
2548
+ it("fails closed after three saturated attempts, with no further Git invocation", async () => {
2549
+ let ignoredCallCount = 0;
2550
+ setExpensiveWorkspaceGitExecutor(async (input) => {
2551
+ if (input.operation === "referenced_source.toplevel") {
2552
+ return { stdout: `${repo}\n`, stderr: "" };
2553
+ }
2554
+ ignoredCallCount += 1;
2555
+ throwSaturated();
2556
+ });
2557
+ vi.useFakeTimers();
2558
+ const resolutionPromise = resolveReferencedSourceIgnore(repo);
2559
+ await vi.advanceTimersByTimeAsync(1_000);
2560
+ await vi.advanceTimersByTimeAsync(2_000);
2561
+ const resolution = await resolutionPromise;
2562
+ expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.scanFailed });
2563
+ // Three total attempts (the first plus two retries) — no fourth,
2564
+ // unscheduled invocation past the retry budget.
2565
+ expect(ignoredCallCount).toBe(3);
2566
+ });
2567
+ it("makes exactly one attempt and fails closed on a timeout, never retrying it", async () => {
2568
+ let ignoredCallCount = 0;
2569
+ setExpensiveWorkspaceGitExecutor(async (input) => {
2570
+ if (input.operation === "referenced_source.toplevel") {
2571
+ return { stdout: `${repo}\n`, stderr: "" };
2572
+ }
2573
+ ignoredCallCount += 1;
2574
+ throw Object.assign(new Error("Workspace Git scan timed out after 8000ms"), {
2575
+ code: "workspace_git_scan_timeout",
2576
+ });
2577
+ });
2578
+ const resolution = await resolveReferencedSourceIgnore(repo);
2579
+ expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.scanFailed });
2580
+ expect(ignoredCallCount).toBe(1);
2581
+ });
2582
+ it("makes exactly one attempt and fails closed on an output-limit breach, never retrying it", async () => {
2583
+ let ignoredCallCount = 0;
2584
+ setExpensiveWorkspaceGitExecutor(async (input) => {
2585
+ if (input.operation === "referenced_source.toplevel") {
2586
+ return { stdout: `${repo}\n`, stderr: "" };
2587
+ }
2588
+ ignoredCallCount += 1;
2589
+ throw Object.assign(new Error("Workspace Git scan exceeded its output limit"), {
2590
+ code: "workspace_git_scan_output_limit",
2591
+ });
2592
+ });
2593
+ const resolution = await resolveReferencedSourceIgnore(repo);
2594
+ expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.scanFailed });
2595
+ expect(ignoredCallCount).toBe(1);
2596
+ });
2597
+ });
2598
+ });
2599
+ it("escapes glob metacharacters so a literal ignored path never over-excludes a sibling", async () => {
2600
+ // A bare tar `--exclude` pattern treats `*`/`?`/`[` as globs. Without
2601
+ // escaping, an ignored file named `secret[1].txt` would exclude the
2602
+ // unrelated sibling `secret1.txt` too (both match the glob `secret[1].txt`,
2603
+ // whose `[1]` is a one-character class matching the literal digit `1`).
2604
+ expect(escapeTarExcludeLiteral("secret[1].txt")).toBe("secret\\[1].txt");
2605
+ expect(escapeTarExcludeLiteral("wildcard*name")).toBe("wildcard\\*name");
2606
+ expect(escapeTarExcludeLiteral("question?mark")).toBe("question\\?mark");
2607
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-glob-"));
2608
+ cleanupDirs.push(rootDir);
2609
+ const referencedDir = path.join(rootDir, "referenced-project");
2610
+ await initGitRepo(referencedDir);
2611
+ // The gitignore pattern itself escapes `[` and `]` (gitignore patterns are
2612
+ // globs too), so it ignores ONLY the literal file `secret[1].txt`.
2613
+ await writeFile(path.join(referencedDir, ".gitignore"), "secret\\[1\\].txt\n", "utf8");
2614
+ await writeFile(path.join(referencedDir, "secret[1].txt"), "ignored\n", "utf8");
2615
+ // A sibling that would ALSO match the UNESCAPED tar exclude glob
2616
+ // `secret[1].txt` (its `[1]` is a one-character class matching `1`), if the
2617
+ // staging path failed to escape the ignored entry before passing it to tar.
2618
+ await writeFile(path.join(referencedDir, "secret1.txt"), "must stay\n", "utf8");
2619
+ const ignoreResolution = await resolveReferencedSourceIgnore(referencedDir);
2620
+ expect(ignoreResolution).toEqual({ kind: "git", ignoredPaths: ["secret[1].txt"] });
2621
+ const prepared = await stageOneReferencedProject(referencedDir, ignoreResolution);
2622
+ const stagedDir = prepared.additionalSourceDirs["proj"];
2623
+ await expect(readFile(path.join(stagedDir, "secret[1].txt"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
2624
+ await expect(readFile(path.join(stagedDir, "secret1.txt"), "utf8")).resolves.toBe("must stay\n");
2625
+ });
2626
+ it("never ships a Git-ignored secret in a referenced project's staged tree", async () => {
2627
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-secret-"));
2628
+ cleanupDirs.push(rootDir);
2629
+ const referencedDir = path.join(rootDir, "referenced-project");
2630
+ await initGitRepo(referencedDir);
2631
+ await writeFile(path.join(referencedDir, ".gitignore"), "secret.env\n", "utf8");
2632
+ await writeFile(path.join(referencedDir, "secret.env"), "TOKEN=abc\n", "utf8");
2633
+ await writeFile(path.join(referencedDir, "tracked.md"), "kept\n", "utf8");
2634
+ const ignoreResolution = await resolveReferencedSourceIgnore(referencedDir);
2635
+ expect(ignoreResolution).toEqual({ kind: "git", ignoredPaths: ["secret.env"] });
2636
+ const prepared = await stageOneReferencedProject(referencedDir, ignoreResolution);
2637
+ const stagedDir = prepared.additionalSourceDirs["proj"];
2638
+ await expect(readFile(path.join(stagedDir, "secret.env"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
2639
+ await expect(readFile(path.join(stagedDir, "tracked.md"), "utf8")).resolves.toBe("kept\n");
2640
+ });
2641
+ it("stages no bytes for a project whose ignore resolution failed, and stages the rest", async () => {
2642
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-failed-skip-"));
2643
+ cleanupDirs.push(rootDir);
2644
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
2645
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
2646
+ const healthyDir = path.join(rootDir, "referenced-healthy");
2647
+ const failedDir = path.join(rootDir, "referenced-failed");
2648
+ await mkdir(localWorkspaceDir, { recursive: true });
2649
+ await mkdir(healthyDir, { recursive: true });
2650
+ await mkdir(failedDir, { recursive: true });
2651
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "anchor\n", "utf8");
2652
+ await writeFile(path.join(healthyDir, "notes.md"), "healthy\n", "utf8");
2653
+ await writeFile(path.join(failedDir, "should-never-ship.txt"), "must not stage\n", "utf8");
2654
+ const prepared = await prepareCommandManagedRuntime({
2655
+ runner: makeInlineSpawnRunner(),
2656
+ spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
2657
+ adapterKey: "test-adapter",
2658
+ workspaceLocalDir: localWorkspaceDir,
2659
+ additionalSources: [
2660
+ { localPath: healthyDir, projectId: "healthy", ignoreResolution: { kind: "other" } },
2661
+ { localPath: failedDir, projectId: "failed", ignoreResolution: { kind: "failed", reason: "boom: git status timed out" } },
2662
+ ],
2663
+ });
2664
+ // The failed project is not staged at all (fail closed) — no bytes reach the
2665
+ // sandbox for it — and is recorded as a first-class failure. The healthy
2666
+ // project stages normally.
2667
+ expect(Object.keys(prepared.additionalSourceDirs)).toEqual(["healthy"]);
2668
+ expect(prepared.additionalSourceFailures.map((failure) => failure.projectId)).toEqual(["failed"]);
2669
+ expect(prepared.additionalSourceFailures[0].error).toContain("boom: git status timed out");
2670
+ const runtimeRootDir = path.posix.join(remoteWorkspaceDir, ".paperclip-runtime", "test-adapter");
2671
+ await expect(readFile(path.join(runtimeRootDir, "project-failed", "should-never-ship.txt"), "utf8")).rejects
2672
+ .toMatchObject({ code: "ENOENT" });
2673
+ });
2674
+ it("builds the workspace tarball inside one host pack span for a usual workspace sync", async () => {
2675
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-pack-span-"));
2676
+ cleanupDirs.push(rootDir);
2677
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
2678
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
2679
+ await mkdir(localWorkspaceDir, { recursive: true });
2680
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace body\n", "utf8");
2681
+ // Record every span name the runner opens and run the wrapped work, so the
2682
+ // test proves the host opens a span around each host-side staging sub-step
2683
+ // for the usual (plain) workspace sync: the git enumeration, the baseline
2684
+ // content-hash walk, and the tarball build, in that order.
2685
+ const openedSpans = [];
2686
+ const runtimeSpan = async (name, work) => {
2687
+ openedSpans.push(name);
2688
+ return await work();
2689
+ };
2690
+ const prepared = await prepareSandboxManagedRuntime({
2691
+ spec: {
2692
+ transport: "sandbox",
2693
+ provider: "test",
2694
+ sandboxId: "sandbox-pack",
2695
+ remoteCwd: remoteWorkspaceDir,
2696
+ timeoutMs: 30_000,
2697
+ apiKey: null,
2698
+ },
2699
+ adapterKey: "test-adapter",
2700
+ client: makeFilesystemClient(),
2701
+ workspaceLocalDir: localWorkspaceDir,
2702
+ runtimeSpan,
2703
+ });
2704
+ // The workspace stage task opens its own `stage.workspace` span, and the
2705
+ // host tarball build opens the `pack` span inside it. The two pre-task
2706
+ // sub-steps stay ahead of the task.
2707
+ expect(openedSpans).toEqual(["snapshot.git", "snapshot.baseline", "stage.workspace", "pack"]);
2708
+ // The tarball build still lands the workspace inside the span, so the wrap
2709
+ // changes no staging behavior.
2710
+ await expect(readFile(path.join(remoteWorkspaceDir, "README.md"), "utf8")).resolves.toBe("workspace body\n");
2711
+ expect(prepared.workspaceRemoteDir).toBe(remoteWorkspaceDir);
2712
+ });
2713
+ it("nests the host pack span under the stage.workspace task span", async () => {
2714
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-pack-nest-"));
2715
+ cleanupDirs.push(rootDir);
2716
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
2717
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
2718
+ await mkdir(localWorkspaceDir, { recursive: true });
2719
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace body\n", "utf8");
2720
+ const { traceContext, spans } = createRecordingTraceContext();
2721
+ // The root span stands in for `sandbox.startup`. Its child context is the
2722
+ // step span's parent, exactly as the executor wires it.
2723
+ const rootHandle = traceContext.tracer.startSpan("sandbox.startup", undefined, undefined);
2724
+ const rootContext = traceContext.contextWithSpan(rootHandle);
2725
+ // The stage runner parents each span to the ACTIVE startup step, so the
2726
+ // `pack` span nests under `stage.sync`. This is the exact runner the
2727
+ // executor threads into the staging seam.
2728
+ const stageRuntimeSpan = createRuntimeSpanRunner(traceContext, () => getActiveStepContext()?.parentContext);
2729
+ // A deterministic monotonic clock, so the step timing stays test-stable.
2730
+ let clock = 0;
2731
+ const now = () => (clock += 1000);
2732
+ await measureStartupStep({}, now, "stage.sync", async () => {
2733
+ await prepareSandboxManagedRuntime({
2734
+ spec: {
2735
+ transport: "sandbox",
2736
+ provider: "test",
2737
+ sandboxId: "sandbox-nest",
2738
+ remoteCwd: remoteWorkspaceDir,
2739
+ timeoutMs: 30_000,
2740
+ apiKey: null,
2741
+ },
2742
+ adapterKey: "test-adapter",
2743
+ client: makeFilesystemClient(),
2744
+ workspaceLocalDir: localWorkspaceDir,
2745
+ runtimeSpan: stageRuntimeSpan,
2746
+ });
2747
+ }, {
2748
+ tracer: traceContext.tracer,
2749
+ parentContext: rootContext,
2750
+ contextWithSpan: (span) => traceContext.contextWithSpan(span),
2751
+ });
2752
+ const stageSpan = spans.find((span) => span.name === "stage.sync");
2753
+ const workspaceSpan = spans.find((span) => span.name === "stage.workspace");
2754
+ const packSpan = spans.find((span) => span.name === "pack");
2755
+ expect(stageSpan).toBeDefined();
2756
+ expect(workspaceSpan).toBeDefined();
2757
+ expect(packSpan).toBeDefined();
2758
+ expect(packSpan.ended).toBe(true);
2759
+ // The workspace stage task opens its own `stage.workspace` span under
2760
+ // `stage.sync`, and the `pack` span nests under `stage.workspace`.
2761
+ expect(workspaceSpan.parentName).toBe("stage.sync");
2762
+ expect(packSpan.parentName).toBe("stage.workspace");
2763
+ // The two pre-`pack` staging sub-steps nest under `stage.sync` the same way,
2764
+ // so the previously hidden gap at the head of the step is now attributed.
2765
+ for (const name of ["snapshot.git", "snapshot.baseline"]) {
2766
+ const span = spans.find((candidate) => candidate.name === name);
2767
+ expect(span, name).toBeDefined();
2768
+ expect(span.ended).toBe(true);
2769
+ expect(span.parentName).toBe("stage.sync");
2770
+ }
2771
+ });
2772
+ });
2773
+ function defer() {
2774
+ let resolve;
2775
+ let reject;
2776
+ const promise = new Promise((res, rej) => {
2777
+ resolve = res;
2778
+ reject = rej;
2779
+ });
2780
+ return { promise, resolve, reject };
2781
+ }
2782
+ function settleTick(ms) {
2783
+ return new Promise((resolve) => setTimeout(resolve, ms));
2784
+ }
2785
+ // A span recorder that captures each opened span name and the set of spans that
2786
+ // are open right now. `opened` records every span in open order. `openNow` holds
2787
+ // the names of the spans that started but did not end yet, so a test proves two
2788
+ // concurrent tasks keep their spans open at the same time.
2789
+ function makeSpanRecorder() {
2790
+ const opened = [];
2791
+ const openNow = new Set();
2792
+ const runtimeSpan = async (name, work) => {
2793
+ opened.push(name);
2794
+ openNow.add(name);
2795
+ try {
2796
+ return await work();
2797
+ }
2798
+ finally {
2799
+ openNow.delete(name);
2800
+ }
2801
+ };
2802
+ return { runtimeSpan, opened, openNow };
2803
+ }
2804
+ function labelOfOperation(operation) {
2805
+ const bases = operation.files.map((mapping) => path.posix.basename(mapping.targetPath));
2806
+ if (bases.some((base) => base === "workspace-upload.tar" || base === "git-workspace-upload.tar")) {
2807
+ return "workspace";
2808
+ }
2809
+ const assetBase = bases.find((base) => base.endsWith("-upload.tar"));
2810
+ if (assetBase) {
2811
+ return assetBase.slice(0, -"-upload.tar".length);
2812
+ }
2813
+ const projectBase = bases.find((base) => base.startsWith("project-"));
2814
+ if (projectBase) {
2815
+ return projectBase;
2816
+ }
2817
+ return bases[0] ?? operation.operationId;
2818
+ }
2819
+ function makeControlledSyncClient(options) {
2820
+ const started = [];
2821
+ const settled = [];
2822
+ const gates = new Map();
2823
+ const failures = new Map();
2824
+ const startWaiters = new Map();
2825
+ const control = {
2826
+ started,
2827
+ settled,
2828
+ waitForStart(label) {
2829
+ if (started.includes(label)) {
2830
+ return Promise.resolve();
2831
+ }
2832
+ return new Promise((resolve) => {
2833
+ const waiters = startWaiters.get(label) ?? [];
2834
+ waiters.push(resolve);
2835
+ startWaiters.set(label, waiters);
2836
+ });
2837
+ },
2838
+ hold(label) {
2839
+ if (!gates.has(label)) {
2840
+ gates.set(label, defer());
2841
+ }
2842
+ },
2843
+ release(label) {
2844
+ gates.get(label)?.resolve();
2845
+ },
2846
+ failWith(label, error) {
2847
+ failures.set(label, error);
2848
+ gates.get(label)?.resolve();
2849
+ },
2850
+ };
2851
+ const noop = async () => { };
2852
+ const client = {
2853
+ makeDir: noop,
2854
+ writeFile: noop,
2855
+ readFile: async () => Buffer.alloc(0),
2856
+ listFiles: async () => [],
2857
+ remove: noop,
2858
+ run: noop,
2859
+ allowConcurrentSyncOperations: options.concurrent,
2860
+ syncIn: async (operations) => {
2861
+ const operation = operations[0];
2862
+ const label = labelOfOperation(operation);
2863
+ started.push(label);
2864
+ const waiters = startWaiters.get(label) ?? [];
2865
+ startWaiters.delete(label);
2866
+ for (const waiter of waiters) {
2867
+ waiter();
2868
+ }
2869
+ try {
2870
+ const gate = gates.get(label);
2871
+ if (gate) {
2872
+ await gate.promise;
2873
+ }
2874
+ const failure = failures.get(label);
2875
+ if (failure) {
2876
+ throw failure;
2877
+ }
2878
+ return {
2879
+ operations: [{
2880
+ operationId: operation.operationId,
2881
+ filesTransferred: operation.files.length,
2882
+ bytesTransferred: 0,
2883
+ }],
2884
+ };
2885
+ }
2886
+ finally {
2887
+ settled.push(label);
2888
+ }
2889
+ },
2890
+ };
2891
+ return { client, control };
2892
+ }
2893
+ describe("sandbox managed runtime inbound coordinator", () => {
2894
+ const cleanupDirs = [];
2895
+ afterEach(async () => {
2896
+ while (cleanupDirs.length > 0) {
2897
+ const dir = cleanupDirs.pop();
2898
+ if (!dir)
2899
+ continue;
2900
+ await rm(dir, { recursive: true, force: true }).catch(() => undefined);
2901
+ }
2902
+ });
2903
+ function makeSpec(remoteCwd) {
2904
+ return {
2905
+ transport: "sandbox",
2906
+ provider: "test",
2907
+ sandboxId: "sandbox-1",
2908
+ remoteCwd,
2909
+ timeoutMs: 30_000,
2910
+ apiKey: null,
2911
+ };
2912
+ }
2913
+ // Create a temp root with one workspace directory and any named asset/project
2914
+ // directories. Each directory carries one file, so the host tar step has bytes.
2915
+ async function makeInboundDirs(names) {
2916
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-inbound-coordinator-"));
2917
+ cleanupDirs.push(rootDir);
2918
+ const workspaceDir = path.join(rootDir, "workspace");
2919
+ await mkdir(workspaceDir, { recursive: true });
2920
+ await writeFile(path.join(workspaceDir, "file.txt"), "workspace\n", "utf8");
2921
+ const dirs = new Map();
2922
+ for (const name of names) {
2923
+ const dir = path.join(rootDir, name);
2924
+ await mkdir(dir, { recursive: true });
2925
+ await writeFile(path.join(dir, "file.txt"), `${name}\n`, "utf8");
2926
+ dirs.set(name, dir);
2927
+ }
2928
+ return { rootDir, workspaceDir, dirOf: (name) => dirs.get(name) };
2929
+ }
2930
+ // Resolve true when the label started within the window, false on timeout.
2931
+ async function startedWithin(control, label, ms) {
2932
+ return Promise.race([
2933
+ control.waitForStart(label).then(() => true),
2934
+ settleTick(ms).then(() => false),
2935
+ ]);
2936
+ }
2937
+ it("with concurrency permitted, the home asset upload starts while the workspace upload is held open", async () => {
2938
+ const { workspaceDir, dirOf } = await makeInboundDirs(["home"]);
2939
+ const { client, control } = makeControlledSyncClient({ concurrent: true });
2940
+ control.hold("workspace");
2941
+ const prepared = prepareSandboxManagedRuntime({
2942
+ spec: makeSpec("/remote/cwd"),
2943
+ adapterKey: "codex",
2944
+ client,
2945
+ workspaceLocalDir: workspaceDir,
2946
+ assets: [{ key: "home", localDir: dirOf("home") }],
2947
+ });
2948
+ prepared.catch(() => undefined);
2949
+ // The workspace upload is open and held. The home asset upload still starts,
2950
+ // so the coordinator runs the two operations concurrently.
2951
+ await control.waitForStart("workspace");
2952
+ expect(await startedWithin(control, "home", 4000)).toBe(true);
2953
+ expect(control.settled).not.toContain("workspace");
2954
+ control.release("workspace");
2955
+ await prepared;
2956
+ expect(control.settled).toContain("home");
2957
+ });
2958
+ it("with five operations and the bound of 4, the fifth operation does not start while four stay open", async () => {
2959
+ expect(SYNC_OPERATION_CONCURRENCY_LIMIT).toBe(4);
2960
+ const assetKeys = Array.from({ length: SYNC_OPERATION_CONCURRENCY_LIMIT + 1 }, (_unused, index) => `asset-${index}`);
2961
+ const { workspaceDir, dirOf } = await makeInboundDirs(assetKeys);
2962
+ const { client, control } = makeControlledSyncClient({ concurrent: true });
2963
+ for (const key of assetKeys) {
2964
+ control.hold(key);
2965
+ }
2966
+ const prepared = prepareSandboxManagedRuntime({
2967
+ spec: makeSpec("/remote/cwd"),
2968
+ adapterKey: "codex",
2969
+ client,
2970
+ syncWorkspace: false,
2971
+ workspaceLocalDir: workspaceDir,
2972
+ assets: assetKeys.map((key) => ({ key, localDir: dirOf(key) })),
2973
+ });
2974
+ prepared.catch(() => undefined);
2975
+ const firstFour = assetKeys.slice(0, SYNC_OPERATION_CONCURRENCY_LIMIT);
2976
+ const fifth = assetKeys[SYNC_OPERATION_CONCURRENCY_LIMIT];
2977
+ for (const key of firstFour) {
2978
+ await control.waitForStart(key);
2979
+ }
2980
+ // The bound holds the fifth operation while four stay open.
2981
+ expect(await startedWithin(control, fifth, 300)).toBe(false);
2982
+ expect(control.started.slice().sort()).toEqual(firstFour.slice().sort());
2983
+ // One release frees one slot, so the fifth operation starts.
2984
+ control.release(firstFour[0]);
2985
+ expect(await startedWithin(control, fifth, 4000)).toBe(true);
2986
+ for (const key of assetKeys) {
2987
+ control.release(key);
2988
+ }
2989
+ await prepared;
2990
+ expect(control.settled.slice().sort()).toEqual(assetKeys.slice().sort());
2991
+ });
2992
+ it("holds one upload open after another upload fails, and returns only after both settle", async () => {
2993
+ const { workspaceDir, dirOf } = await makeInboundDirs(["asset-a", "asset-b"]);
2994
+ const { client, control } = makeControlledSyncClient({ concurrent: true });
2995
+ control.hold("asset-b");
2996
+ let coordinatorSettled = false;
2997
+ const prepared = prepareSandboxManagedRuntime({
2998
+ spec: makeSpec("/remote/cwd"),
2999
+ adapterKey: "codex",
3000
+ client,
3001
+ syncWorkspace: false,
3002
+ workspaceLocalDir: workspaceDir,
3003
+ assets: [
3004
+ { key: "asset-a", localDir: dirOf("asset-a") },
3005
+ { key: "asset-b", localDir: dirOf("asset-b") },
3006
+ ],
3007
+ });
3008
+ const done = prepared.then(() => { coordinatorSettled = true; }, () => { coordinatorSettled = true; });
3009
+ control.failWith("asset-a", new Error("asset-a-fail"));
3010
+ await control.waitForStart("asset-b");
3011
+ await settleTick(100);
3012
+ // The first upload already failed. The second upload is still open, so the
3013
+ // coordinator must not return yet.
3014
+ expect(coordinatorSettled).toBe(false);
3015
+ expect(control.settled).toContain("asset-a");
3016
+ expect(control.settled).not.toContain("asset-b");
3017
+ control.release("asset-b");
3018
+ await done;
3019
+ expect(coordinatorSettled).toBe(true);
3020
+ expect(control.settled).toEqual(expect.arrayContaining(["asset-a", "asset-b"]));
3021
+ });
3022
+ it("records a referenced-project failure as nonfatal and finishes the other referenced-project uploads", async () => {
3023
+ const { workspaceDir, dirOf } = await makeInboundDirs(["good", "bad"]);
3024
+ const { client, control } = makeControlledSyncClient({ concurrent: true });
3025
+ control.failWith("project-bad", new Error("bad-upload"));
3026
+ const prepared = await prepareSandboxManagedRuntime({
3027
+ spec: makeSpec("/remote/cwd"),
3028
+ adapterKey: "codex",
3029
+ client,
3030
+ syncWorkspace: false,
3031
+ workspaceLocalDir: workspaceDir,
3032
+ additionalSources: [
3033
+ { localPath: dirOf("good"), projectId: "good", ignoreResolution: { kind: "other" } },
3034
+ { localPath: dirOf("bad"), projectId: "bad", ignoreResolution: { kind: "other" } },
3035
+ ],
3036
+ });
3037
+ // The healthy project synced; the failed project is a recorded, nonfatal
3038
+ // outcome, so the coordinator resolved.
3039
+ expect(Object.keys(prepared.additionalSourceDirs)).toEqual(["good"]);
3040
+ expect(prepared.additionalSourceFailures.map((failure) => failure.projectId)).toEqual(["bad"]);
3041
+ expect(prepared.additionalSourceFailures[0].error).toContain("bad-upload");
3042
+ expect(control.settled).toContain("project-good");
3043
+ });
3044
+ it("raises an asset failure as fatal after the barrier", async () => {
3045
+ const { workspaceDir, dirOf } = await makeInboundDirs(["asset-good", "asset-bad"]);
3046
+ const { client, control } = makeControlledSyncClient({ concurrent: true });
3047
+ control.failWith("asset-bad", new Error("asset-bad-fail"));
3048
+ await expect(prepareSandboxManagedRuntime({
3049
+ spec: makeSpec("/remote/cwd"),
3050
+ adapterKey: "codex",
3051
+ client,
3052
+ syncWorkspace: false,
3053
+ workspaceLocalDir: workspaceDir,
3054
+ assets: [
3055
+ { key: "asset-good", localDir: dirOf("asset-good") },
3056
+ { key: "asset-bad", localDir: dirOf("asset-bad") },
3057
+ ],
3058
+ })).rejects.toThrow("asset-bad-fail");
3059
+ // The barrier still settles the healthy asset before the coordinator raises.
3060
+ expect(control.settled).toContain("asset-good");
3061
+ });
3062
+ it("raises the earlier required failure when the workspace and an asset both fail", async () => {
3063
+ const { workspaceDir, dirOf } = await makeInboundDirs(["asset-a"]);
3064
+ const { client, control } = makeControlledSyncClient({ concurrent: true });
3065
+ control.failWith("workspace", new Error("workspace-fail"));
3066
+ control.failWith("asset-a", new Error("asset-a-fail"));
3067
+ // The workspace comes before the asset in stable operation order, so the
3068
+ // coordinator raises the workspace failure.
3069
+ await expect(prepareSandboxManagedRuntime({
3070
+ spec: makeSpec("/remote/cwd"),
3071
+ adapterKey: "codex",
3072
+ client,
3073
+ workspaceLocalDir: workspaceDir,
3074
+ assets: [{ key: "asset-a", localDir: dirOf("asset-a") }],
3075
+ })).rejects.toThrow("workspace-fail");
3076
+ });
3077
+ it("with concurrency forbidden, keeps the serial schedule in the current order", async () => {
3078
+ const { workspaceDir, dirOf } = await makeInboundDirs(["asset-a"]);
3079
+ const { client, control } = makeControlledSyncClient({ concurrent: false });
3080
+ control.hold("workspace");
3081
+ const prepared = prepareSandboxManagedRuntime({
3082
+ spec: makeSpec("/remote/cwd"),
3083
+ adapterKey: "codex",
3084
+ client,
3085
+ workspaceLocalDir: workspaceDir,
3086
+ assets: [{ key: "asset-a", localDir: dirOf("asset-a") }],
3087
+ });
3088
+ prepared.catch(() => undefined);
3089
+ // The workspace runs first and is held open. Serial mode runs one operation
3090
+ // at a time, so the asset upload does not start until the workspace settles.
3091
+ await control.waitForStart("workspace");
3092
+ expect(await startedWithin(control, "asset-a", 300)).toBe(false);
3093
+ expect(control.started).toEqual(["workspace"]);
3094
+ control.release("workspace");
3095
+ await control.waitForStart("asset-a");
3096
+ await prepared;
3097
+ // The serial order stays workspace first, then the asset.
3098
+ expect(control.started).toEqual(["workspace", "asset-a"]);
3099
+ });
3100
+ it("opens one named span per inbound task: stage.workspace, stage.asset.<key>, stage.project.<id>", async () => {
3101
+ const { workspaceDir, dirOf } = await makeInboundDirs(["home", "proj"]);
3102
+ const { client } = makeControlledSyncClient({ concurrent: false });
3103
+ const { runtimeSpan, opened } = makeSpanRecorder();
3104
+ await prepareSandboxManagedRuntime({
3105
+ spec: makeSpec("/remote/cwd"),
3106
+ adapterKey: "codex",
3107
+ client,
3108
+ workspaceLocalDir: workspaceDir,
3109
+ assets: [{ key: "home", localDir: dirOf("home") }],
3110
+ additionalSources: [{ localPath: dirOf("proj"), projectId: "proj-1", ignoreResolution: { kind: "other" } }],
3111
+ runtimeSpan,
3112
+ });
3113
+ // Each inbound task carries its own named span. The workspace task, the home
3114
+ // asset task, and the referenced-project task each open one.
3115
+ expect(opened).toContain("stage.workspace");
3116
+ expect(opened).toContain("stage.asset.home");
3117
+ expect(opened).toContain("stage.project.proj-1");
3118
+ });
3119
+ it("with concurrency permitted, the workspace and asset inbound task spans overlap in time", async () => {
3120
+ const { workspaceDir, dirOf } = await makeInboundDirs(["home"]);
3121
+ const { client, control } = makeControlledSyncClient({ concurrent: true });
3122
+ const { runtimeSpan, openNow } = makeSpanRecorder();
3123
+ control.hold("workspace");
3124
+ control.hold("home");
3125
+ const prepared = prepareSandboxManagedRuntime({
3126
+ spec: makeSpec("/remote/cwd"),
3127
+ adapterKey: "codex",
3128
+ client,
3129
+ workspaceLocalDir: workspaceDir,
3130
+ assets: [{ key: "home", localDir: dirOf("home") }],
3131
+ runtimeSpan,
3132
+ });
3133
+ prepared.catch(() => undefined);
3134
+ // Both uploads are held open at their transfer. Each task span opens before
3135
+ // its transfer and stays open while the transfer is held, so the two spans
3136
+ // are open at the same time.
3137
+ await control.waitForStart("workspace");
3138
+ await control.waitForStart("home");
3139
+ expect(openNow.has("stage.workspace")).toBe(true);
3140
+ expect(openNow.has("stage.asset.home")).toBe(true);
3141
+ control.release("workspace");
3142
+ control.release("home");
3143
+ await prepared;
3144
+ });
3145
+ });
3146
+ function makeOutboundControl() {
3147
+ const started = [];
3148
+ const settled = [];
3149
+ const restoreTempDirs = new Map();
3150
+ const gates = new Map();
3151
+ const failures = new Map();
3152
+ const startWaiters = new Map();
3153
+ const control = {
3154
+ started,
3155
+ settled,
3156
+ restoreTempDirs,
3157
+ waitForStart(label) {
3158
+ if (started.includes(label)) {
3159
+ return Promise.resolve();
3160
+ }
3161
+ return new Promise((resolve) => {
3162
+ const waiters = startWaiters.get(label) ?? [];
3163
+ waiters.push(resolve);
3164
+ startWaiters.set(label, waiters);
3165
+ });
3166
+ },
3167
+ hold(label) {
3168
+ if (!gates.has(label)) {
3169
+ gates.set(label, defer());
3170
+ }
3171
+ },
3172
+ release(label) {
3173
+ gates.get(label)?.resolve();
3174
+ },
3175
+ failWith(label, error) {
3176
+ failures.set(label, error);
3177
+ gates.get(label)?.resolve();
3178
+ },
3179
+ };
3180
+ // Record the task start, notify start waiters, wait for the gate, raise a set
3181
+ // failure, then run the task body and record the settle. A gate that a test
3182
+ // never holds resolves at once, so an unheld task runs straight through.
3183
+ async function gate(label, run) {
3184
+ started.push(label);
3185
+ const waiters = startWaiters.get(label) ?? [];
3186
+ startWaiters.delete(label);
3187
+ for (const waiter of waiters) {
3188
+ waiter();
3189
+ }
3190
+ try {
3191
+ const held = gates.get(label);
3192
+ if (held) {
3193
+ await held.promise;
3194
+ }
3195
+ const failure = failures.get(label);
3196
+ if (failure) {
3197
+ throw failure;
3198
+ }
3199
+ return await run();
3200
+ }
3201
+ finally {
3202
+ settled.push(label);
3203
+ }
3204
+ }
3205
+ return { control, gate };
3206
+ }
3207
+ // A native filesystem client whose `syncOut` copies the sandbox workspace back
3208
+ // through the gate. The inbound prepare step uses the base64-tar fallback
3209
+ // `syncIn`. The client opts into concurrency by the flag.
3210
+ function makeGatedOutboundClient(concurrent, gate) {
3211
+ const client = {
3212
+ makeDir: async (remotePath) => {
3213
+ await mkdir(remotePath, { recursive: true });
3214
+ },
3215
+ writeFile: async (remotePath, bytes) => {
3216
+ await mkdir(path.dirname(remotePath), { recursive: true });
3217
+ await writeFile(remotePath, Buffer.from(bytes));
3218
+ },
3219
+ readFile: async (remotePath) => await readFile(remotePath),
3220
+ listFiles: async (remotePath) => {
3221
+ const entries = await readdir(remotePath, { withFileTypes: true }).catch(() => []);
3222
+ return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
3223
+ },
3224
+ remove: async (remotePath) => {
3225
+ await rm(remotePath, { recursive: true, force: true });
3226
+ },
3227
+ run: async (command) => {
3228
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
3229
+ },
3230
+ allowConcurrentSyncOperations: concurrent,
3231
+ syncOut: async (operations) => gate("workspace", async () => {
3232
+ for (const operation of operations) {
3233
+ for (const mapping of operation.files) {
3234
+ if (mapping.kind === "directory") {
3235
+ await mirrorDirectory(mapping.sourcePath, mapping.targetPath);
3236
+ }
3237
+ else {
3238
+ await mkdir(path.dirname(mapping.targetPath), { recursive: true });
3239
+ await writeFile(mapping.targetPath, await readFile(mapping.sourcePath));
3240
+ }
3241
+ }
3242
+ }
3243
+ return {
3244
+ operations: operations.map((operation) => ({
3245
+ operationId: operation.operationId,
3246
+ filesTransferred: operation.files.length,
3247
+ bytesTransferred: 0,
3248
+ })),
3249
+ };
3250
+ }),
3251
+ };
3252
+ attachFallbackSyncIn(client);
3253
+ return client;
3254
+ }
3255
+ // Build one asset with a controlled `restore`. The restore records its own
3256
+ // temp directory, then runs through the gate. The temp directory proves that
3257
+ // two concurrent restore tasks keep separate scratch state.
3258
+ function makeControlledAsset(key, localDir, control, gate) {
3259
+ return {
3260
+ key,
3261
+ localDir,
3262
+ restore: async (ctx) => {
3263
+ control.restoreTempDirs.set(key, ctx.tempDir);
3264
+ await gate(key, async () => {
3265
+ if (ctx.tempDir) {
3266
+ await writeFile(path.join(ctx.tempDir, `scratch-${key}.txt`), key, "utf8");
3267
+ }
3268
+ });
3269
+ },
3270
+ };
3271
+ }
3272
+ describe("sandbox managed runtime outbound coordinator", () => {
3273
+ const cleanupDirs = [];
3274
+ afterEach(async () => {
3275
+ while (cleanupDirs.length > 0) {
3276
+ const dir = cleanupDirs.pop();
3277
+ if (!dir)
3278
+ continue;
3279
+ await rm(dir, { recursive: true, force: true }).catch(() => undefined);
3280
+ }
3281
+ });
3282
+ function makeSpec(remoteCwd) {
3283
+ return {
3284
+ transport: "sandbox",
3285
+ provider: "test",
3286
+ sandboxId: "sandbox-1",
3287
+ remoteCwd,
3288
+ timeoutMs: 30_000,
3289
+ apiKey: null,
3290
+ };
3291
+ }
3292
+ // Create a temp root with one workspace directory and any named asset
3293
+ // directories. Each directory carries one file, so the host tar step has
3294
+ // bytes and the merge has content.
3295
+ async function makeOutboundDirs(names) {
3296
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-outbound-coordinator-"));
3297
+ cleanupDirs.push(rootDir);
3298
+ const workspaceDir = path.join(rootDir, "workspace");
3299
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
3300
+ await mkdir(workspaceDir, { recursive: true });
3301
+ await writeFile(path.join(workspaceDir, "file.txt"), "workspace\n", "utf8");
3302
+ const dirs = new Map();
3303
+ for (const name of names) {
3304
+ const dir = path.join(rootDir, name);
3305
+ await mkdir(dir, { recursive: true });
3306
+ await writeFile(path.join(dir, "file.txt"), `${name}\n`, "utf8");
3307
+ dirs.set(name, dir);
3308
+ }
3309
+ return { rootDir, workspaceDir, remoteWorkspaceDir, dirOf: (name) => dirs.get(name) };
3310
+ }
3311
+ // Resolve true when the label started within the window, false on timeout.
3312
+ async function startedWithin(control, label, ms) {
3313
+ return Promise.race([
3314
+ control.waitForStart(label).then(() => true),
3315
+ settleTick(ms).then(() => false),
3316
+ ]);
3317
+ }
3318
+ it("with concurrency permitted, an asset restore starts while the workspace restore is held open", async () => {
3319
+ const { workspaceDir, remoteWorkspaceDir, dirOf } = await makeOutboundDirs(["home"]);
3320
+ const { control, gate } = makeOutboundControl();
3321
+ const client = makeGatedOutboundClient(true, gate);
3322
+ control.hold("workspace");
3323
+ const prepared = await prepareSandboxManagedRuntime({
3324
+ spec: makeSpec(remoteWorkspaceDir),
3325
+ adapterKey: "codex",
3326
+ client,
3327
+ workspaceLocalDir: workspaceDir,
3328
+ assets: [makeControlledAsset("home", dirOf("home"), control, gate)],
3329
+ });
3330
+ const restore = prepared.restoreWorkspace();
3331
+ restore.catch(() => undefined);
3332
+ // The workspace restore is open and held. The home asset restore still
3333
+ // starts, so the coordinator runs the two tasks concurrently.
3334
+ await control.waitForStart("workspace");
3335
+ expect(await startedWithin(control, "home", 4000)).toBe(true);
3336
+ expect(control.settled).not.toContain("workspace");
3337
+ control.release("workspace");
3338
+ await restore;
3339
+ expect(control.settled).toContain("home");
3340
+ expect(control.settled).toContain("workspace");
3341
+ });
3342
+ it("holds one restore open after another restore fails, and returns only after both settle", async () => {
3343
+ const { workspaceDir, remoteWorkspaceDir, dirOf } = await makeOutboundDirs(["asset-a", "asset-b"]);
3344
+ const { control, gate } = makeOutboundControl();
3345
+ const client = makeGatedOutboundClient(true, gate);
3346
+ const prepared = await prepareSandboxManagedRuntime({
3347
+ spec: makeSpec(remoteWorkspaceDir),
3348
+ adapterKey: "codex",
3349
+ client,
3350
+ syncWorkspace: false,
3351
+ workspaceLocalDir: workspaceDir,
3352
+ assets: [
3353
+ makeControlledAsset("asset-a", dirOf("asset-a"), control, gate),
3354
+ makeControlledAsset("asset-b", dirOf("asset-b"), control, gate),
3355
+ ],
3356
+ });
3357
+ control.hold("asset-b");
3358
+ control.failWith("asset-a", new Error("asset-a-fail"));
3359
+ let coordinatorSettled = false;
3360
+ const done = prepared.restoreWorkspace().then(() => { coordinatorSettled = true; }, () => { coordinatorSettled = true; });
3361
+ await control.waitForStart("asset-b");
3362
+ await settleTick(100);
3363
+ // The first restore already failed. The second restore is still open, so
3364
+ // the coordinator must not return yet.
3365
+ expect(coordinatorSettled).toBe(false);
3366
+ expect(control.settled).toContain("asset-a");
3367
+ expect(control.settled).not.toContain("asset-b");
3368
+ control.release("asset-b");
3369
+ await done;
3370
+ expect(coordinatorSettled).toBe(true);
3371
+ expect(control.settled).toEqual(expect.arrayContaining(["asset-a", "asset-b"]));
3372
+ });
3373
+ it("with concurrency forbidden, keeps the serial restore schedule in the current order", async () => {
3374
+ const { workspaceDir, remoteWorkspaceDir, dirOf } = await makeOutboundDirs(["asset-a"]);
3375
+ const { control, gate } = makeOutboundControl();
3376
+ const client = makeGatedOutboundClient(false, gate);
3377
+ control.hold("workspace");
3378
+ const prepared = await prepareSandboxManagedRuntime({
3379
+ spec: makeSpec(remoteWorkspaceDir),
3380
+ adapterKey: "codex",
3381
+ client,
3382
+ workspaceLocalDir: workspaceDir,
3383
+ assets: [makeControlledAsset("asset-a", dirOf("asset-a"), control, gate)],
3384
+ });
3385
+ const restore = prepared.restoreWorkspace();
3386
+ restore.catch(() => undefined);
3387
+ // The workspace restore runs first and is held open. Serial mode runs one
3388
+ // task at a time, so the asset restore does not start until the workspace
3389
+ // restore settles.
3390
+ await control.waitForStart("workspace");
3391
+ expect(await startedWithin(control, "asset-a", 300)).toBe(false);
3392
+ expect(control.started).toEqual(["workspace"]);
3393
+ control.release("workspace");
3394
+ await control.waitForStart("asset-a");
3395
+ await restore;
3396
+ // The serial order stays the workspace restore first, then the asset.
3397
+ expect(control.started).toEqual(["workspace", "asset-a"]);
3398
+ });
3399
+ it("with concurrency permitted, two concurrent restore tasks use separate temporary state", async () => {
3400
+ const { workspaceDir, remoteWorkspaceDir, dirOf } = await makeOutboundDirs(["asset-a", "asset-b"]);
3401
+ const { control, gate } = makeOutboundControl();
3402
+ const client = makeGatedOutboundClient(true, gate);
3403
+ const prepared = await prepareSandboxManagedRuntime({
3404
+ spec: makeSpec(remoteWorkspaceDir),
3405
+ adapterKey: "codex",
3406
+ client,
3407
+ syncWorkspace: false,
3408
+ workspaceLocalDir: workspaceDir,
3409
+ assets: [
3410
+ makeControlledAsset("asset-a", dirOf("asset-a"), control, gate),
3411
+ makeControlledAsset("asset-b", dirOf("asset-b"), control, gate),
3412
+ ],
3413
+ });
3414
+ control.hold("asset-a");
3415
+ control.hold("asset-b");
3416
+ const restore = prepared.restoreWorkspace();
3417
+ // Both restore tasks start together under the bound. Each task carries its
3418
+ // own restore temp directory, so the two directories differ.
3419
+ await control.waitForStart("asset-a");
3420
+ await control.waitForStart("asset-b");
3421
+ const tempA = control.restoreTempDirs.get("asset-a");
3422
+ const tempB = control.restoreTempDirs.get("asset-b");
3423
+ expect(tempA).toBeTruthy();
3424
+ expect(tempB).toBeTruthy();
3425
+ expect(tempA).not.toBe(tempB);
3426
+ expect(tempA).toContain("paperclip-sandbox-restore-");
3427
+ expect(tempB).toContain("paperclip-sandbox-restore-");
3428
+ control.release("asset-a");
3429
+ control.release("asset-b");
3430
+ await restore;
3431
+ expect(control.settled).toEqual(expect.arrayContaining(["asset-a", "asset-b"]));
3432
+ });
3433
+ it("opens one named span per outbound restore task: restore.workspace, restore.asset.<key>", async () => {
3434
+ const { workspaceDir, remoteWorkspaceDir, dirOf } = await makeOutboundDirs(["home"]);
3435
+ const { control, gate } = makeOutboundControl();
3436
+ const client = makeGatedOutboundClient(true, gate);
3437
+ const { runtimeSpan, opened } = makeSpanRecorder();
3438
+ const prepared = await prepareSandboxManagedRuntime({
3439
+ spec: makeSpec(remoteWorkspaceDir),
3440
+ adapterKey: "codex",
3441
+ client,
3442
+ workspaceLocalDir: workspaceDir,
3443
+ assets: [makeControlledAsset("home", dirOf("home"), control, gate)],
3444
+ runtimeSpan,
3445
+ });
3446
+ await prepared.restoreWorkspace();
3447
+ // Each outbound restore task carries its own named span. The workspace
3448
+ // restore task and the home asset restore task each open one.
3449
+ expect(opened).toContain("restore.workspace");
3450
+ expect(opened).toContain("restore.asset.home");
3451
+ });
3452
+ it("with concurrency permitted, the workspace and asset restore task spans overlap in time", async () => {
3453
+ const { workspaceDir, remoteWorkspaceDir, dirOf } = await makeOutboundDirs(["home"]);
3454
+ const { control, gate } = makeOutboundControl();
3455
+ const client = makeGatedOutboundClient(true, gate);
3456
+ const { runtimeSpan, openNow } = makeSpanRecorder();
3457
+ const prepared = await prepareSandboxManagedRuntime({
3458
+ spec: makeSpec(remoteWorkspaceDir),
3459
+ adapterKey: "codex",
3460
+ client,
3461
+ workspaceLocalDir: workspaceDir,
3462
+ assets: [makeControlledAsset("home", dirOf("home"), control, gate)],
3463
+ runtimeSpan,
3464
+ });
3465
+ control.hold("workspace");
3466
+ control.hold("home");
3467
+ const restore = prepared.restoreWorkspace();
3468
+ restore.catch(() => undefined);
3469
+ // Both restore tasks are held open. Each restore task span opens before its
3470
+ // work and stays open while the work is held, so the two spans are open at
3471
+ // the same time.
3472
+ await control.waitForStart("workspace");
3473
+ await control.waitForStart("home");
3474
+ expect(openNow.has("restore.workspace")).toBe(true);
3475
+ expect(openNow.has("restore.asset.home")).toBe(true);
3476
+ control.release("workspace");
3477
+ control.release("home");
3478
+ await restore;
3479
+ });
3480
+ });
3481
+ // The bundle export inside the workspace restore task selects its outbound
3482
+ // transport by the client. A client with native `syncOut` copies the bundle
3483
+ // straight into the host restore temp directory through one `kind: "file"`
3484
+ // mapping. A client without native `syncOut` reads the bundle back through
3485
+ // `readFile`. These tests build a git-backed workspace and assert the transport
3486
+ // branch, the confinement guard, and the full-bundle retry.
3487
+ describe("sandbox git-bundle export transport", () => {
3488
+ const cleanupDirs = [];
3489
+ afterEach(async () => {
3490
+ while (cleanupDirs.length > 0) {
3491
+ const dir = cleanupDirs.pop();
3492
+ if (!dir)
3493
+ continue;
3494
+ await rm(dir, { recursive: true, force: true }).catch(() => undefined);
3495
+ }
3496
+ });
3497
+ // Copy a directory tree and drop each entry whose name matches an exclude
3498
+ // term. This models a native provider `syncOut` for a directory mapping: it
3499
+ // honors `exclude`, so `.git`, `node_modules`, and the runtime root never
3500
+ // reach the host restore temp directory. The tar fallback drops the same set.
3501
+ async function copyDirectoryWithExclude(sourceDir, targetDir, exclude) {
3502
+ const excludeNames = new Set((exclude ?? []).map((entry) => entry.replace(/\/$/, "")));
3503
+ await mkdir(targetDir, { recursive: true });
3504
+ const entries = await readdir(sourceDir, { withFileTypes: true });
3505
+ for (const entry of entries) {
3506
+ if (excludeNames.has(entry.name))
3507
+ continue;
3508
+ const source = path.join(sourceDir, entry.name);
3509
+ const target = path.join(targetDir, entry.name);
3510
+ if (entry.isDirectory()) {
3511
+ await copyDirectoryWithExclude(source, target, exclude);
3512
+ }
3513
+ else if (entry.isSymbolicLink()) {
3514
+ await symlink(await fsPromises.readlink(source), target);
3515
+ }
3516
+ else {
3517
+ await writeFile(target, await readFile(source));
3518
+ }
3519
+ }
3520
+ }
3521
+ // Build a git-backed managed-runtime client. `native` toggles the outbound
3522
+ // `syncOut`. The client records every `syncOut` operation and every `readFile`
3523
+ // remote path, so a test can prove which transport moved the bundle.
3524
+ function makeTransportClient(native, capture) {
3525
+ const client = {
3526
+ makeDir: async (remotePath) => {
3527
+ await mkdir(remotePath, { recursive: true });
3528
+ },
3529
+ writeFile: async (remotePath, bytes) => {
3530
+ await mkdir(path.dirname(remotePath), { recursive: true });
3531
+ await writeFile(remotePath, Buffer.from(bytes));
3532
+ },
3533
+ readFile: async (remotePath) => {
3534
+ capture.readFilePaths.push(remotePath);
3535
+ return await readFile(remotePath);
3536
+ },
3537
+ listFiles: async () => [],
3538
+ remove: async (remotePath) => {
3539
+ await rm(remotePath, { recursive: true, force: true });
3540
+ },
3541
+ run: async (command) => {
3542
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
3543
+ },
3544
+ };
3545
+ attachFallbackSyncIn(client);
3546
+ if (native) {
3547
+ client.syncOut = async (operations) => {
3548
+ const resultOperations = [];
3549
+ for (const operation of operations) {
3550
+ capture.syncOutOperations.push(operation);
3551
+ // Report a real `bytesTransferred`, from a post-copy byte count, so a
3552
+ // test can assert on the emitted progress line — the same way a real
3553
+ // provider reports the bytes it actually moved.
3554
+ let bytesTransferred = 0;
3555
+ for (const mapping of operation.files) {
3556
+ if (mapping.kind === "directory") {
3557
+ await copyDirectoryWithExclude(mapping.sourcePath, mapping.targetPath, mapping.exclude);
3558
+ bytesTransferred += await directoryByteSize(mapping.targetPath);
3559
+ }
3560
+ else {
3561
+ await mkdir(path.dirname(mapping.targetPath), { recursive: true });
3562
+ const bytes = await readFile(mapping.sourcePath);
3563
+ await writeFile(mapping.targetPath, bytes);
3564
+ bytesTransferred += bytes.byteLength;
3565
+ }
3566
+ }
3567
+ resultOperations.push({
3568
+ operationId: operation.operationId,
3569
+ filesTransferred: operation.files.length,
3570
+ bytesTransferred,
3571
+ });
3572
+ }
3573
+ return { operations: resultOperations };
3574
+ };
3575
+ }
3576
+ return client;
3577
+ }
3578
+ const gitSpec = (remoteWorkspaceDir) => ({
3579
+ transport: "sandbox",
3580
+ provider: "test",
3581
+ sandboxId: "sandbox-1",
3582
+ remoteCwd: remoteWorkspaceDir,
3583
+ timeoutMs: 30_000,
3584
+ apiKey: null,
3585
+ });
3586
+ // Create a git repository with one commit and a linked worktree. Return the
3587
+ // host worktree directory and the sandbox workspace directory the prepare step
3588
+ // fills through a shallow standalone clone.
3589
+ async function setupGitBackedWorkspace(prefix) {
3590
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), prefix));
3591
+ cleanupDirs.push(rootDir);
3592
+ const sourceRepoDir = path.join(rootDir, "source-repo");
3593
+ const localWorkspaceDir = path.join(rootDir, "local-worktree");
3594
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
3595
+ await mkdir(sourceRepoDir, { recursive: true });
3596
+ await git(sourceRepoDir, ["init"]);
3597
+ await git(sourceRepoDir, ["checkout", "-b", "main"]);
3598
+ await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
3599
+ await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
3600
+ await writeFile(path.join(sourceRepoDir, ".gitignore"), "node_modules/\n", "utf8");
3601
+ await writeFile(path.join(sourceRepoDir, "tracked.txt"), "base\n", "utf8");
3602
+ await git(sourceRepoDir, ["add", "-A"]);
3603
+ await git(sourceRepoDir, ["commit", "-m", "base"]);
3604
+ await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
3605
+ return { localWorkspaceDir, remoteWorkspaceDir };
3606
+ }
3607
+ // Advance the sandbox history by one commit that also adds a new file. The
3608
+ // caller runs the restore after this, so the export moves this new history.
3609
+ async function commitInSandbox(remoteWorkspaceDir) {
3610
+ await git(remoteWorkspaceDir, ["config", "user.name", "Paperclip Sandbox"]);
3611
+ await git(remoteWorkspaceDir, ["config", "user.email", "sandbox@paperclip.dev"]);
3612
+ await writeFile(path.join(remoteWorkspaceDir, "remote-only.txt"), "from sandbox\n", "utf8");
3613
+ await git(remoteWorkspaceDir, ["add", "-A"]);
3614
+ await git(remoteWorkspaceDir, ["commit", "-m", "sandbox update"]);
3615
+ }
3616
+ function bundleFileMappings(capture) {
3617
+ return capture.syncOutOperations
3618
+ .flatMap((operation) => operation.files)
3619
+ .filter((mapping) => path.posix.basename(mapping.sourcePath) === "git-delta.bundle");
3620
+ }
3621
+ it("moves the bundle through one native syncOut file mapping, never through readFile", async () => {
3622
+ const capture = { syncOutOperations: [], readFilePaths: [] };
3623
+ const { localWorkspaceDir, remoteWorkspaceDir } = await setupGitBackedWorkspace("paperclip-bundle-native-");
3624
+ const client = makeTransportClient(true, capture);
3625
+ const prepared = await prepareSandboxManagedRuntime({
3626
+ spec: gitSpec(remoteWorkspaceDir),
3627
+ adapterKey: "test-adapter",
3628
+ client,
3629
+ workspaceLocalDir: localWorkspaceDir,
3630
+ });
3631
+ await commitInSandbox(remoteWorkspaceDir);
3632
+ await prepared.restoreWorkspace();
3633
+ // The restore imported the sandbox commit and its new file.
3634
+ expect(await git(localWorkspaceDir, ["log", "-1", "--pretty=%s"])).toBe("sandbox update");
3635
+ await expect(readFile(path.join(localWorkspaceDir, "remote-only.txt"), "utf8")).resolves.toBe("from sandbox\n");
3636
+ // The bundle rode exactly one native `kind: "file"` mapping into the host
3637
+ // restore temp directory; `readFile` never touched the bundle.
3638
+ const mappings = bundleFileMappings(capture);
3639
+ expect(mappings).toHaveLength(1);
3640
+ expect(mappings[0].kind).toBe("file");
3641
+ expect(path.posix.basename(mappings[0].targetPath)).toBe("git-delta.bundle");
3642
+ expect(capture.readFilePaths.some((remotePath) => remotePath.endsWith("git-delta.bundle"))).toBe(false);
3643
+ // The small status file stays on `readFile`; the change does not migrate it.
3644
+ expect(capture.readFilePaths.some((remotePath) => remotePath.endsWith("workspace-status.txt"))).toBe(true);
3645
+ });
3646
+ it("reads the bundle through readFile when the client has no native syncOut", async () => {
3647
+ const capture = { syncOutOperations: [], readFilePaths: [] };
3648
+ const { localWorkspaceDir, remoteWorkspaceDir } = await setupGitBackedWorkspace("paperclip-bundle-fallback-");
3649
+ const client = makeTransportClient(false, capture);
3650
+ const prepared = await prepareSandboxManagedRuntime({
3651
+ spec: gitSpec(remoteWorkspaceDir),
3652
+ adapterKey: "test-adapter",
3653
+ client,
3654
+ workspaceLocalDir: localWorkspaceDir,
3655
+ });
3656
+ await commitInSandbox(remoteWorkspaceDir);
3657
+ await prepared.restoreWorkspace();
3658
+ expect(await git(localWorkspaceDir, ["log", "-1", "--pretty=%s"])).toBe("sandbox update");
3659
+ await expect(readFile(path.join(localWorkspaceDir, "remote-only.txt"), "utf8")).resolves.toBe("from sandbox\n");
3660
+ // Without native `syncOut` the fallback path is unchanged: no sync operation
3661
+ // ran and the bundle came back through `readFile`.
3662
+ expect(client.syncOut).toBeUndefined();
3663
+ expect(capture.syncOutOperations).toHaveLength(0);
3664
+ expect(capture.readFilePaths.some((remotePath) => remotePath.endsWith("git-delta.bundle"))).toBe(true);
3665
+ });
3666
+ it("retries the full bundle through the native branch when the delta misses its prerequisite", async () => {
3667
+ const capture = { syncOutOperations: [], readFilePaths: [] };
3668
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bundle-retry-"));
3669
+ cleanupDirs.push(rootDir);
3670
+ // A standalone host repository, so the test controls object reachability. A
3671
+ // linked worktree shares the source object store, and the boundary commit
3672
+ // stays reachable there; a standalone repository lets `gc` prune it.
3673
+ const localWorkspaceDir = path.join(rootDir, "local-repo");
3674
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
3675
+ await mkdir(localWorkspaceDir, { recursive: true });
3676
+ await git(localWorkspaceDir, ["init"]);
3677
+ await git(localWorkspaceDir, ["checkout", "-b", "work"]);
3678
+ await git(localWorkspaceDir, ["config", "user.name", "Paperclip Test"]);
3679
+ await git(localWorkspaceDir, ["config", "user.email", "test@paperclip.dev"]);
3680
+ await writeFile(path.join(localWorkspaceDir, "tracked.txt"), "base\n", "utf8");
3681
+ await git(localWorkspaceDir, ["add", "-A"]);
3682
+ await git(localWorkspaceDir, ["commit", "-m", "base"]);
3683
+ const firstCommit = await git(localWorkspaceDir, ["rev-parse", "HEAD"]);
3684
+ await writeFile(path.join(localWorkspaceDir, "tracked.txt"), "second\n", "utf8");
3685
+ await git(localWorkspaceDir, ["add", "-A"]);
3686
+ await git(localWorkspaceDir, ["commit", "-m", "second"]);
3687
+ const stagedBase = await git(localWorkspaceDir, ["rev-parse", "HEAD"]);
3688
+ const client = makeTransportClient(true, capture);
3689
+ const prepared = await prepareSandboxManagedRuntime({
3690
+ spec: gitSpec(remoteWorkspaceDir),
3691
+ adapterKey: "test-adapter",
3692
+ client,
3693
+ workspaceLocalDir: localWorkspaceDir,
3694
+ });
3695
+ await commitInSandbox(remoteWorkspaceDir);
3696
+ const sandboxHead = await git(remoteWorkspaceDir, ["rev-parse", "HEAD"]);
3697
+ // The host drops below the staged base commit and prunes it. The delta
3698
+ // bundle names that boundary commit as a prerequisite the host no longer
3699
+ // holds, so the import fails and forces the full-bundle retry.
3700
+ await git(localWorkspaceDir, ["reset", "--hard", firstCommit]);
3701
+ await git(localWorkspaceDir, ["reflog", "expire", "--expire=now", "--all"]);
3702
+ await git(localWorkspaceDir, ["gc", "--prune=now"]);
3703
+ await expect(git(localWorkspaceDir, ["cat-file", "-e", `${stagedBase}^{commit}`])).rejects.toThrow();
3704
+ await prepared.restoreWorkspace();
3705
+ // Two bundle exports rode native file mappings: the delta attempt and the
3706
+ // full-bundle retry. `readFile` never moved the bundle on either attempt.
3707
+ const mappings = bundleFileMappings(capture);
3708
+ expect(mappings).toHaveLength(2);
3709
+ expect(mappings.every((mapping) => mapping.kind === "file")).toBe(true);
3710
+ expect(capture.readFilePaths.some((remotePath) => remotePath.endsWith("git-delta.bundle"))).toBe(false);
3711
+ // The full bundle was self-contained: the host repository now holds the
3712
+ // sandbox head commit.
3713
+ await expect(git(localWorkspaceDir, ["cat-file", "-e", `${sandboxHead}^{commit}`])).resolves.toBe("");
3714
+ });
3715
+ it("reports the real transferred bytes for the native git-history export and workspace restore", async () => {
3716
+ const capture = { syncOutOperations: [], readFilePaths: [] };
3717
+ const { localWorkspaceDir, remoteWorkspaceDir } = await setupGitBackedWorkspace("paperclip-restore-bytes-");
3718
+ const client = makeTransportClient(true, capture);
3719
+ const prepared = await prepareSandboxManagedRuntime({
3720
+ spec: gitSpec(remoteWorkspaceDir),
3721
+ adapterKey: "test-adapter",
3722
+ client,
3723
+ workspaceLocalDir: localWorkspaceDir,
3724
+ });
3725
+ // Add a sizeable, incompressible tracked file in the sandbox, so both the
3726
+ // packed git-history bundle and the restored working tree carry real,
3727
+ // non-trivial bytes well above the 0.1 MB rounding step. Random bytes,
3728
+ // not a repeated byte, so git's own pack compression cannot shrink the
3729
+ // bundle back down to a trivial size.
3730
+ await writeFile(path.join(remoteWorkspaceDir, "large.bin"), randomBytes(300 * 1024));
3731
+ await commitInSandbox(remoteWorkspaceDir);
3732
+ const lines = [];
3733
+ await prepared.restoreWorkspace((line) => { lines.push(line); });
3734
+ const exportLines = lines.filter((line) => line.includes("Exporting git history from environment"));
3735
+ const restoreLines = lines.filter((line) => line.includes("Restoring workspace from environment"));
3736
+ // Both terminal lines carry the real transferred byte total, not the
3737
+ // "0.0 MB" a discarded native `syncOut` result would leave behind.
3738
+ expect(exportLines.length).toBeGreaterThan(0);
3739
+ expect(exportLines.some((line) => /\(\d+\.\d\/\d+\.\d MB\)/.test(line))).toBe(true);
3740
+ expect(exportLines.some((line) => line.includes("(0.0/0.0 MB)"))).toBe(false);
3741
+ expect(restoreLines.length).toBeGreaterThan(0);
3742
+ expect(restoreLines.some((line) => /\(\d+\.\d\/\d+\.\d MB\)/.test(line))).toBe(true);
3743
+ expect(restoreLines.some((line) => line.includes("(0.0/0.0 MB)"))).toBe(false);
3744
+ });
3745
+ });
3746
+ //# sourceMappingURL=sandbox-managed-runtime.test.js.map