agent-hitch 0.2.8 → 0.2.10

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 (414) hide show
  1. package/README.md +206 -137
  2. package/README.zh-CN.md +193 -119
  3. package/dist/scripts/canary-native-training.js +221 -0
  4. package/dist/scripts/canary-native-training.js.map +1 -0
  5. package/dist/scripts/canary-remote-training.js +246 -0
  6. package/dist/scripts/canary-remote-training.js.map +1 -0
  7. package/dist/scripts/canary-remote-verifier.js +124 -0
  8. package/dist/scripts/canary-remote-verifier.js.map +1 -0
  9. package/dist/scripts/canary-worker-host.js +113 -0
  10. package/dist/scripts/canary-worker-host.js.map +1 -0
  11. package/dist/scripts/canary-worker-peer.js +188 -0
  12. package/dist/scripts/canary-worker-peer.js.map +1 -0
  13. package/dist/scripts/check-architecture.js +4 -3
  14. package/dist/scripts/check-architecture.js.map +1 -1
  15. package/dist/src/adapters/catalog.js +2 -1
  16. package/dist/src/adapters/catalog.js.map +1 -1
  17. package/dist/src/adapters/providers/codex.js +65 -2
  18. package/dist/src/adapters/providers/codex.js.map +1 -1
  19. package/dist/src/adapters/providers/model-call.js +17 -1
  20. package/dist/src/adapters/providers/model-call.js.map +1 -1
  21. package/dist/src/adapters/providers/training-tool.js +29 -0
  22. package/dist/src/adapters/providers/training-tool.js.map +1 -0
  23. package/dist/src/artifacts/index.js +1 -1
  24. package/dist/src/artifacts/index.js.map +1 -1
  25. package/dist/src/artifacts/store.js +15 -10
  26. package/dist/src/artifacts/store.js.map +1 -1
  27. package/dist/src/backends/harbor/backend.js +24 -15
  28. package/dist/src/backends/harbor/backend.js.map +1 -1
  29. package/dist/src/backends/harbor/bridge-environment.js +12 -0
  30. package/dist/src/backends/harbor/bridge-environment.js.map +1 -0
  31. package/dist/src/backends/harbor/environment-config.js +2 -2
  32. package/dist/src/backends/harbor/environment-config.js.map +1 -1
  33. package/dist/src/backends/harbor/host-credential-helper.js +81 -0
  34. package/dist/src/backends/harbor/host-credential-helper.js.map +1 -0
  35. package/dist/src/backends/harbor/index.js +3 -0
  36. package/dist/src/backends/harbor/index.js.map +1 -1
  37. package/dist/src/backends/harbor/model-proxy-config.js +18 -1
  38. package/dist/src/backends/harbor/model-proxy-config.js.map +1 -1
  39. package/dist/src/backends/harbor/observation.js +61 -0
  40. package/dist/src/backends/harbor/observation.js.map +1 -0
  41. package/dist/src/backends/harbor/portable-regrade.js +150 -0
  42. package/dist/src/backends/harbor/portable-regrade.js.map +1 -0
  43. package/dist/src/backends/harbor/process.js +26 -5
  44. package/dist/src/backends/harbor/process.js.map +1 -1
  45. package/dist/src/backends/harbor/regrade.js +4 -1
  46. package/dist/src/backends/harbor/regrade.js.map +1 -1
  47. package/dist/src/backends/harbor/supervisor.js +22 -0
  48. package/dist/src/backends/harbor/supervisor.js.map +1 -1
  49. package/dist/src/backends/harbor/tools.js +2 -0
  50. package/dist/src/backends/harbor/tools.js.map +1 -1
  51. package/dist/src/backends/index.js +3 -0
  52. package/dist/src/backends/index.js.map +1 -1
  53. package/dist/src/cli/arguments.js +33 -0
  54. package/dist/src/cli/arguments.js.map +1 -1
  55. package/dist/src/cli/commands/capabilities.js +14 -1
  56. package/dist/src/cli/commands/capabilities.js.map +1 -1
  57. package/dist/src/cli/commands/daemon.js +19 -0
  58. package/dist/src/cli/commands/daemon.js.map +1 -1
  59. package/dist/src/cli/commands/eval.js +38 -10
  60. package/dist/src/cli/commands/eval.js.map +1 -1
  61. package/dist/src/cli/commands/local.js +130 -0
  62. package/dist/src/cli/commands/local.js.map +1 -0
  63. package/dist/src/cli/commands/model-node.js +26 -0
  64. package/dist/src/cli/commands/model-node.js.map +1 -0
  65. package/dist/src/cli/commands/models.js +72 -0
  66. package/dist/src/cli/commands/models.js.map +1 -0
  67. package/dist/src/cli/commands/run.js +36 -1
  68. package/dist/src/cli/commands/run.js.map +1 -1
  69. package/dist/src/cli/commands/training.js +32 -0
  70. package/dist/src/cli/commands/training.js.map +1 -0
  71. package/dist/src/cli/commands/worker.js +30 -2
  72. package/dist/src/cli/commands/worker.js.map +1 -1
  73. package/dist/src/cli/main.js +8 -0
  74. package/dist/src/cli/main.js.map +1 -1
  75. package/dist/src/cli/output.js +24 -2
  76. package/dist/src/cli/output.js.map +1 -1
  77. package/dist/src/control-plane/eval-records.js +2 -0
  78. package/dist/src/control-plane/eval-records.js.map +1 -1
  79. package/dist/src/control-plane/eval-recovery.js +7 -1
  80. package/dist/src/control-plane/eval-recovery.js.map +1 -1
  81. package/dist/src/control-plane/eval-scheduler.js +24 -50
  82. package/dist/src/control-plane/eval-scheduler.js.map +1 -1
  83. package/dist/src/control-plane/eval-submission-persist.js +30 -0
  84. package/dist/src/control-plane/eval-submission-persist.js.map +1 -0
  85. package/dist/src/control-plane/execution-observation-contract.js +60 -0
  86. package/dist/src/control-plane/execution-observation-contract.js.map +1 -0
  87. package/dist/src/control-plane/index.js +10 -0
  88. package/dist/src/control-plane/index.js.map +1 -1
  89. package/dist/src/control-plane/inference-manager.js +357 -0
  90. package/dist/src/control-plane/inference-manager.js.map +1 -0
  91. package/dist/src/control-plane/managed-gateway-receipt.js +26 -0
  92. package/dist/src/control-plane/managed-gateway-receipt.js.map +1 -0
  93. package/dist/src/control-plane/managed-service-recovery.js +158 -0
  94. package/dist/src/control-plane/managed-service-recovery.js.map +1 -0
  95. package/dist/src/control-plane/model-capture-planning.js +15 -1
  96. package/dist/src/control-plane/model-capture-planning.js.map +1 -1
  97. package/dist/src/control-plane/ordered-eval-control.js +221 -0
  98. package/dist/src/control-plane/ordered-eval-control.js.map +1 -0
  99. package/dist/src/control-plane/remote-backend-result.js +18 -0
  100. package/dist/src/control-plane/remote-backend-result.js.map +1 -0
  101. package/dist/src/control-plane/remote-execution-lease.js +15 -0
  102. package/dist/src/control-plane/remote-execution-lease.js.map +1 -0
  103. package/dist/src/control-plane/remote-execution-observation.js +108 -0
  104. package/dist/src/control-plane/remote-execution-observation.js.map +1 -0
  105. package/dist/src/control-plane/remote-model-routes.js +107 -0
  106. package/dist/src/control-plane/remote-model-routes.js.map +1 -0
  107. package/dist/src/control-plane/remote-observation-reporter.js +37 -0
  108. package/dist/src/control-plane/remote-observation-reporter.js.map +1 -0
  109. package/dist/src/control-plane/remote-offer-contract.js +48 -0
  110. package/dist/src/control-plane/remote-offer-contract.js.map +1 -0
  111. package/dist/src/control-plane/remote-rerun-journal.js +61 -0
  112. package/dist/src/control-plane/remote-rerun-journal.js.map +1 -0
  113. package/dist/src/control-plane/remote-result-transport.js +55 -9
  114. package/dist/src/control-plane/remote-result-transport.js.map +1 -1
  115. package/dist/src/control-plane/remote-verifier-import.js +52 -0
  116. package/dist/src/control-plane/remote-verifier-import.js.map +1 -0
  117. package/dist/src/control-plane/remote-verifier-result.js +61 -0
  118. package/dist/src/control-plane/remote-verifier-result.js.map +1 -0
  119. package/dist/src/control-plane/remote-verifier-source-transport.js +68 -0
  120. package/dist/src/control-plane/remote-verifier-source-transport.js.map +1 -0
  121. package/dist/src/control-plane/remote-work-coordinator.js +103 -47
  122. package/dist/src/control-plane/remote-work-coordinator.js.map +1 -1
  123. package/dist/src/control-plane/remote-work-inputs.js +26 -5
  124. package/dist/src/control-plane/remote-work-inputs.js.map +1 -1
  125. package/dist/src/control-plane/remote-work-recovery.js +123 -34
  126. package/dist/src/control-plane/remote-work-recovery.js.map +1 -1
  127. package/dist/src/control-plane/remote-worker-artifacts.js +10 -7
  128. package/dist/src/control-plane/remote-worker-artifacts.js.map +1 -1
  129. package/dist/src/control-plane/remote-worker-cleanup.js +104 -0
  130. package/dist/src/control-plane/remote-worker-cleanup.js.map +1 -0
  131. package/dist/src/control-plane/remote-worker-client.js +156 -14
  132. package/dist/src/control-plane/remote-worker-client.js.map +1 -1
  133. package/dist/src/control-plane/remote-worker-codecs.js +160 -0
  134. package/dist/src/control-plane/remote-worker-codecs.js.map +1 -0
  135. package/dist/src/control-plane/remote-worker-generation-recovery.js +67 -0
  136. package/dist/src/control-plane/remote-worker-generation-recovery.js.map +1 -0
  137. package/dist/src/control-plane/remote-worker-ownership.js +51 -0
  138. package/dist/src/control-plane/remote-worker-ownership.js.map +1 -0
  139. package/dist/src/control-plane/remote-worker-protocol.js +85 -119
  140. package/dist/src/control-plane/remote-worker-protocol.js.map +1 -1
  141. package/dist/src/control-plane/remote-worker-runner.js +95 -18
  142. package/dist/src/control-plane/remote-worker-runner.js.map +1 -1
  143. package/dist/src/control-plane/remote-workers.js +33 -5
  144. package/dist/src/control-plane/remote-workers.js.map +1 -1
  145. package/dist/src/control-plane/rerun-recovery.js +89 -0
  146. package/dist/src/control-plane/rerun-recovery.js.map +1 -0
  147. package/dist/src/control-plane/rerun-scheduler.js +70 -96
  148. package/dist/src/control-plane/rerun-scheduler.js.map +1 -1
  149. package/dist/src/control-plane/rerun-source.js +57 -0
  150. package/dist/src/control-plane/rerun-source.js.map +1 -0
  151. package/dist/src/control-plane/worker-model-relay.js +76 -0
  152. package/dist/src/control-plane/worker-model-relay.js.map +1 -0
  153. package/dist/src/controller-runtime/hash.js +3 -0
  154. package/dist/src/controller-runtime/hash.js.map +1 -1
  155. package/dist/src/controller-runtime/index.js +2 -0
  156. package/dist/src/controller-runtime/index.js.map +1 -1
  157. package/dist/src/controller-runtime/observation.js +29 -0
  158. package/dist/src/controller-runtime/observation.js.map +1 -0
  159. package/dist/src/controller-runtime/resident.js +22 -0
  160. package/dist/src/controller-runtime/resident.js.map +1 -0
  161. package/dist/src/daemon/auth.js +7 -2
  162. package/dist/src/daemon/auth.js.map +1 -1
  163. package/dist/src/daemon/client.js +34 -1
  164. package/dist/src/daemon/client.js.map +1 -1
  165. package/dist/src/daemon/execution-observation.js +38 -0
  166. package/dist/src/daemon/execution-observation.js.map +1 -0
  167. package/dist/src/daemon/http.js +57 -0
  168. package/dist/src/daemon/http.js.map +1 -0
  169. package/dist/src/daemon/inference-prepare.js +47 -0
  170. package/dist/src/daemon/inference-prepare.js.map +1 -0
  171. package/dist/src/daemon/scheduler.js +4 -1
  172. package/dist/src/daemon/scheduler.js.map +1 -1
  173. package/dist/src/daemon/server.js +80 -82
  174. package/dist/src/daemon/server.js.map +1 -1
  175. package/dist/src/daemon/worker-observation.js +34 -0
  176. package/dist/src/daemon/worker-observation.js.map +1 -0
  177. package/dist/src/daemon/worker-ownership.js +71 -0
  178. package/dist/src/daemon/worker-ownership.js.map +1 -0
  179. package/dist/src/daemon/worker-routes.js +83 -15
  180. package/dist/src/daemon/worker-routes.js.map +1 -1
  181. package/dist/src/domain/execution-observation.js +2 -0
  182. package/dist/src/domain/execution-observation.js.map +1 -0
  183. package/dist/src/domain/index.js +1 -0
  184. package/dist/src/domain/index.js.map +1 -1
  185. package/dist/src/domain/inference.js +2 -0
  186. package/dist/src/domain/inference.js.map +1 -0
  187. package/dist/src/domain/runs.js.map +1 -1
  188. package/dist/src/domain/training.js +2 -0
  189. package/dist/src/domain/training.js.map +1 -0
  190. package/dist/src/domain/validation.js +11 -0
  191. package/dist/src/domain/validation.js.map +1 -1
  192. package/dist/src/domain/verifier-source.js +2 -0
  193. package/dist/src/domain/verifier-source.js.map +1 -0
  194. package/dist/src/domain/worker-recovery.js +2 -0
  195. package/dist/src/domain/worker-recovery.js.map +1 -0
  196. package/dist/src/evals/eval-logical-plan.js +8 -1
  197. package/dist/src/evals/eval-logical-plan.js.map +1 -1
  198. package/dist/src/evals/execution-leases.js +15 -2
  199. package/dist/src/evals/execution-leases.js.map +1 -1
  200. package/dist/src/evals/execution-plan.js +11 -1
  201. package/dist/src/evals/execution-plan.js.map +1 -1
  202. package/dist/src/evals/harbor-bridge-error.js +9 -0
  203. package/dist/src/evals/harbor-bridge-error.js.map +1 -1
  204. package/dist/src/evals/index.js +19 -0
  205. package/dist/src/evals/index.js.map +1 -1
  206. package/dist/src/evals/model-capture-plan.js +12 -0
  207. package/dist/src/evals/model-capture-plan.js.map +1 -1
  208. package/dist/src/evals/model-capture-runtime.js +25 -0
  209. package/dist/src/evals/model-capture-runtime.js.map +1 -1
  210. package/dist/src/evals/physical-work-plan.js +57 -0
  211. package/dist/src/evals/physical-work-plan.js.map +1 -0
  212. package/dist/src/evals/preparation-rerun.js +6 -0
  213. package/dist/src/evals/preparation-rerun.js.map +1 -1
  214. package/dist/src/evals/recovery.js +8 -1
  215. package/dist/src/evals/recovery.js.map +1 -1
  216. package/dist/src/evals/regrade-evidence.js +4 -4
  217. package/dist/src/evals/regrade-evidence.js.map +1 -1
  218. package/dist/src/evals/remote-execution-ownership.js +50 -0
  219. package/dist/src/evals/remote-execution-ownership.js.map +1 -0
  220. package/dist/src/evals/remote-infrastructure-retry.js +2 -0
  221. package/dist/src/evals/remote-infrastructure-retry.js.map +1 -1
  222. package/dist/src/evals/remote-lease-release.js +58 -0
  223. package/dist/src/evals/remote-lease-release.js.map +1 -0
  224. package/dist/src/evals/remote-rerun-execution.js +85 -0
  225. package/dist/src/evals/remote-rerun-execution.js.map +1 -0
  226. package/dist/src/evals/remote-trial.js +6 -0
  227. package/dist/src/evals/remote-trial.js.map +1 -0
  228. package/dist/src/evals/remote-verifier-assessment.js +85 -0
  229. package/dist/src/evals/remote-verifier-assessment.js.map +1 -0
  230. package/dist/src/evals/remote-verifier-contract.js +43 -0
  231. package/dist/src/evals/remote-verifier-contract.js.map +1 -0
  232. package/dist/src/evals/remote-verifier-execution.js +105 -0
  233. package/dist/src/evals/remote-verifier-execution.js.map +1 -0
  234. package/dist/src/evals/remote-verifier-journal.js +21 -0
  235. package/dist/src/evals/remote-verifier-journal.js.map +1 -0
  236. package/dist/src/evals/remote-verifier-outcome.js +51 -0
  237. package/dist/src/evals/remote-verifier-outcome.js.map +1 -0
  238. package/dist/src/evals/remote-verifier-rerun.js +134 -0
  239. package/dist/src/evals/remote-verifier-rerun.js.map +1 -0
  240. package/dist/src/evals/remote-verifier-selection.js +61 -0
  241. package/dist/src/evals/remote-verifier-selection.js.map +1 -0
  242. package/dist/src/evals/remote-verifier-source.js +33 -0
  243. package/dist/src/evals/remote-verifier-source.js.map +1 -0
  244. package/dist/src/evals/remote-worker-cleanup.js +46 -0
  245. package/dist/src/evals/remote-worker-cleanup.js.map +1 -0
  246. package/dist/src/evals/request.js +58 -1
  247. package/dist/src/evals/request.js.map +1 -1
  248. package/dist/src/evals/rerun-completion.js +39 -0
  249. package/dist/src/evals/rerun-completion.js.map +1 -0
  250. package/dist/src/evals/rerun-inputs.js +54 -0
  251. package/dist/src/evals/rerun-inputs.js.map +1 -1
  252. package/dist/src/evals/rerun-resume.js +70 -0
  253. package/dist/src/evals/rerun-resume.js.map +1 -0
  254. package/dist/src/evals/rerun-state.js +25 -0
  255. package/dist/src/evals/rerun-state.js.map +1 -0
  256. package/dist/src/evals/rerun-types.js.map +1 -1
  257. package/dist/src/evals/rerun.js +94 -101
  258. package/dist/src/evals/rerun.js.map +1 -1
  259. package/dist/src/evals/service.js +59 -5
  260. package/dist/src/evals/service.js.map +1 -1
  261. package/dist/src/evals/verifier-only-rerun.js +2 -16
  262. package/dist/src/evals/verifier-only-rerun.js.map +1 -1
  263. package/dist/src/evals/verifier-rerun-result.js +24 -0
  264. package/dist/src/evals/verifier-rerun-result.js.map +1 -0
  265. package/dist/src/evals/verifier-source-import.js +48 -0
  266. package/dist/src/evals/verifier-source-import.js.map +1 -0
  267. package/dist/src/evals/verifier-source.js +139 -0
  268. package/dist/src/evals/verifier-source.js.map +1 -0
  269. package/dist/src/foundation/base64.js +19 -0
  270. package/dist/src/foundation/base64.js.map +1 -0
  271. package/dist/src/foundation/config.js +10 -0
  272. package/dist/src/foundation/config.js.map +1 -1
  273. package/dist/src/foundation/credential-redaction.js +22 -9
  274. package/dist/src/foundation/credential-redaction.js.map +1 -1
  275. package/dist/src/foundation/index.js +1 -0
  276. package/dist/src/foundation/index.js.map +1 -1
  277. package/dist/src/foundation/process-identity.js +13 -9
  278. package/dist/src/foundation/process-identity.js.map +1 -1
  279. package/dist/src/inference/daemon-capacity.js +41 -0
  280. package/dist/src/inference/daemon-capacity.js.map +1 -0
  281. package/dist/src/inference/device-reservation.js +58 -0
  282. package/dist/src/inference/device-reservation.js.map +1 -0
  283. package/dist/src/inference/doctor.js +95 -0
  284. package/dist/src/inference/doctor.js.map +1 -0
  285. package/dist/src/inference/generation-recovery.js +83 -0
  286. package/dist/src/inference/generation-recovery.js.map +1 -0
  287. package/dist/src/inference/index.js +24 -0
  288. package/dist/src/inference/index.js.map +1 -0
  289. package/dist/src/inference/lock.js +331 -0
  290. package/dist/src/inference/lock.js.map +1 -0
  291. package/dist/src/inference/managed-launcher.js +35 -0
  292. package/dist/src/inference/managed-launcher.js.map +1 -0
  293. package/dist/src/inference/manifest.js +262 -0
  294. package/dist/src/inference/manifest.js.map +1 -0
  295. package/dist/src/inference/materialize.js +66 -0
  296. package/dist/src/inference/materialize.js.map +1 -0
  297. package/dist/src/inference/model-location.js +34 -0
  298. package/dist/src/inference/model-location.js.map +1 -0
  299. package/dist/src/inference/model-store.js +439 -0
  300. package/dist/src/inference/model-store.js.map +1 -0
  301. package/dist/src/inference/node-client.js +167 -0
  302. package/dist/src/inference/node-client.js.map +1 -0
  303. package/dist/src/inference/node-registry.js +79 -0
  304. package/dist/src/inference/node-registry.js.map +1 -0
  305. package/dist/src/inference/observation.js +79 -0
  306. package/dist/src/inference/observation.js.map +1 -0
  307. package/dist/src/inference/preflight.js +132 -0
  308. package/dist/src/inference/preflight.js.map +1 -0
  309. package/dist/src/inference/process-sglang.js +169 -0
  310. package/dist/src/inference/process-sglang.js.map +1 -0
  311. package/dist/src/inference/protocol-probe.js +29 -0
  312. package/dist/src/inference/protocol-probe.js.map +1 -0
  313. package/dist/src/inference/remote-model-store.js +24 -0
  314. package/dist/src/inference/remote-model-store.js.map +1 -0
  315. package/dist/src/inference/runtime-catalog.js +41 -0
  316. package/dist/src/inference/runtime-catalog.js.map +1 -0
  317. package/dist/src/inference/runtime-store.js +93 -0
  318. package/dist/src/inference/runtime-store.js.map +1 -0
  319. package/dist/src/inference/service-attachment.js +46 -0
  320. package/dist/src/inference/service-attachment.js.map +1 -0
  321. package/dist/src/inference/service-inspection.js +38 -0
  322. package/dist/src/inference/service-inspection.js.map +1 -0
  323. package/dist/src/inference/sglang.js +279 -0
  324. package/dist/src/inference/sglang.js.map +1 -0
  325. package/dist/src/inference/supervisor.js +460 -0
  326. package/dist/src/inference/supervisor.js.map +1 -0
  327. package/dist/src/model-access/environment.js +28 -0
  328. package/dist/src/model-access/environment.js.map +1 -0
  329. package/dist/src/model-access/index.js +4 -0
  330. package/dist/src/model-access/index.js.map +1 -1
  331. package/dist/src/model-access/local-gateway-request.js +139 -0
  332. package/dist/src/model-access/local-gateway-request.js.map +1 -0
  333. package/dist/src/model-access/local-gateway.js +323 -0
  334. package/dist/src/model-access/local-gateway.js.map +1 -0
  335. package/dist/src/model-access/proxy.js +90 -5
  336. package/dist/src/model-access/proxy.js.map +1 -1
  337. package/dist/src/model-access/remote-model.js +58 -0
  338. package/dist/src/model-access/remote-model.js.map +1 -0
  339. package/dist/src/model-access/training.js +115 -0
  340. package/dist/src/model-access/training.js.map +1 -0
  341. package/dist/src/runs/adapter-process.js +2 -2
  342. package/dist/src/runs/adapter-process.js.map +1 -1
  343. package/dist/src/runs/executor-types.js +2 -0
  344. package/dist/src/runs/executor-types.js.map +1 -0
  345. package/dist/src/runs/executor.js +54 -34
  346. package/dist/src/runs/executor.js.map +1 -1
  347. package/dist/src/runs/index.js +1 -0
  348. package/dist/src/runs/index.js.map +1 -1
  349. package/dist/src/runs/local-inference-environment.js +59 -0
  350. package/dist/src/runs/local-inference-environment.js.map +1 -0
  351. package/dist/src/runs/local-inference-run.js +53 -0
  352. package/dist/src/runs/local-inference-run.js.map +1 -0
  353. package/dist/src/runs/outcome.js +22 -0
  354. package/dist/src/runs/outcome.js.map +1 -1
  355. package/dist/src/runs/records.js +9 -0
  356. package/dist/src/runs/records.js.map +1 -1
  357. package/dist/src/runs/request.js +58 -3
  358. package/dist/src/runs/request.js.map +1 -1
  359. package/dist/src/runs/training-evidence.js +22 -0
  360. package/dist/src/runs/training-evidence.js.map +1 -0
  361. package/dist/src/runs/training-runtime.js +20 -0
  362. package/dist/src/runs/training-runtime.js.map +1 -0
  363. package/dist/src/workers/execution-observation.js +32 -0
  364. package/dist/src/workers/execution-observation.js.map +1 -0
  365. package/dist/src/workers/index.js +2 -0
  366. package/dist/src/workers/index.js.map +1 -1
  367. package/dist/src/workers/remote-harbor-ownership.js +266 -0
  368. package/dist/src/workers/remote-harbor-ownership.js.map +1 -0
  369. package/dist/src/workers/remote-harbor-verifier.js +76 -0
  370. package/dist/src/workers/remote-harbor-verifier.js.map +1 -0
  371. package/dist/src/workers/remote-harbor-work-spec.js +66 -10
  372. package/dist/src/workers/remote-harbor-work-spec.js.map +1 -1
  373. package/dist/src/workers/remote-harbor-worker.js +56 -49
  374. package/dist/src/workers/remote-harbor-worker.js.map +1 -1
  375. package/dist/src/workers/remote-worker-host.js +33 -0
  376. package/dist/src/workers/remote-worker-host.js.map +1 -0
  377. package/docs/local-model-inference-spec.zh-CN.md +666 -0
  378. package/docs/schemas/controller-runtime-observation.schema.json +21 -0
  379. package/docs/schemas/eval-execution-plan.schema.json +1 -0
  380. package/docs/schemas/eval-request.schema.json +3 -1
  381. package/docs/schemas/execution-lease.schema.json +95 -1
  382. package/docs/schemas/execution-provider-status.schema.json +162 -28
  383. package/docs/schemas/inference-execution-evidence.schema.json +487 -0
  384. package/docs/schemas/inference-lock.schema.json +487 -0
  385. package/docs/schemas/inference-runtime-manifest.schema.json +156 -0
  386. package/docs/schemas/inference-service-record.schema.json +227 -0
  387. package/docs/schemas/local-inference-selection.schema.json +98 -0
  388. package/docs/schemas/local-model-manifest.schema.json +43 -0
  389. package/docs/schemas/model-node-binding.schema.json +34 -0
  390. package/docs/schemas/model-node-generation-release.schema.json +46 -0
  391. package/docs/schemas/ordered-eval-control.schema.json +39 -0
  392. package/docs/schemas/remote-execution-lease-grant.schema.json +45 -0
  393. package/docs/schemas/remote-harbor-work-spec.schema.json +325 -0
  394. package/docs/schemas/remote-model-binding.schema.json +21 -0
  395. package/docs/schemas/remote-physical-execution.schema.json +108 -0
  396. package/docs/schemas/remote-result-envelope.schema.json +9 -3
  397. package/docs/schemas/remote-tree-envelope.schema.json +17 -3
  398. package/docs/schemas/remote-verifier-result.schema.json +203 -0
  399. package/docs/schemas/remote-verifier-source.schema.json +52 -0
  400. package/docs/schemas/remote-verifier-work.schema.json +125 -0
  401. package/docs/schemas/remote-work-offer.schema.json +170 -34
  402. package/docs/schemas/remote-worker-execution-admission.schema.json +214 -0
  403. package/docs/schemas/remote-worker-generation-cleanup.schema.json +199 -0
  404. package/docs/schemas/remote-worker-registration.schema.json +226 -25
  405. package/docs/schemas/run-manifest.schema.json +211 -44
  406. package/docs/schemas/run-request.schema.json +2 -1
  407. package/docs/schemas/training-external-binding.schema.json +26 -0
  408. package/integrations/harbor/hitch_harbor_agent.py +366 -38
  409. package/integrations/harbor/hitch_harbor_environment.py +22 -1
  410. package/integrations/harbor/hitch_host_credentials.py +354 -0
  411. package/integrations/harbor/hitch_private_exec.py +150 -0
  412. package/integrations/model-call/cli.js +13 -5
  413. package/integrations/training-tool/cli.js +72 -0
  414. package/package.json +8 -2
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import hashlib
7
7
  import json
8
+ import math
8
9
  import os
9
10
  import re
10
11
  import secrets
@@ -127,6 +128,7 @@ class HitchHarborAgent(BaseAgent):
127
128
  verifier_identity: str | None = None,
128
129
  logical_attempt: int | None = None,
129
130
  model_capture: dict[str, Any] | None = None,
131
+ managed_local_inference: dict[str, Any] | None = None,
130
132
  **kwargs: Any,
131
133
  ) -> None:
132
134
  super().__init__(logs_dir=logs_dir, **kwargs)
@@ -165,6 +167,14 @@ class HitchHarborAgent(BaseAgent):
165
167
  raise ValueError("logical_attempt must be a positive integer")
166
168
  self.logical_attempt = logical_attempt
167
169
  self.model_capture = _validate_model_capture(model_capture)
170
+ if managed_local_inference is not None and not _valid_managed_model_identity(managed_local_inference):
171
+ raise ValueError("managed_local_inference identity is invalid")
172
+ if managed_local_inference is not None and self.model_capture is None:
173
+ raise ValueError("managed_local_inference requires model_capture")
174
+ self.managed_local_inference = dict(managed_local_inference) if managed_local_inference else None
175
+ capture_inference = self.model_capture.get("managed_inference") if self.model_capture else None
176
+ if capture_inference != self.managed_local_inference:
177
+ raise ValueError("managed_local_inference must match model_capture identity")
168
178
  self._hitch_version: str | None = None
169
179
  self._entrypoint: str | None = None
170
180
  self._artifact_manifest: dict[str, Any] | None = None
@@ -241,8 +251,7 @@ class HitchHarborAgent(BaseAgent):
241
251
  # bookkeeping and is not identity (spec §4.2).
242
252
  await environment.upload_dir(payload_dir, "/opt/hitch")
243
253
  await self._ensure_node(environment)
244
- platform = await self._container_platform(environment)
245
- node_version = await self._container_node_version(environment)
254
+ platform, node_version = await self._container_node_identity(environment)
246
255
  if self._artifact_manifest is None or not self._artifact_compatible(
247
256
  self._artifact_manifest, platform, node_version
248
257
  ):
@@ -447,22 +456,43 @@ class HitchHarborAgent(BaseAgent):
447
456
  raise RuntimeError(f"{label} exceeds the size limit ({maximum} bytes)")
448
457
  return info
449
458
 
450
- async def _container_platform(self, environment: BaseEnvironment) -> str:
451
- result = await self._exec(
452
- environment,
453
- f'{self._node_prefix()} node -p "process.platform + \'-\' + process.arch"',
459
+ async def _container_node_identity(self, environment: BaseEnvironment) -> tuple[str, str]:
460
+ # Harbor may merge stderr into stdout. Frame only the machine value so
461
+ # startup warnings cannot be mistaken for the platform or version.
462
+ marker = "__HITCH_NODE_IDENTITY__"
463
+ script = (
464
+ f"process.stdout.write('\\n{marker}' + process.platform + '-' + "
465
+ "process.arch + ' ' + process.version + '\\n')"
454
466
  )
455
- platform = (result.stdout or "").strip()
456
- if not re.fullmatch(r"(?:linux|darwin|win32)-[a-z0-9_]+", platform):
457
- raise RuntimeError("container returned an invalid Node.js platform identity")
458
- return platform
459
-
460
- async def _container_node_version(self, environment: BaseEnvironment) -> str:
461
- result = await self._exec(environment, f'{self._node_prefix()} node -p "process.version"')
462
- version = (result.stdout or "").strip()
463
- if not re.fullmatch(r"v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?", version):
464
- raise RuntimeError("container returned an invalid Node.js version")
465
- return version
467
+ command = f"{self._node_prefix()} node -e {shlex.quote(script)}"
468
+ result = await environment.exec(command)
469
+ lines = [line for line in (result.stdout or "").splitlines() if line.startswith(marker)]
470
+ identity = re.fullmatch(
471
+ re.escape(marker) + r"((?:linux|darwin|win32)-[a-z0-9_]+) (v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)",
472
+ lines[0],
473
+ ) if len(lines) == 1 else None
474
+ if result.return_code == 0 and identity is not None:
475
+ return identity.group(1), identity.group(2)
476
+
477
+ reason = f"exit={result.return_code}" if result.return_code != 0 else "expected one valid identity marker"
478
+ failure = self._node_runtime_error(
479
+ "hitch_node_runtime_identity_invalid",
480
+ f"container returned an invalid Node.js identity ({reason}); "
481
+ f"stdout={self._bounded_tail(result.stdout or '', 512)!r}; "
482
+ f"stderr={self._bounded_tail(result.stderr or '', 512)!r}",
483
+ )
484
+ failure.evidence["probe"] = {
485
+ "command": command,
486
+ "return_code": result.return_code,
487
+ "stdout_tail": self._bounded_tail(result.stdout or ""),
488
+ "stderr_tail": self._bounded_tail(result.stderr or ""),
489
+ }
490
+ self._record_node_runtime({"source": "failed", "bin_directory": self._node_bin_directory, **failure.evidence})
491
+ try:
492
+ await self._write_bridge_error(environment, failure.evidence)
493
+ except Exception:
494
+ pass # Keep the probe failure if the container cannot export logs.
495
+ raise failure
466
496
 
467
497
  @staticmethod
468
498
  def _artifact_compatible(manifest: dict[str, Any], platform: str, node_version: str) -> bool:
@@ -864,22 +894,43 @@ class HitchHarborAgent(BaseAgent):
864
894
  if timeout_ms <= 0:
865
895
  raise RuntimeError("candidate whole-task budget expired during phase binding/upload")
866
896
  elif session:
867
- preparation_ms = (time.monotonic_ns() - invocation_started_ns) // 1_000_000
868
- timeout_ms = task_budget_ms - preparation_ms
869
- (self.logs_dir / "hitch-agent-budget.json").write_text(json.dumps({
870
- "schema_version": "hitch-agent-budget@1", "run_id": run_id,
871
- "task_budget_ms": task_budget_ms, "preparation_ms": preparation_ms,
872
- "hitch_timeout_ms": max(0, timeout_ms),
873
- "collection_timeout_ms": session.config["profile"]["budget"]["collection_timeout_ms"],
874
- "scope": "invocation-budget-and-collection-allowance",
875
- }))
897
+ timeout_ms = self._record_session_budget(
898
+ session, run_id, invocation_started_ns, task_budget_ms
899
+ )
876
900
  if timeout_ms <= 0:
877
901
  raise RuntimeError("candidate budget expired during input preparation; no model was launched")
902
+ private_environment, native_deadline_ns = await self._host_task_credentials(
903
+ environment,
904
+ context,
905
+ run_id,
906
+ timeout_ms,
907
+ invocation_started_ns=(
908
+ invocation_started_ns
909
+ if prepared_phase is None and session is None
910
+ else None
911
+ ),
912
+ )
913
+ if private_environment is not None:
914
+ if prepared_phase is not None:
915
+ timeout_ms = (prepared_phase.deadline_ns - time.monotonic_ns()) // 1_000_000
916
+ if native_deadline_ns is not None:
917
+ native_remaining_ms = (native_deadline_ns - time.monotonic_ns()) // 1_000_000
918
+ if native_remaining_ms <= 0:
919
+ raise RuntimeError("candidate native Harbor budget expired during host credential preparation; no model was launched")
920
+ elif timeout_ms <= 0:
921
+ raise RuntimeError("candidate budget expired during host credential preparation; no model was launched")
922
+ if prepared_phase is None and session:
923
+ timeout_ms = self._record_session_budget(
924
+ session, run_id, invocation_started_ns, task_budget_ms
925
+ )
926
+ if timeout_ms <= 0:
927
+ stage = "host credential preparation" if private_environment is not None else "input preparation"
928
+ raise RuntimeError(f"candidate budget expired during {stage}; no model was launched")
878
929
  arguments = [
879
930
  self._node_prefix(),
880
931
  "HITCH_ROOT=/tmp/hitch-state",
881
932
  *proxy_environment,
882
- *(["HITCH_HARBOR_INTERNAL=1"] if self._artifact_uploaded or parent_payload is not None else []),
933
+ "HITCH_HARBOR_INTERNAL=1",
883
934
  f"node {entry} run",
884
935
  "--harness",
885
936
  shlex.quote(self.harness_ref),
@@ -896,11 +947,11 @@ class HitchHarborAgent(BaseAgent):
896
947
  str(timeout_ms),
897
948
  "--output",
898
949
  "jsonl",
950
+ "--internal-run-id", run_id,
899
951
  ]
900
952
  if parent_payload is not None:
901
953
  arguments.extend([
902
954
  "--parent-file", "/tmp/hitch-parent.json",
903
- "--internal-run-id", run_id,
904
955
  ])
905
956
  if prepared_phase is None:
906
957
  arguments.append("--internal-defer-benchmark-observation")
@@ -912,13 +963,22 @@ class HitchHarborAgent(BaseAgent):
912
963
  arguments.extend(["--agent-arg", shlex.quote(value)])
913
964
  for name in self.credential_names:
914
965
  arguments.extend(["--internal-credential-name", shlex.quote(name)])
915
- command = (
916
- "set -o pipefail; "
917
- + " ".join(arguments)
918
- + " 2> >(tee /logs/agent/hitch-stderr.log >&2)"
919
- + " | tee /logs/agent/hitch-events.jsonl"
920
- )
921
- execution = await environment.exec(command, cwd=workdir)
966
+ command = self._logged_run_command(" ".join(arguments))
967
+ try:
968
+ if private_environment is None:
969
+ execution = await environment.exec(command, cwd=workdir)
970
+ else:
971
+ execution = await environment.hitch_exec_with_private_env(
972
+ command,
973
+ cwd=workdir,
974
+ env=private_environment,
975
+ )
976
+ except (Exception, asyncio.CancelledError):
977
+ # Harbor cancels this await on its agent deadline. Collect what is
978
+ # already durable before it removes the container, without turning
979
+ # a cancelled execution into a successful observation.
980
+ await self._preserve_interrupted_run(environment, assigned_run_id)
981
+ raise
922
982
  collection = self._collect_run(
923
983
  environment, context, execution, assigned_run_id=assigned_run_id,
924
984
  context_payload=context_payload, parent_payload=parent_payload, prepared_phase=prepared_phase,
@@ -937,6 +997,231 @@ class HitchHarborAgent(BaseAgent):
937
997
  (self.logs_dir / "hitch-collection-timeout.json").write_text(json.dumps(receipt))
938
998
  raise RuntimeError("hitch_run_collection_timeout: terminal evidence export exceeded its allowance") from error
939
999
 
1000
+ async def _host_task_credentials(
1001
+ self,
1002
+ environment: BaseEnvironment,
1003
+ context: AgentContext,
1004
+ run_id: str,
1005
+ remaining_timeout_ms: int,
1006
+ *,
1007
+ invocation_started_ns: int | None,
1008
+ ) -> tuple[dict[str, str] | None, int | None]:
1009
+ # Preserve the original single-file bridge behavior when this optional
1010
+ # host feature is not configured. An empty value remains configured and
1011
+ # reaches the strict parser below.
1012
+ if "HITCH_HOST_CREDENTIAL_HELPER_JSON" not in os.environ:
1013
+ return None, None
1014
+ from hitch_host_credentials import (
1015
+ HOST_CREDENTIAL_VALIDITY_MARGIN_MS,
1016
+ HostCredentialHelperError,
1017
+ load_host_credential_helper,
1018
+ prepare_host_credentials,
1019
+ )
1020
+
1021
+ try:
1022
+ config = load_host_credential_helper(self.credential_names)
1023
+ if config is None:
1024
+ return None, None
1025
+ if not callable(getattr(environment, "hitch_exec_with_private_env", None)):
1026
+ raise HostCredentialHelperError(
1027
+ "host_credential_transport_unsupported",
1028
+ "Harbor environment does not support private Target credentials",
1029
+ )
1030
+ native_deadline_ns: int | None = None
1031
+ if invocation_started_ns is not None and remaining_timeout_ms <= 0:
1032
+ native_timeout_ms = self._native_harbor_agent_timeout_ms(environment)
1033
+ if native_timeout_ms is None:
1034
+ raise HostCredentialHelperError(
1035
+ "host_credential_helper_request_invalid",
1036
+ "native Harbor agent timeout is not finite",
1037
+ )
1038
+ native_deadline_ns = invocation_started_ns + native_timeout_ms * 1_000_000
1039
+ native_remaining_ms = (native_deadline_ns - time.monotonic_ns()) // 1_000_000
1040
+ if native_remaining_ms <= 0:
1041
+ raise RuntimeError("candidate native Harbor budget expired during input preparation; no model was launched")
1042
+ remaining_timeout_ms = native_remaining_ms
1043
+ credentials = await prepare_host_credentials(
1044
+ config,
1045
+ remaining_timeout_ms + HOST_CREDENTIAL_VALIDITY_MARGIN_MS,
1046
+ )
1047
+ return credentials, native_deadline_ns
1048
+ except HostCredentialHelperError as error:
1049
+ trial_id, task_id, attempt = self._trial_identity()
1050
+ evidence = {
1051
+ "schema_version": "1",
1052
+ "code": error.code,
1053
+ "message": error.message,
1054
+ "recorded_at": datetime.now(timezone.utc).isoformat(),
1055
+ "scope": "host-task-credential-helper",
1056
+ "eval_id": self.eval_id,
1057
+ "trial_id": trial_id,
1058
+ "task_id": task_id,
1059
+ "attempt": attempt,
1060
+ "assigned_run_id": run_id,
1061
+ }
1062
+ if context.metadata is None:
1063
+ context.metadata = {}
1064
+ context.metadata["hitch_bridge_error_code"] = error.code
1065
+ context.metadata["hitch_bridge_error_artifact"] = "hitch-bridge-error.json"
1066
+ try:
1067
+ await self._write_bridge_error(environment, evidence)
1068
+ except Exception:
1069
+ pass
1070
+ raise HitchBridgeError(error.code, error.message, evidence) from None
1071
+
1072
+ def _native_harbor_agent_timeout_ms(self, environment: BaseEnvironment) -> int | None:
1073
+ """Resolve Harbor 0.21's effective native agent timeout from its own inputs."""
1074
+ try:
1075
+ from harbor.models.task.config import TaskConfig
1076
+ from harbor.models.trial.config import TrialConfig
1077
+
1078
+ environment_dir = Path(getattr(environment, "environment_dir"))
1079
+ trial_paths = getattr(environment, "trial_paths")
1080
+ config_path = Path(getattr(trial_paths, "config_path"))
1081
+ task_config = TaskConfig.model_validate_toml(
1082
+ (environment_dir.parent / "task.toml").read_text(encoding="utf-8")
1083
+ )
1084
+ trial_config = TrialConfig.model_validate_json(
1085
+ config_path.read_text(encoding="utf-8")
1086
+ )
1087
+
1088
+ def resolve(default_timeout_sec: float | None) -> float | None:
1089
+ base_timeout_sec = trial_config.agent.override_timeout_sec or default_timeout_sec
1090
+ if base_timeout_sec is None:
1091
+ return None
1092
+ multiplier = trial_config.agent_timeout_multiplier
1093
+ if multiplier is None:
1094
+ multiplier = trial_config.timeout_multiplier
1095
+ return min(
1096
+ base_timeout_sec,
1097
+ trial_config.agent.max_timeout_sec or float("inf"),
1098
+ ) * multiplier
1099
+
1100
+ if task_config.steps:
1101
+ resolved = [
1102
+ resolve(
1103
+ step.agent.timeout_sec
1104
+ if step.agent.timeout_sec is not None
1105
+ else task_config.agent.timeout_sec
1106
+ )
1107
+ for step in task_config.steps
1108
+ ]
1109
+ # Harbor does not identify the current step in BaseAgent.run().
1110
+ # Requiring the largest finite step budget prevents a credential
1111
+ # from expiring during any native step without guessing order.
1112
+ timeout_sec = None if any(value is None for value in resolved) else max(resolved)
1113
+ else:
1114
+ timeout_sec = resolve(task_config.agent.timeout_sec)
1115
+ if timeout_sec is None:
1116
+ return None
1117
+ timeout_ms = math.ceil(timeout_sec * 1_000)
1118
+ if not math.isfinite(timeout_sec) or not 1 <= timeout_ms <= 9_007_199_254_740_991:
1119
+ raise ValueError("native Harbor timeout is outside the supported range")
1120
+ return timeout_ms
1121
+ except Exception:
1122
+ # Task/config contents and host paths are not safe diagnostic text.
1123
+ from hitch_host_credentials import HostCredentialHelperError
1124
+ raise HostCredentialHelperError(
1125
+ "host_credential_helper_request_invalid",
1126
+ "native Harbor agent timeout could not be resolved",
1127
+ ) from None
1128
+
1129
+ def _record_session_budget(
1130
+ self,
1131
+ session,
1132
+ run_id: str,
1133
+ invocation_started_ns: int,
1134
+ task_budget_ms: int,
1135
+ ) -> int:
1136
+ preparation_ms = (time.monotonic_ns() - invocation_started_ns) // 1_000_000
1137
+ timeout_ms = task_budget_ms - preparation_ms
1138
+ (self.logs_dir / "hitch-agent-budget.json").write_text(json.dumps({
1139
+ "schema_version": "hitch-agent-budget@1", "run_id": run_id,
1140
+ "task_budget_ms": task_budget_ms, "preparation_ms": preparation_ms,
1141
+ "hitch_timeout_ms": max(0, timeout_ms),
1142
+ "collection_timeout_ms": session.config["profile"]["budget"]["collection_timeout_ms"],
1143
+ "scope": "invocation-budget-and-collection-allowance",
1144
+ }))
1145
+ return timeout_ms
1146
+
1147
+ @staticmethod
1148
+ def _logged_run_command(invocation: str) -> str:
1149
+ # Bash process substitution leaves an extra pipe FD in the harness and
1150
+ # its descendants. A task's background service may outlive the harness.
1151
+ # Regular files keep live logs without tying exec completion to that FD.
1152
+ return (
1153
+ "hitch_run_exit=0; " + invocation
1154
+ + " > /logs/agent/hitch-events.jsonl 2> /logs/agent/hitch-stderr.log || hitch_run_exit=$?; "
1155
+ + "cat /logs/agent/hitch-events.jsonl || exit $?; "
1156
+ + "cat /logs/agent/hitch-stderr.log >&2 || exit $?; "
1157
+ + 'exit "$hitch_run_exit"'
1158
+ )
1159
+
1160
+ async def _preserve_interrupted_run(self, environment: BaseEnvironment, run_id: str) -> None:
1161
+ # This is a diagnostic directory, never a complete/sealed run bundle.
1162
+ # runtime-home contains credentials and must not be copied. Only files
1163
+ # already produced by Hitch's redacted evidence writers are eligible.
1164
+ destination = f"/logs/agent/hitch-interrupted-run-{uuid.uuid4().hex}"
1165
+ script = r"""
1166
+ import {lstat,mkdir,open,readFile,writeFile} from 'node:fs/promises';
1167
+ import {constants} from 'node:fs';
1168
+ import path from 'node:path';
1169
+ const [source,target]=process.argv.slice(1);
1170
+ let remaining=64*1024*1024;
1171
+ const copied=[];
1172
+ await mkdir(target,{mode:0o700});
1173
+ async function copy(relative){
1174
+ if(copied.includes(relative))return;
1175
+ if(typeof relative!=='string'||relative.includes('\\')||path.isAbsolute(relative)
1176
+ ||relative.split('/').some(p=>!p||p==='.'||p==='..')) return;
1177
+ const file=path.join(source,relative);
1178
+ try {
1179
+ for(let p=path.dirname(file);p!=='/';p=path.dirname(p)) {
1180
+ const info=await lstat(p); if(!info.isDirectory()||info.isSymbolicLink()) return;
1181
+ }
1182
+ if(!(await lstat(file)).isFile())return;
1183
+ const input=await open(file,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
1184
+ try {
1185
+ const info=await input.stat();
1186
+ if(!info.isFile()||info.nlink!==1||info.size>remaining) return;
1187
+ const bytes=Buffer.alloc(info.size);
1188
+ let offset=0;
1189
+ while(offset<bytes.length){const r=await input.read(bytes,offset,bytes.length-offset,offset);if(!r.bytesRead)break;offset+=r.bytesRead;}
1190
+ remaining-=offset;
1191
+ const output=path.join(target,relative);
1192
+ await mkdir(path.dirname(output),{recursive:true,mode:0o700});
1193
+ await writeFile(output,bytes.subarray(0,offset),{flag:'wx',mode:0o600});
1194
+ copied.push(relative);
1195
+ } finally {await input.close();}
1196
+ } catch(e) {if(!['ENOENT','ELOOP','ENOTDIR'].includes(e.code))throw e;}
1197
+ }
1198
+ for(const name of ['result.json','manifest.json','events.jsonl','stdout.log','stderr.log','trajectory.ref.json']) await copy(name);
1199
+ if(copied.includes('trajectory.ref.json')) {
1200
+ try {
1201
+ const ref=JSON.parse(await readFile(path.join(target,'trajectory.ref.json'),'utf8'));
1202
+ const files=Array.isArray(ref.files)?ref.files.map(f=>f?.path):[ref.path];
1203
+ for(const file of files.slice(0,256)) if(typeof file==='string'&&file.startsWith('trajectory/'))await copy(file);
1204
+ } catch(e) {if(!(e instanceof SyntaxError))throw e;}
1205
+ }
1206
+ // ProviderCaptureWriter persists redacted output before finalization creates the reference.
1207
+ for(const name of ['trajectory/provider/events.jsonl','trajectory/provider/transcript.txt']) await copy(name);
1208
+ await writeFile(path.join(target,'diagnostic.json'),JSON.stringify({complete:false,copied}),{flag:'wx',mode:0o600});
1209
+ """.strip()
1210
+ receipt: dict[str, Any] = {"run_id": run_id, "complete": False, "directory": destination}
1211
+ try:
1212
+ result = await asyncio.wait_for(environment.exec(
1213
+ f"{self._node_prefix()} node --input-type=module -e {shlex.quote(script)} "
1214
+ f"{shlex.quote(f'/tmp/hitch-state/runs/{run_id}')} {shlex.quote(destination)}"
1215
+ ), timeout=5)
1216
+ receipt["export_return_code"] = result.return_code
1217
+ except (Exception, asyncio.CancelledError) as error:
1218
+ # Preserve the original exception, not a best-effort export failure.
1219
+ receipt["export_error"] = type(error).__name__
1220
+ try:
1221
+ (self.logs_dir / "hitch-interrupted-run.json").write_text(json.dumps(receipt) + "\n")
1222
+ except OSError:
1223
+ pass
1224
+
940
1225
  async def _collect_run(
941
1226
  self, environment: BaseEnvironment, context: AgentContext, execution: ExecResult, *,
942
1227
  assigned_run_id: str, context_payload: dict[str, Any], parent_payload: dict[str, Any] | None,
@@ -1131,9 +1416,25 @@ mv "$stage_dir" "$target_dir"
1131
1416
  raise RuntimeError("hitch-model-proxy-health: required model proxy is unreachable")
1132
1417
  return [], "degraded-unreachable"
1133
1418
  base = self.model_capture["base_url_template"].replace("{run_id}", run_id)
1419
+ training = self.model_capture.get("training_external")
1134
1420
  return [
1135
1421
  f"OPENAI_BASE_URL={shlex.quote(base.replace('{provider}', 'openai'))}",
1136
1422
  f"ANTHROPIC_BASE_URL={shlex.quote(base.replace('{provider}', 'anthropic'))}",
1423
+ *([
1424
+ "HITCH_TRAINING_EXTERNAL=1",
1425
+ f"HITCH_TRAINING_RUN_ID={run_id}",
1426
+ f"HITCH_TRAINING_BINDING={shlex.quote(json.dumps(training, separators=(',', ':')))}",
1427
+ "OPENAI_API_KEY=hitch-training-external",
1428
+ ] if training else []),
1429
+ *([
1430
+ "HITCH_MANAGED_LOCAL_INFERENCE=1",
1431
+ f"HITCH_MANAGED_RUN_ID={run_id}",
1432
+ f"HITCH_MANAGED_INFERENCE_ID={self.managed_local_inference['inference_id']}",
1433
+ f"HITCH_MANAGED_MODEL_ID={self.managed_local_inference['model_id']}",
1434
+ "OPENAI_API_KEY=hitch-managed-local",
1435
+ *([f"HITCH_MANAGED_NODE_BINDING={shlex.quote(json.dumps(self.managed_local_inference['model_node'], separators=(',', ':')))}"]
1436
+ if self.managed_local_inference.get("model_node") else []),
1437
+ ] if self.managed_local_inference else []),
1137
1438
  ], "healthy"
1138
1439
 
1139
1440
  @staticmethod
@@ -1595,15 +1896,16 @@ esac""", "hitch_node_runtime_incompatible")
1595
1896
  def _validate_model_capture(value: dict[str, Any] | None) -> dict[str, Any] | None:
1596
1897
  if value is None:
1597
1898
  return None
1598
- if not isinstance(value, dict) or set(value) != {
1899
+ required_fields = {
1599
1900
  "schema_version", "mode", "required", "topology", "base_url_template", "health_url_template"
1600
- }:
1901
+ }
1902
+ if not isinstance(value, dict) or not required_fields.issubset(value) or set(value) - required_fields - {"managed_inference", "training_external"}:
1601
1903
  raise ValueError("model_capture fields are invalid")
1602
1904
  if (
1603
1905
  value.get("schema_version") != "1"
1604
1906
  or value.get("mode") not in {"proxy", "hybrid"}
1605
1907
  or not isinstance(value.get("required"), bool)
1606
- or value.get("topology") != "host-side"
1908
+ or value.get("topology") not in {"host-side", "in-sandbox"}
1607
1909
  ):
1608
1910
  raise ValueError("model_capture identity is invalid")
1609
1911
  for field, provider_count in (("base_url_template", 1), ("health_url_template", 0)):
@@ -1622,9 +1924,35 @@ def _validate_model_capture(value: dict[str, Any] | None) -> dict[str, Any] | No
1622
1924
  )
1623
1925
  if parsed.scheme not in {"http", "https"} or not parsed.hostname or parsed.username or parsed.password:
1624
1926
  raise ValueError(f"model_capture {field} URL is invalid")
1927
+ managed = value.get("managed_inference")
1928
+ training = value.get("training_external")
1929
+ if training is not None:
1930
+ if (managed is not None or not isinstance(training, dict)
1931
+ or set(training) != {"binding_id", "training_run_id", "policy_version", "generation_contract_digest", "api", "max_output_tokens", "max_episode_steps"}
1932
+ or training.get("api") != "chat-completions" or value.get("required") is not True
1933
+ or any(not isinstance(training.get(k), str) or not training[k] for k in ("binding_id", "training_run_id", "policy_version"))
1934
+ or re.fullmatch(r"sha256:[a-f0-9]{64}", str(training.get("generation_contract_digest"))) is None
1935
+ or any(type(training.get(k)) is not int or training[k] <= 0 for k in ("max_output_tokens", "max_episode_steps"))):
1936
+ raise ValueError("model_capture training identity is invalid")
1937
+ if managed is not None and not _valid_managed_model_identity(managed):
1938
+ raise ValueError("model_capture managed inference identity is invalid")
1625
1939
  return dict(value)
1626
1940
 
1627
1941
 
1942
+ def _valid_managed_model_identity(value: Any) -> bool:
1943
+ if (not isinstance(value, dict) or not {"inference_id", "model_id"}.issubset(value)
1944
+ or set(value) - {"inference_id", "model_id", "model_node"}
1945
+ or any(not isinstance(value[k], str) or re.fullmatch(r"sha256:[a-f0-9]{64}", value[k]) is None for k in ("inference_id", "model_id"))):
1946
+ return False
1947
+ if "model_node" not in value:
1948
+ return True
1949
+ node = value["model_node"]
1950
+ return (isinstance(node, dict) and set(node) == {"schema_version", "node_id", "generation", "runtime_digest", "launcher"}
1951
+ and node["schema_version"] == "2" and node["launcher"] == "process"
1952
+ and all(isinstance(node[k], str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}", node[k]) is not None for k in ("node_id", "generation"))
1953
+ and isinstance(node["runtime_digest"], str) and re.fullmatch(r"sha256:[a-f0-9]{64}", node["runtime_digest"]) is not None)
1954
+
1955
+
1628
1956
  def canonical_manifest_json(manifest: dict[str, Any]) -> str:
1629
1957
  """Canonically encode the runtime identity `{ schema_version, node_range,
1630
1958
  entrypoints, files }` with sorted object keys and no insignificant
@@ -31,6 +31,7 @@ _REQUIRED_LABELS = {
31
31
  _ALLOWED_LABELS = _REQUIRED_LABELS | {_LABEL_TASK}
32
32
  _COMPOSE_RESET_MARKER = "__HITCH_COMPOSE_RESET_NULL__"
33
33
  _GPU_OVERRIDE_PREFIX = "__HITCH_GPU_OVERRIDE_"
34
+ _HOST_CREDENTIAL_HELPER_ENV = "HITCH_HOST_CREDENTIAL_HELPER_JSON"
34
35
 
35
36
 
36
37
  class HitchHarborDockerEnvironment(DockerEnvironment):
@@ -128,6 +129,25 @@ class HitchHarborDockerEnvironment(DockerEnvironment):
128
129
  return result.model_copy(update={"disable_internet": True, "dynamic_network_policy": False})
129
130
  return result
130
131
 
132
+ def _compose_env_vars(self, include_os_env=True):
133
+ result = super()._compose_env_vars(include_os_env=include_os_env)
134
+ # The host-only helper command is not a task Compose interpolation
135
+ # input. The agent reads it directly from its trusted host process.
136
+ result.pop(_HOST_CREDENTIAL_HELPER_ENV, None)
137
+ return result
138
+
139
+ async def hitch_exec_with_private_env(self, command, *, env, cwd=None, timeout_sec=None, user=None):
140
+ """Execute one Target without placing per-exec credential values in argv."""
141
+ from hitch_private_exec import exec_with_private_environment
142
+ return await exec_with_private_environment(
143
+ self,
144
+ command,
145
+ env=env,
146
+ cwd=cwd,
147
+ timeout_sec=timeout_sec,
148
+ user=user,
149
+ )
150
+
131
151
  async def _apply_network_policy(self, network_policy):
132
152
  if self._hitch_static_no_network:
133
153
  if network_policy != self.network_policy:
@@ -375,7 +395,8 @@ def _validate_labels(value: Mapping[str, str] | None) -> dict[str, str]:
375
395
  labels = dict(value)
376
396
  if (
377
397
  not re.fullmatch(r"[a-f0-9]{24}", labels.get(_LABEL_ROOT, ""))
378
- or labels.get(_LABEL_PROVIDER) != "local-docker"
398
+ or not isinstance(labels.get(_LABEL_PROVIDER), str)
399
+ or not re.fullmatch(r"[a-z0-9][a-z0-9._-]{0,127}", labels[_LABEL_PROVIDER])
379
400
  or not re.fullmatch(r"eval_[a-f0-9]{32}", labels.get(_LABEL_EVAL, ""))
380
401
  or not re.fullmatch(r"work_[a-f0-9]{32}", labels.get(_LABEL_WORK, ""))
381
402
  or not re.fullmatch(r"lease_[a-f0-9]{32}", labels.get(_LABEL_LEASE, ""))