agent-hitch 0.2.5 → 0.2.7

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 (395) hide show
  1. package/README.md +14 -1
  2. package/dist/scripts/canary-buildkit-secrets.js +166 -0
  3. package/dist/scripts/canary-buildkit-secrets.js.map +1 -0
  4. package/dist/scripts/canary-collision-domains.js +77 -0
  5. package/dist/scripts/canary-collision-domains.js.map +1 -0
  6. package/dist/scripts/canary-docker-image-observation.js +181 -0
  7. package/dist/scripts/canary-docker-image-observation.js.map +1 -0
  8. package/dist/scripts/canary-gpu-hardware.js +95 -0
  9. package/dist/scripts/canary-gpu-hardware.js.map +1 -0
  10. package/dist/scripts/canary-harbor-load.js +365 -0
  11. package/dist/scripts/canary-harbor-load.js.map +1 -0
  12. package/dist/scripts/canary-packaged-harness.js +75 -0
  13. package/dist/scripts/canary-packaged-harness.js.map +1 -0
  14. package/dist/scripts/canary-resource-load.js +188 -0
  15. package/dist/scripts/canary-resource-load.js.map +1 -0
  16. package/dist/scripts/check-architecture.js +8 -4
  17. package/dist/scripts/check-architecture.js.map +1 -1
  18. package/dist/scripts/docker-reaper-crash-child.js +21 -0
  19. package/dist/scripts/docker-reaper-crash-child.js.map +1 -0
  20. package/dist/src/adapters/catalog.js +1 -0
  21. package/dist/src/adapters/catalog.js.map +1 -1
  22. package/dist/src/adapters/discovery.js +1 -1
  23. package/dist/src/adapters/discovery.js.map +1 -1
  24. package/dist/src/adapters/providers/claude.js +6 -0
  25. package/dist/src/adapters/providers/claude.js.map +1 -1
  26. package/dist/src/adapters/providers/codex.js +6 -0
  27. package/dist/src/adapters/providers/codex.js.map +1 -1
  28. package/dist/src/adapters/providers/deepseek.js +6 -0
  29. package/dist/src/adapters/providers/deepseek.js.map +1 -1
  30. package/dist/src/adapters/providers/opencode.js +6 -0
  31. package/dist/src/adapters/providers/opencode.js.map +1 -1
  32. package/dist/src/adapters/providers/pi.js +6 -0
  33. package/dist/src/adapters/providers/pi.js.map +1 -1
  34. package/dist/src/artifacts/index.js +1 -0
  35. package/dist/src/artifacts/index.js.map +1 -1
  36. package/dist/src/backends/harbor/backend.js +55 -104
  37. package/dist/src/backends/harbor/backend.js.map +1 -1
  38. package/dist/src/backends/harbor/environment-config.js +92 -0
  39. package/dist/src/backends/harbor/environment-config.js.map +1 -0
  40. package/dist/src/backends/harbor/index.js +5 -1
  41. package/dist/src/backends/harbor/index.js.map +1 -1
  42. package/dist/src/backends/harbor/model-proxy-config.js +23 -0
  43. package/dist/src/backends/harbor/model-proxy-config.js.map +1 -0
  44. package/dist/src/backends/harbor/process.js +177 -0
  45. package/dist/src/backends/harbor/process.js.map +1 -0
  46. package/dist/src/backends/harbor/runtime-toolchain.js +30 -0
  47. package/dist/src/backends/harbor/runtime-toolchain.js.map +1 -0
  48. package/dist/src/backends/harbor/supervisor.js +74 -0
  49. package/dist/src/backends/harbor/supervisor.js.map +1 -0
  50. package/dist/src/backends/harbor/task-resources.js +206 -0
  51. package/dist/src/backends/harbor/task-resources.js.map +1 -0
  52. package/dist/src/backends/harbor/tools.js +6 -29
  53. package/dist/src/backends/harbor/tools.js.map +1 -1
  54. package/dist/src/backends/harbor/verifier-config.js +11 -0
  55. package/dist/src/backends/harbor/verifier-config.js.map +1 -0
  56. package/dist/src/backends/index.js +4 -1
  57. package/dist/src/backends/index.js.map +1 -1
  58. package/dist/src/cli/arguments.js +14 -0
  59. package/dist/src/cli/arguments.js.map +1 -1
  60. package/dist/src/cli/commands/daemon.js +135 -8
  61. package/dist/src/cli/commands/daemon.js.map +1 -1
  62. package/dist/src/cli/commands/eval.js +214 -4
  63. package/dist/src/cli/commands/eval.js.map +1 -1
  64. package/dist/src/cli/commands/images.js +40 -0
  65. package/dist/src/cli/commands/images.js.map +1 -0
  66. package/dist/src/cli/commands/run.js +6 -3
  67. package/dist/src/cli/commands/run.js.map +1 -1
  68. package/dist/src/cli/commands/runs.js +25 -3
  69. package/dist/src/cli/commands/runs.js.map +1 -1
  70. package/dist/src/cli/commands/worker.js +79 -0
  71. package/dist/src/cli/commands/worker.js.map +1 -0
  72. package/dist/src/cli/main.js +4 -0
  73. package/dist/src/cli/main.js.map +1 -1
  74. package/dist/src/cli/output.js +72 -3
  75. package/dist/src/cli/output.js.map +1 -1
  76. package/dist/src/control-plane/build-admission.js +69 -0
  77. package/dist/src/control-plane/build-admission.js.map +1 -0
  78. package/dist/src/control-plane/build-records.js +6 -0
  79. package/dist/src/control-plane/build-records.js.map +1 -0
  80. package/dist/src/control-plane/collisions.js +40 -0
  81. package/dist/src/control-plane/collisions.js.map +1 -0
  82. package/dist/src/control-plane/eval-control-state.js +22 -0
  83. package/dist/src/control-plane/eval-control-state.js.map +1 -0
  84. package/dist/src/control-plane/eval-control-work.js +39 -0
  85. package/dist/src/control-plane/eval-control-work.js.map +1 -0
  86. package/dist/src/control-plane/eval-image-resolution.js +52 -0
  87. package/dist/src/control-plane/eval-image-resolution.js.map +1 -0
  88. package/dist/src/control-plane/eval-image-services.js +30 -0
  89. package/dist/src/control-plane/eval-image-services.js.map +1 -0
  90. package/dist/src/control-plane/eval-records.js +252 -0
  91. package/dist/src/control-plane/eval-records.js.map +1 -0
  92. package/dist/src/control-plane/eval-recovery.js +181 -0
  93. package/dist/src/control-plane/eval-recovery.js.map +1 -0
  94. package/dist/src/control-plane/eval-scheduler.js +447 -0
  95. package/dist/src/control-plane/eval-scheduler.js.map +1 -0
  96. package/dist/src/control-plane/index.js +20 -0
  97. package/dist/src/control-plane/index.js.map +1 -0
  98. package/dist/src/control-plane/local-worker.js +49 -0
  99. package/dist/src/control-plane/local-worker.js.map +1 -0
  100. package/dist/src/control-plane/model-capture-planning.js +12 -0
  101. package/dist/src/control-plane/model-capture-planning.js.map +1 -0
  102. package/dist/src/control-plane/remote-result-transport.js +172 -0
  103. package/dist/src/control-plane/remote-result-transport.js.map +1 -0
  104. package/dist/src/control-plane/remote-work-coordinator.js +332 -0
  105. package/dist/src/control-plane/remote-work-coordinator.js.map +1 -0
  106. package/dist/src/control-plane/remote-work-inputs.js +266 -0
  107. package/dist/src/control-plane/remote-work-inputs.js.map +1 -0
  108. package/dist/src/control-plane/remote-work-recovery.js +181 -0
  109. package/dist/src/control-plane/remote-work-recovery.js.map +1 -0
  110. package/dist/src/control-plane/remote-worker-artifacts.js +135 -0
  111. package/dist/src/control-plane/remote-worker-artifacts.js.map +1 -0
  112. package/dist/src/control-plane/remote-worker-client.js +202 -0
  113. package/dist/src/control-plane/remote-worker-client.js.map +1 -0
  114. package/dist/src/control-plane/remote-worker-configuration.js +34 -0
  115. package/dist/src/control-plane/remote-worker-configuration.js.map +1 -0
  116. package/dist/src/control-plane/remote-worker-credentials.js +58 -0
  117. package/dist/src/control-plane/remote-worker-credentials.js.map +1 -0
  118. package/dist/src/control-plane/remote-worker-protocol.js +510 -0
  119. package/dist/src/control-plane/remote-worker-protocol.js.map +1 -0
  120. package/dist/src/control-plane/remote-worker-runner.js +214 -0
  121. package/dist/src/control-plane/remote-worker-runner.js.map +1 -0
  122. package/dist/src/control-plane/remote-workers.js +377 -0
  123. package/dist/src/control-plane/remote-workers.js.map +1 -0
  124. package/dist/src/control-plane/rerun-scheduler.js +411 -0
  125. package/dist/src/control-plane/rerun-scheduler.js.map +1 -0
  126. package/dist/src/control-plane/rerun-submission.js +56 -0
  127. package/dist/src/control-plane/rerun-submission.js.map +1 -0
  128. package/dist/src/control-plane/resources.js +182 -0
  129. package/dist/src/control-plane/resources.js.map +1 -0
  130. package/dist/src/control-plane/scheduler-eval-entry.js +32 -0
  131. package/dist/src/control-plane/scheduler-eval-entry.js.map +1 -0
  132. package/dist/src/control-plane/synthetic-result.js +21 -0
  133. package/dist/src/control-plane/synthetic-result.js.map +1 -0
  134. package/dist/src/control-plane/work-admission.js +21 -0
  135. package/dist/src/control-plane/work-admission.js.map +1 -0
  136. package/dist/src/control-plane/work-dispatcher.js +229 -0
  137. package/dist/src/control-plane/work-dispatcher.js.map +1 -0
  138. package/dist/src/controller-runtime/hash.js +12 -1
  139. package/dist/src/controller-runtime/hash.js.map +1 -1
  140. package/dist/src/controller-runtime/index.js +1 -1
  141. package/dist/src/controller-runtime/index.js.map +1 -1
  142. package/dist/src/controller-runtime/store.js +15 -6
  143. package/dist/src/controller-runtime/store.js.map +1 -1
  144. package/dist/src/daemon/client.js.map +1 -1
  145. package/dist/src/daemon/event-stream.js +46 -0
  146. package/dist/src/daemon/event-stream.js.map +1 -0
  147. package/dist/src/daemon/health.js +66 -0
  148. package/dist/src/daemon/health.js.map +1 -0
  149. package/dist/src/daemon/launcher.js +28 -1
  150. package/dist/src/daemon/launcher.js.map +1 -1
  151. package/dist/src/daemon/logging.js +23 -0
  152. package/dist/src/daemon/logging.js.map +1 -0
  153. package/dist/src/daemon/scheduler.js +26 -5
  154. package/dist/src/daemon/scheduler.js.map +1 -1
  155. package/dist/src/daemon/server.js +255 -98
  156. package/dist/src/daemon/server.js.map +1 -1
  157. package/dist/src/daemon/telemetry.js +163 -0
  158. package/dist/src/daemon/telemetry.js.map +1 -0
  159. package/dist/src/daemon/worker-routes.js +219 -0
  160. package/dist/src/daemon/worker-routes.js.map +1 -0
  161. package/dist/src/domain/bundles.js +2 -0
  162. package/dist/src/domain/bundles.js.map +1 -0
  163. package/dist/src/domain/control-plane.js +2 -0
  164. package/dist/src/domain/control-plane.js.map +1 -0
  165. package/dist/src/domain/execution-evidence.js +2 -0
  166. package/dist/src/domain/execution-evidence.js.map +1 -0
  167. package/dist/src/domain/execution-plan.js +2 -0
  168. package/dist/src/domain/execution-plan.js.map +1 -0
  169. package/dist/src/domain/images.js +2 -0
  170. package/dist/src/domain/images.js.map +1 -0
  171. package/dist/src/domain/index.js +9 -0
  172. package/dist/src/domain/index.js.map +1 -1
  173. package/dist/src/domain/interactions.js +2 -0
  174. package/dist/src/domain/interactions.js.map +1 -0
  175. package/dist/src/domain/providers.js +2 -0
  176. package/dist/src/domain/providers.js.map +1 -0
  177. package/dist/src/domain/resources.js +2 -0
  178. package/dist/src/domain/resources.js.map +1 -0
  179. package/dist/src/domain/workers.js +2 -0
  180. package/dist/src/domain/workers.js.map +1 -0
  181. package/dist/src/evals/collect-only-rerun.js +182 -0
  182. package/dist/src/evals/collect-only-rerun.js.map +1 -0
  183. package/dist/src/evals/directory.js +30 -0
  184. package/dist/src/evals/directory.js.map +1 -0
  185. package/dist/src/evals/docker-engine-stats.js +89 -0
  186. package/dist/src/evals/docker-engine-stats.js.map +1 -0
  187. package/dist/src/evals/docker-ownership.js +51 -0
  188. package/dist/src/evals/docker-ownership.js.map +1 -0
  189. package/dist/src/evals/docker-reaper.js +144 -0
  190. package/dist/src/evals/docker-reaper.js.map +1 -0
  191. package/dist/src/evals/docker-resource-observer.js +207 -0
  192. package/dist/src/evals/docker-resource-observer.js.map +1 -0
  193. package/dist/src/evals/environment-image-planning.js +164 -0
  194. package/dist/src/evals/environment-image-planning.js.map +1 -0
  195. package/dist/src/evals/eval-artifact-planning.js +56 -0
  196. package/dist/src/evals/eval-artifact-planning.js.map +1 -0
  197. package/dist/src/evals/eval-finalization.js +10 -0
  198. package/dist/src/evals/eval-finalization.js.map +1 -0
  199. package/dist/src/evals/eval-lifecycle-events.js +16 -0
  200. package/dist/src/evals/eval-lifecycle-events.js.map +1 -0
  201. package/dist/src/evals/events.js +53 -5
  202. package/dist/src/evals/events.js.map +1 -1
  203. package/dist/src/evals/execution-evidence.js +116 -0
  204. package/dist/src/evals/execution-evidence.js.map +1 -0
  205. package/dist/src/evals/execution-leases.js +342 -0
  206. package/dist/src/evals/execution-leases.js.map +1 -0
  207. package/dist/src/evals/execution-plan-artifacts.js +72 -0
  208. package/dist/src/evals/execution-plan-artifacts.js.map +1 -0
  209. package/dist/src/evals/execution-plan-images.js +69 -0
  210. package/dist/src/evals/execution-plan-images.js.map +1 -0
  211. package/dist/src/evals/execution-plan-resources.js +174 -0
  212. package/dist/src/evals/execution-plan-resources.js.map +1 -0
  213. package/dist/src/evals/execution-plan.js +415 -0
  214. package/dist/src/evals/execution-plan.js.map +1 -0
  215. package/dist/src/evals/harbor-artifact-builder-image.js +99 -0
  216. package/dist/src/evals/harbor-artifact-builder-image.js.map +1 -0
  217. package/dist/src/evals/harbor-artifact-builder.js +354 -0
  218. package/dist/src/evals/harbor-artifact-builder.js.map +1 -0
  219. package/dist/src/evals/harbor-bridge-error.js +39 -3
  220. package/dist/src/evals/harbor-bridge-error.js.map +1 -1
  221. package/dist/src/evals/harbor-phase-timings.js +60 -0
  222. package/dist/src/evals/harbor-phase-timings.js.map +1 -0
  223. package/dist/src/evals/index.js +26 -3
  224. package/dist/src/evals/index.js.map +1 -1
  225. package/dist/src/evals/infrastructure-retry.js +218 -0
  226. package/dist/src/evals/infrastructure-retry.js.map +1 -0
  227. package/dist/src/evals/interaction-capture-import.js +49 -0
  228. package/dist/src/evals/interaction-capture-import.js.map +1 -0
  229. package/dist/src/evals/local-docker-provider.js +372 -0
  230. package/dist/src/evals/local-docker-provider.js.map +1 -0
  231. package/dist/src/evals/local-eval-planning.js +80 -0
  232. package/dist/src/evals/local-eval-planning.js.map +1 -0
  233. package/dist/src/evals/model-capture-plan.js +22 -0
  234. package/dist/src/evals/model-capture-plan.js.map +1 -0
  235. package/dist/src/evals/model-capture-runtime.js +114 -0
  236. package/dist/src/evals/model-capture-runtime.js.map +1 -0
  237. package/dist/src/evals/model-proxy-runtime-state.js +53 -0
  238. package/dist/src/evals/model-proxy-runtime-state.js.map +1 -0
  239. package/dist/src/evals/planned-execution-support.js +54 -0
  240. package/dist/src/evals/planned-execution-support.js.map +1 -0
  241. package/dist/src/evals/planned-execution.js +461 -0
  242. package/dist/src/evals/planned-execution.js.map +1 -0
  243. package/dist/src/evals/planned-remote-execution.js +40 -0
  244. package/dist/src/evals/planned-remote-execution.js.map +1 -0
  245. package/dist/src/evals/planned-retry-lifecycle.js +186 -0
  246. package/dist/src/evals/planned-retry-lifecycle.js.map +1 -0
  247. package/dist/src/evals/prepared-harness.js +76 -0
  248. package/dist/src/evals/prepared-harness.js.map +1 -0
  249. package/dist/src/evals/progress.js +4 -4
  250. package/dist/src/evals/progress.js.map +1 -1
  251. package/dist/src/evals/records.js +20 -3
  252. package/dist/src/evals/records.js.map +1 -1
  253. package/dist/src/evals/recovery.js +192 -0
  254. package/dist/src/evals/recovery.js.map +1 -0
  255. package/dist/src/evals/remote-infrastructure-retry.js +99 -0
  256. package/dist/src/evals/remote-infrastructure-retry.js.map +1 -0
  257. package/dist/src/evals/request.js +14 -1
  258. package/dist/src/evals/request.js.map +1 -1
  259. package/dist/src/evals/rerun-inputs.js +89 -0
  260. package/dist/src/evals/rerun-inputs.js.map +1 -0
  261. package/dist/src/evals/rerun-slots.js +12 -3
  262. package/dist/src/evals/rerun-slots.js.map +1 -1
  263. package/dist/src/evals/rerun-types.js +49 -0
  264. package/dist/src/evals/rerun-types.js.map +1 -0
  265. package/dist/src/evals/rerun.js +113 -75
  266. package/dist/src/evals/rerun.js.map +1 -1
  267. package/dist/src/evals/result-helpers.js +65 -0
  268. package/dist/src/evals/result-helpers.js.map +1 -0
  269. package/dist/src/evals/resume-state.js +36 -0
  270. package/dist/src/evals/resume-state.js.map +1 -0
  271. package/dist/src/evals/service-types.js +2 -0
  272. package/dist/src/evals/service-types.js.map +1 -0
  273. package/dist/src/evals/service.js +327 -237
  274. package/dist/src/evals/service.js.map +1 -1
  275. package/dist/src/evals/task-resources.js +209 -0
  276. package/dist/src/evals/task-resources.js.map +1 -0
  277. package/dist/src/evals/trial-environment-evidence.js +87 -0
  278. package/dist/src/evals/trial-environment-evidence.js.map +1 -0
  279. package/dist/src/evals/trial-execution-evidence.js +13 -0
  280. package/dist/src/evals/trial-execution-evidence.js.map +1 -0
  281. package/dist/src/evals/trial-import-identity.js +31 -0
  282. package/dist/src/evals/trial-import-identity.js.map +1 -0
  283. package/dist/src/evals/trial-import.js +166 -168
  284. package/dist/src/evals/trial-import.js.map +1 -1
  285. package/dist/src/evals/trial-publication-recovery.js +104 -0
  286. package/dist/src/evals/trial-publication-recovery.js.map +1 -0
  287. package/dist/src/evals/trial-publication.js +25 -0
  288. package/dist/src/evals/trial-publication.js.map +1 -0
  289. package/dist/src/evals/verifier-diagnostics.js +235 -0
  290. package/dist/src/evals/verifier-diagnostics.js.map +1 -0
  291. package/dist/src/evals/work-item-artifacts.js +23 -0
  292. package/dist/src/evals/work-item-artifacts.js.map +1 -0
  293. package/dist/src/foundation/config.js +17 -0
  294. package/dist/src/foundation/config.js.map +1 -1
  295. package/dist/src/foundation/credential-redaction.js +137 -0
  296. package/dist/src/foundation/credential-redaction.js.map +1 -0
  297. package/dist/src/foundation/environment-image-references.js +59 -0
  298. package/dist/src/foundation/environment-image-references.js.map +1 -0
  299. package/dist/src/foundation/executable.js +45 -5
  300. package/dist/src/foundation/executable.js.map +1 -1
  301. package/dist/src/foundation/index.js +5 -2
  302. package/dist/src/foundation/index.js.map +1 -1
  303. package/dist/src/foundation/process-identity.js +69 -0
  304. package/dist/src/foundation/process-identity.js.map +1 -0
  305. package/dist/src/images/context.js +47 -0
  306. package/dist/src/images/context.js.map +1 -0
  307. package/dist/src/images/docker-buildkit.js +137 -0
  308. package/dist/src/images/docker-buildkit.js.map +1 -0
  309. package/dist/src/images/dockerfile.js +67 -0
  310. package/dist/src/images/dockerfile.js.map +1 -0
  311. package/dist/src/images/gc.js +261 -0
  312. package/dist/src/images/gc.js.map +1 -0
  313. package/dist/src/images/index.js +10 -0
  314. package/dist/src/images/index.js.map +1 -0
  315. package/dist/src/images/manifest.js +109 -0
  316. package/dist/src/images/manifest.js.map +1 -0
  317. package/dist/src/images/ownership.js +5 -0
  318. package/dist/src/images/ownership.js.map +1 -0
  319. package/dist/src/images/records.js +32 -0
  320. package/dist/src/images/records.js.map +1 -0
  321. package/dist/src/images/registry.js +154 -0
  322. package/dist/src/images/registry.js.map +1 -0
  323. package/dist/src/images/service.js +224 -0
  324. package/dist/src/images/service.js.map +1 -0
  325. package/dist/src/model-access/capture.js +255 -0
  326. package/dist/src/model-access/capture.js.map +1 -0
  327. package/dist/src/model-access/index.js +5 -0
  328. package/dist/src/model-access/index.js.map +1 -0
  329. package/dist/src/model-access/policy.js +49 -0
  330. package/dist/src/model-access/policy.js.map +1 -0
  331. package/dist/src/model-access/proxy.js +336 -0
  332. package/dist/src/model-access/proxy.js.map +1 -0
  333. package/dist/src/model-access/records.js +167 -0
  334. package/dist/src/model-access/records.js.map +1 -0
  335. package/dist/src/runs/bundle.js +450 -0
  336. package/dist/src/runs/bundle.js.map +1 -0
  337. package/dist/src/runs/events.js +12 -1
  338. package/dist/src/runs/events.js.map +1 -1
  339. package/dist/src/runs/executor.js +24 -17
  340. package/dist/src/runs/executor.js.map +1 -1
  341. package/dist/src/runs/index.js +3 -1
  342. package/dist/src/runs/index.js.map +1 -1
  343. package/dist/src/runs/manifest.js +5 -5
  344. package/dist/src/runs/manifest.js.map +1 -1
  345. package/dist/src/runs/request.js +6 -1
  346. package/dist/src/runs/request.js.map +1 -1
  347. package/dist/src/runs/training-candidate.js +161 -0
  348. package/dist/src/runs/training-candidate.js.map +1 -0
  349. package/dist/src/trajectories/provider-capture.js +15 -24
  350. package/dist/src/trajectories/provider-capture.js.map +1 -1
  351. package/dist/src/trajectories/providers/deepseek.js +3 -3
  352. package/dist/src/trajectories/providers/deepseek.js.map +1 -1
  353. package/dist/src/workers/index.js +3 -0
  354. package/dist/src/workers/index.js.map +1 -0
  355. package/dist/src/workers/remote-harbor-work-spec.js +97 -0
  356. package/dist/src/workers/remote-harbor-work-spec.js.map +1 -0
  357. package/dist/src/workers/remote-harbor-worker.js +220 -0
  358. package/dist/src/workers/remote-harbor-worker.js.map +1 -0
  359. package/docs/schemas/environment-build-index.schema.json +13 -0
  360. package/docs/schemas/environment-build-record.schema.json +25 -0
  361. package/docs/schemas/environment-image-manifest.schema.json +62 -0
  362. package/docs/schemas/eval-control.schema.json +36 -0
  363. package/docs/schemas/eval-execution-plan.schema.json +216 -0
  364. package/docs/schemas/eval-request.schema.json +2 -0
  365. package/docs/schemas/eval-rerun-result.schema.json +53 -0
  366. package/docs/schemas/eval-rerun-state.schema.json +61 -0
  367. package/docs/schemas/eval-rerun-submission.schema.json +58 -0
  368. package/docs/schemas/eval-result.schema.json +34 -1
  369. package/docs/schemas/eval-submission-input.schema.json +14 -0
  370. package/docs/schemas/eval-submission.schema.json +68 -0
  371. package/docs/schemas/eval-trial-publication.schema.json +39 -0
  372. package/docs/schemas/execution-evidence.schema.json +96 -0
  373. package/docs/schemas/execution-lease.schema.json +50 -0
  374. package/docs/schemas/execution-provider-status.schema.json +65 -0
  375. package/docs/schemas/execution-worker.schema.json +53 -0
  376. package/docs/schemas/harbor-process-exit.schema.json +14 -0
  377. package/docs/schemas/interaction-capture-ref.schema.json +45 -0
  378. package/docs/schemas/local-provider-execution.schema.json +33 -0
  379. package/docs/schemas/model-interaction.schema.json +41 -0
  380. package/docs/schemas/remote-result-envelope.schema.json +32 -0
  381. package/docs/schemas/remote-tree-envelope.schema.json +25 -0
  382. package/docs/schemas/remote-work-offer.schema.json +58 -0
  383. package/docs/schemas/remote-work-receipt.schema.json +50 -0
  384. package/docs/schemas/remote-worker-artifact.schema.json +17 -0
  385. package/docs/schemas/remote-worker-event.schema.json +18 -0
  386. package/docs/schemas/remote-worker-heartbeat.schema.json +37 -0
  387. package/docs/schemas/remote-worker-registration.schema.json +52 -0
  388. package/docs/schemas/result-bundle-index.schema.json +133 -0
  389. package/docs/schemas/run-environment-images.schema.json +39 -0
  390. package/docs/schemas/training-data-candidate.schema.json +46 -0
  391. package/integrations/harbor/hitch_harbor_agent.py +300 -547
  392. package/integrations/harbor/hitch_harbor_environment.py +331 -0
  393. package/integrations/harbor/hitch_harbor_task_resources.py +275 -0
  394. package/integrations/harbor/hitch_harbor_verifier.py +337 -0
  395. package/package.json +16 -2
@@ -2,17 +2,15 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import asyncio
6
- import errno
7
5
  import hashlib
8
6
  import json
9
- import os
10
7
  import re
11
8
  import shlex
12
- import shutil
13
9
  import stat as stat_module
14
10
  import tempfile
11
+ import time
15
12
  import uuid
13
+ from urllib.parse import urlparse
16
14
  from datetime import datetime, timezone
17
15
  from pathlib import Path, PurePosixPath
18
16
  from typing import Any
@@ -22,12 +20,7 @@ from harbor.environments.base import BaseEnvironment, ExecResult
22
20
  from harbor.models.agent.context import AgentContext
23
21
 
24
22
  CONTROLLER_RUNTIME_MANIFEST_VERSION = "2"
25
- LOCAL_GIT_TRANSPORT_MANIFEST_VERSION = "1"
26
- HARNESS_ARTIFACT_MANIFEST_VERSION = "1"
27
- LOCAL_GIT_TRANSPORT_MAX_BYTES = 512 * 1024 * 1024
28
- LOCAL_GIT_REMOTE_ROOT = "/opt/hitch-local-source"
29
23
  HARNESS_ARTIFACT_REMOTE_ROOT = "/opt/hitch-harness-artifact"
30
- HITCH_CONTAINER_STATE_ROOT = "/tmp/hitch-state"
31
24
  HITCH_BRIDGE_ERROR_LOG = "/logs/agent/hitch-bridge-error.json"
32
25
  HITCH_DIAGNOSTIC_MAX_BYTES = 8 * 1024
33
26
  HITCH_BRIDGE_ERROR_MAX_BYTES = 64 * 1024
@@ -56,16 +49,22 @@ class HitchHarborAgent(BaseAgent):
56
49
  candidate_id: str = "candidate-1",
57
50
  controller_runtime_id: str | None = None,
58
51
  harness_artifact: dict[str, Any] | None = None,
52
+ node_version: str = "v22.23.0",
53
+ # Legacy parameters are accepted only so old persisted jobs fail at
54
+ # the explicit artifact handoff boundary instead of leaking unknown
55
+ # kwargs into Harbor's BaseAgent. New jobs pass neither value.
59
56
  harness_artifact_cache_dir: str | None = None,
60
57
  local_source_transport: dict[str, Any] | None = None,
61
58
  hitch_timeout_ms: int = 900_000,
62
59
  agent_args: list[str] | None = None,
63
- workdir: str = "/app",
60
+ credential_names: list[str] | None = None,
61
+ workdir: str | None = None,
64
62
  eval_id: str | None = None,
65
63
  benchmark_id: str | None = None,
66
64
  benchmark_revision: str | None = None,
67
65
  verifier_identity: str | None = None,
68
66
  logical_attempt: int | None = None,
67
+ model_capture: dict[str, Any] | None = None,
69
68
  **kwargs: Any,
70
69
  ) -> None:
71
70
  super().__init__(logs_dir=logs_dir, **kwargs)
@@ -74,12 +73,27 @@ class HitchHarborAgent(BaseAgent):
74
73
  self.hitch_runtime_dir = Path(hitch_runtime_dir)
75
74
  self.controller_runtime_id = controller_runtime_id
76
75
  self.harness_artifact = dict(harness_artifact) if harness_artifact else None
77
- self.harness_artifact_cache_dir = Path(harness_artifact_cache_dir) if harness_artifact_cache_dir else None
78
- self.local_source_transport = dict(local_source_transport) if local_source_transport else None
76
+ if harness_artifact_cache_dir is not None:
77
+ raise ValueError("trial-side harness artifact caches are no longer supported")
78
+ if not isinstance(node_version, str) or re.fullmatch(r"v\d+\.\d+\.\d+", node_version) is None:
79
+ raise ValueError("node_version must be an exact stable Node.js version")
80
+ self.required_node_version = node_version
81
+ if local_source_transport is not None:
82
+ raise ValueError("trial-side local source transports are no longer supported")
79
83
  self.candidate_id = candidate_id
80
84
  self.hitch_timeout_ms = int(hitch_timeout_ms)
81
85
  self.agent_args = list(agent_args or [])
82
- self.workdir = workdir
86
+ raw_credential_names = list(credential_names or [])
87
+ if (
88
+ any(not isinstance(name, str) or re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name) is None for name in raw_credential_names)
89
+ or len(set(raw_credential_names)) != len(raw_credential_names)
90
+ ):
91
+ raise ValueError("credential_names must contain unique environment variable names")
92
+ self.credential_names = sorted(raw_credential_names)
93
+ if workdir is not None and (not isinstance(workdir, str) or not workdir.strip()):
94
+ raise ValueError("workdir must be a non-empty string when provided")
95
+ self.workdir = workdir.strip() if isinstance(workdir, str) else None
96
+ self._workdir_source: str | None = "agent_config" if self.workdir is not None else None
83
97
  self.eval_id = eval_id
84
98
  self.benchmark_id = benchmark_id
85
99
  self.benchmark_revision = benchmark_revision
@@ -87,13 +101,13 @@ class HitchHarborAgent(BaseAgent):
87
101
  if logical_attempt is not None and (isinstance(logical_attempt, bool) or not isinstance(logical_attempt, int) or logical_attempt < 1):
88
102
  raise ValueError("logical_attempt must be a positive integer")
89
103
  self.logical_attempt = logical_attempt
104
+ self.model_capture = _validate_model_capture(model_capture)
90
105
  self._hitch_version: str | None = None
91
106
  self._entrypoint: str | None = None
92
107
  self._artifact_manifest: dict[str, Any] | None = None
93
108
  self._artifact_host_directory: Path | None = None
94
109
  self._artifact_uploaded = False
95
110
  self._artifact_transport_status: str | None = None
96
- self._local_manifest: dict[str, Any] | None = None
97
111
 
98
112
  @staticmethod
99
113
  def name() -> str:
@@ -103,6 +117,13 @@ class HitchHarborAgent(BaseAgent):
103
117
  return self._hitch_version
104
118
 
105
119
  async def setup(self, environment: BaseEnvironment) -> None:
120
+ started_ns = time.monotonic_ns()
121
+ try:
122
+ await self._setup(environment)
123
+ finally:
124
+ self._write_phase_timing("setup", started_ns)
125
+
126
+ async def _setup(self, environment: BaseEnvironment) -> None:
106
127
  if not self.hitch_runtime_dir.is_dir():
107
128
  raise RuntimeError(f"Hitch runtime directory does not exist: {self.hitch_runtime_dir}")
108
129
  # The manifest declares the CLI entrypoint (relative to the upload
@@ -126,25 +147,17 @@ class HitchHarborAgent(BaseAgent):
126
147
  # and the actual container upload, spec §4.6).
127
148
  self._verify_manifest_identity(manifest)
128
149
  self._verify_payload(manifest)
129
- if self.harness_artifact is not None:
130
- try:
131
- self._artifact_manifest = self._verify_harness_artifact_host()
132
- self._artifact_host_directory = Path(str(self.harness_artifact["directory"]))
133
- except Exception as error:
134
- raise RuntimeError(f"hitch-artifact-materialize: {error}") from error
135
- if self.harness_artifact_cache_dir is not None:
136
- try:
137
- self._verify_harness_artifact_cache_host()
138
- except Exception as error:
139
- raise RuntimeError(f"hitch-artifact-cache: {error}") from error
140
- if self.local_source_transport is not None:
141
- try:
142
- self._local_manifest = self._verify_local_source_host()
143
- except Exception as error:
144
- raise RuntimeError(f"hitch-local-source-materialize: {error}") from error
150
+ if self.harness_artifact is None:
151
+ raise RuntimeError("hitch-artifact-materialize: Harbor requires a dedicated-builder artifact")
152
+ try:
153
+ self._artifact_manifest = self._verify_harness_artifact_host()
154
+ self._artifact_host_directory = Path(str(self.harness_artifact["directory"]))
155
+ except Exception as error:
156
+ raise RuntimeError(f"hitch-artifact-materialize: {error}") from error
145
157
  payload_dir = self.hitch_runtime_dir / "payload"
146
158
  if not payload_dir.is_dir():
147
159
  raise RuntimeError(f"Hitch runtime bundle has no payload directory: {self.hitch_runtime_dir}")
160
+ await self._resolve_workdir(environment)
148
161
  # Upload the cached bundle's payload (package.json + dist/) as the
149
162
  # package root under /opt/hitch; the local cache path is host-side
150
163
  # bookkeeping and is not identity (spec §4.2).
@@ -152,46 +165,143 @@ class HitchHarborAgent(BaseAgent):
152
165
  await self._ensure_node(environment)
153
166
  platform = await self._container_platform(environment)
154
167
  node_version = await self._container_node_version(environment)
155
- cache_lock = None
156
- try:
157
- if self._artifact_manifest is not None and self._artifact_compatible(
158
- self._artifact_manifest, platform, node_version
159
- ):
160
- await self._upload_harness_artifact(environment, self._artifact_host_directory)
161
- self._artifact_transport_status = "uploaded"
162
- elif self.harness_artifact_cache_dir is not None:
163
- cache_lock = await self._acquire_artifact_cache_lock(platform, node_version)
164
- cached = self._find_cached_harness_artifact(platform, node_version)
165
- if cached is not None:
166
- self._artifact_manifest, self._artifact_host_directory = cached
167
- await self._release_artifact_cache_lock(cache_lock)
168
- cache_lock = None
169
- await self._upload_harness_artifact(environment, self._artifact_host_directory)
170
- self._artifact_transport_status = "host_cache_hit"
168
+ if self._artifact_manifest is None or not self._artifact_compatible(
169
+ self._artifact_manifest, platform, node_version
170
+ ):
171
+ raise RuntimeError(
172
+ "hitch-artifact-platform: dedicated-builder artifact is incompatible with "
173
+ f"trial runtime {platform}/{node_version}"
174
+ )
175
+ await self._upload_harness_artifact(environment, self._artifact_host_directory)
176
+ self._artifact_transport_status = "dedicated_builder_upload"
177
+ entry = self._remote_entry(entrypoint)
178
+ version = await self._exec(environment, f"{self._node_prefix()} node {entry} --version")
179
+ self._hitch_version = (version.stdout or "").strip() or None
180
+
181
+ async def _resolve_workdir(self, environment: BaseEnvironment) -> str:
182
+ """Resolve Harbor's effective task directory and prove it is usable.
183
+
184
+ Harbor environments already combine task-level ``[environment].workdir``
185
+ with the container image's ``WORKDIR``. Honor an explicit bridge
186
+ override, then the task configuration, then ask the running container
187
+ for its default cwd instead of assuming a global path such as /app.
188
+ """
189
+ candidate = self.workdir
190
+ source = self._workdir_source
191
+ task_config = getattr(environment, "task_env_config", None)
192
+ task_workdir = getattr(task_config, "workdir", None)
193
+ discovery: ExecResult | None = None
194
+ if candidate is None and isinstance(task_workdir, str) and task_workdir.strip():
195
+ candidate = task_workdir.strip()
196
+ source = "task_environment"
197
+ if candidate is None:
198
+ discovery = await environment.exec("pwd -P")
199
+ candidate = (discovery.stdout or "").strip()
200
+ source = "container_workdir"
201
+ if discovery.return_code != 0 or not candidate:
202
+ detail = self._exec_diagnostic(discovery)
203
+ await self._raise_workdir_error(
204
+ environment,
205
+ "Could not determine the Harbor task working directory from the container "
206
+ f"(exit={discovery.return_code}): {detail}",
207
+ source=source,
208
+ candidate=candidate or None,
209
+ probe=discovery,
210
+ )
211
+ if (
212
+ candidate is None
213
+ or not PurePosixPath(candidate).is_absolute()
214
+ or "\x00" in candidate
215
+ or "\n" in candidate
216
+ or "\r" in candidate
217
+ ):
218
+ await self._raise_workdir_error(
219
+ environment,
220
+ f"Harbor task working directory must be an absolute POSIX path; got {candidate!r} "
221
+ f"from {source or 'unknown'}",
222
+ source=source,
223
+ candidate=candidate,
224
+ probe=discovery,
225
+ )
171
226
 
172
- if self._local_manifest is not None and not self._artifact_uploaded:
173
- try:
174
- await self._upload_and_materialize_local_source(environment, self._local_manifest)
175
- except Exception as error:
176
- raise RuntimeError(f"hitch-local-source-materialize: {error}") from error
177
- entry = self._remote_entry(entrypoint)
178
- version = await self._exec(environment, f"{self._node_prefix()} node {entry} --version")
179
- self._hitch_version = (version.stdout or "").strip() or None
180
- if not self._artifact_uploaded:
181
- prepared = await self._prepare_harness_in_container(environment, entry)
182
- if cache_lock is not None:
183
- try:
184
- self._artifact_manifest, self._artifact_host_directory = await self._cache_container_artifact(
185
- environment, prepared, platform, node_version
186
- )
187
- self._artifact_transport_status = "host_cache_populated"
188
- except Exception:
189
- self._artifact_transport_status = "host_cache_write_failed"
190
- else:
191
- self._artifact_transport_status = "container_prepare"
192
- finally:
193
- if cache_lock is not None:
194
- await self._release_artifact_cache_lock(cache_lock)
227
+ exists = await environment.exec(f"test -d {shlex.quote(candidate)}", cwd="/")
228
+ if exists.return_code != 0:
229
+ detail = self._exec_diagnostic(exists)
230
+ await self._raise_workdir_error(
231
+ environment,
232
+ f"Harbor task working directory does not exist or is not a directory: {candidate} "
233
+ f"(source={source}, exit={exists.return_code}): {detail}",
234
+ source=source,
235
+ candidate=candidate,
236
+ probe=exists,
237
+ )
238
+
239
+ usable = await environment.exec("pwd -P", cwd=candidate)
240
+ resolved = (usable.stdout or "").strip()
241
+ if usable.return_code != 0 or not resolved or not PurePosixPath(resolved).is_absolute():
242
+ detail = self._exec_diagnostic(usable)
243
+ await self._raise_workdir_error(
244
+ environment,
245
+ f"Harbor task working directory exists but cannot be used to start the Hitch agent: {candidate} "
246
+ f"(source={source}, exit={usable.return_code}): {detail}",
247
+ source=source,
248
+ candidate=candidate,
249
+ probe=usable,
250
+ )
251
+ self.workdir = resolved
252
+ self._workdir_source = source
253
+ return resolved
254
+
255
+ async def _raise_workdir_error(
256
+ self,
257
+ environment: BaseEnvironment,
258
+ message: str,
259
+ *,
260
+ source: str | None,
261
+ candidate: str | None,
262
+ probe: ExecResult | None,
263
+ ) -> None:
264
+ evidence: dict[str, Any] = {
265
+ "schema_version": "1",
266
+ "code": "hitch_workdir_invalid",
267
+ "message": self._bounded_tail(message, 2048),
268
+ "recorded_at": datetime.now(timezone.utc).isoformat(),
269
+ "eval_id": self.eval_id,
270
+ "workdir": {
271
+ "source": source,
272
+ "candidate": candidate,
273
+ "return_code": probe.return_code if probe is not None else None,
274
+ "stdout_tail": self._bounded_tail(probe.stdout or "") if probe is not None else "",
275
+ "stderr_tail": self._bounded_tail(probe.stderr or "") if probe is not None else "",
276
+ },
277
+ }
278
+ await self._write_bridge_error(environment, evidence)
279
+ raise HitchBridgeError("hitch_workdir_invalid", message, evidence)
280
+
281
+ def _require_workdir(self) -> str:
282
+ if self.workdir is None:
283
+ raise RuntimeError("Hitch agent setup() must resolve the Harbor task working directory before run()")
284
+ return self.workdir
285
+
286
+ def _write_phase_timing(self, phase: str, started_ns: int) -> None:
287
+ if phase not in {"setup", "agent"}:
288
+ raise ValueError("invalid Harbor agent phase timing")
289
+ self.logs_dir.mkdir(parents=True, exist_ok=True)
290
+ target = self.logs_dir / "hitch-phase-timings.json"
291
+ value: dict[str, Any] = {"schema_version": "1", "phases": {}}
292
+ try:
293
+ existing = json.loads(target.read_text(encoding="utf-8")) if target.is_file() else None
294
+ if isinstance(existing, dict) and existing.get("schema_version") == "1" and isinstance(existing.get("phases"), dict):
295
+ value = existing
296
+ except (OSError, json.JSONDecodeError):
297
+ pass
298
+ value["phases"][phase] = {
299
+ "duration_ms": max(0, (time.monotonic_ns() - started_ns) // 1_000_000),
300
+ "completed_at": datetime.now(timezone.utc).isoformat(),
301
+ }
302
+ temporary = target.with_suffix(".json.tmp")
303
+ temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
304
+ temporary.replace(target)
195
305
 
196
306
  def _verify_harness_artifact_host(self) -> dict[str, Any]:
197
307
  """Pin host artifact metadata before Harbor copies the directory."""
@@ -247,6 +357,18 @@ class HitchHarborAgent(BaseAgent):
247
357
  raise RuntimeError("prepared artifact Node.js version is invalid")
248
358
  return manifest
249
359
 
360
+ @staticmethod
361
+ def _assert_regular_host_file(path: Path, label: str, maximum: int) -> Any:
362
+ try:
363
+ info = path.lstat()
364
+ except OSError as error:
365
+ raise RuntimeError(f"{label} is missing or unreadable") from error
366
+ if not stat_module.S_ISREG(info.st_mode):
367
+ raise RuntimeError(f"{label} must be a regular file")
368
+ if info.st_size > maximum:
369
+ raise RuntimeError(f"{label} exceeds the size limit ({maximum} bytes)")
370
+ return info
371
+
250
372
  async def _container_platform(self, environment: BaseEnvironment) -> str:
251
373
  result = await self._exec(
252
374
  environment,
@@ -281,448 +403,6 @@ class HitchHarborAgent(BaseAgent):
281
403
  await environment.upload_dir(directory, HARNESS_ARTIFACT_REMOTE_ROOT)
282
404
  self._artifact_uploaded = True
283
405
 
284
- async def _prepare_harness_in_container(
285
- self,
286
- environment: BaseEnvironment,
287
- entry: str,
288
- ) -> ExecResult:
289
- prepare = " ".join(
290
- [
291
- self._node_prefix(),
292
- f"HITCH_ROOT={HITCH_CONTAINER_STATE_ROOT}",
293
- *(["HITCH_HARBOR_INTERNAL=1"] if self._local_manifest is not None else []),
294
- f"node {entry} prepare",
295
- shlex.quote(self.harness_ref),
296
- *self._local_source_cli_args(),
297
- "--json",
298
- ]
299
- )
300
- return await self._exec(environment, prepare)
301
-
302
- def _verify_harness_artifact_cache_host(self) -> None:
303
- cache = self.harness_artifact_cache_dir
304
- if cache is None or not cache.is_absolute():
305
- raise RuntimeError("prepared artifact cache path must be absolute")
306
- try:
307
- info = cache.lstat()
308
- except OSError as error:
309
- raise RuntimeError("prepared artifact cache is missing or unreadable") from error
310
- if not stat_module.S_ISDIR(info.st_mode) or cache.is_symlink():
311
- raise RuntimeError("prepared artifact cache must be a regular directory")
312
- for name in ("artifacts", "locks", "tmp", "invalid"):
313
- child = cache / name
314
- child.mkdir(mode=0o700, exist_ok=True)
315
- child_info = child.lstat()
316
- if not stat_module.S_ISDIR(child_info.st_mode) or child.is_symlink():
317
- raise RuntimeError(f"prepared artifact cache {name} path must be a regular directory")
318
-
319
- def _artifact_cache_key(self, platform: str, node_version: str) -> str:
320
- payload = json.dumps(
321
- [
322
- self.revision_identity,
323
- self.harness_artifact.get("adapter_version") if self.harness_artifact else None,
324
- self.harness_artifact.get("recipe_version") if self.harness_artifact else None,
325
- platform,
326
- node_version,
327
- ],
328
- separators=(",", ":"),
329
- ).encode("utf-8")
330
- return hashlib.sha256(payload).hexdigest()
331
-
332
- async def _acquire_artifact_cache_lock(self, platform: str, node_version: str) -> Any:
333
- cache = self.harness_artifact_cache_dir
334
- if cache is None:
335
- raise RuntimeError("prepared artifact cache is unavailable")
336
- handle = (cache / "locks" / f"{self._artifact_cache_key(platform, node_version)}.lock").open("a+b")
337
- try:
338
- while True:
339
- try:
340
- self._try_lock_file(handle)
341
- return handle
342
- except OSError as error:
343
- if error.errno not in (errno.EACCES, errno.EAGAIN):
344
- raise
345
- await asyncio.sleep(0.1)
346
- except BaseException:
347
- handle.close()
348
- raise
349
-
350
- @staticmethod
351
- def _try_lock_file(handle: Any) -> None:
352
- if os.name == "nt":
353
- import msvcrt
354
- handle.seek(0)
355
- if not handle.read(1):
356
- handle.write(b"\0")
357
- handle.flush()
358
- handle.seek(0)
359
- msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
360
- return
361
- import fcntl
362
- fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
363
-
364
- @staticmethod
365
- async def _release_artifact_cache_lock(handle: Any) -> None:
366
- try:
367
- if os.name == "nt":
368
- import msvcrt
369
- handle.seek(0)
370
- msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
371
- else:
372
- import fcntl
373
- fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
374
- finally:
375
- handle.close()
376
-
377
- def _find_cached_harness_artifact(
378
- self,
379
- platform: str,
380
- node_version: str,
381
- ) -> tuple[dict[str, Any], Path] | None:
382
- cache = self.harness_artifact_cache_dir
383
- if cache is None:
384
- return None
385
- requested = self.harness_artifact or {}
386
- for directory in sorted((cache / "artifacts").iterdir(), key=lambda item: item.name):
387
- if not re.fullmatch(r"[0-9a-f]{64}", directory.name):
388
- continue
389
- try:
390
- manifest = json.loads((directory / "artifact.json").read_text(encoding="utf-8"))
391
- except (OSError, UnicodeError, json.JSONDecodeError):
392
- continue
393
- if not isinstance(manifest, dict) or (
394
- manifest.get("revision_identity") != self.revision_identity
395
- or manifest.get("adapter_version") != requested.get("adapter_version")
396
- or manifest.get("recipe_version") != requested.get("recipe_version")
397
- or not self._artifact_compatible(manifest, platform, node_version)
398
- ):
399
- continue
400
- try:
401
- return self._verify_cached_harness_artifact(directory, platform, node_version), directory
402
- except Exception:
403
- self._quarantine_cached_artifact(directory)
404
- return None
405
-
406
- async def _cache_container_artifact(
407
- self,
408
- environment: BaseEnvironment,
409
- prepared: ExecResult,
410
- platform: str,
411
- node_version: str,
412
- ) -> tuple[dict[str, Any], Path]:
413
- try:
414
- payload = json.loads(prepared.stdout or "")
415
- artifact = payload["artifact"]
416
- except (KeyError, TypeError, json.JSONDecodeError) as error:
417
- raise RuntimeError("container prepare returned invalid artifact metadata") from error
418
- artifact_id = artifact.get("artifact_id") if isinstance(artifact, dict) else None
419
- if not isinstance(artifact_id, str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", artifact_id):
420
- raise RuntimeError("container prepare returned an invalid artifact ID")
421
- if (
422
- artifact.get("revision_identity") != self.revision_identity
423
- or not self._artifact_compatible(artifact, platform, node_version)
424
- ):
425
- raise RuntimeError("container prepare returned an incompatible artifact")
426
- cache = self.harness_artifact_cache_dir
427
- if cache is None:
428
- raise RuntimeError("prepared artifact cache is unavailable")
429
- staging = cache / "tmp" / f"artifact-{uuid.uuid4().hex}"
430
- staging.mkdir(mode=0o700)
431
- try:
432
- remote = f'{HITCH_CONTAINER_STATE_ROOT}/store/artifacts/{artifact_id.removeprefix("sha256:")}'
433
- await environment.download_dir(remote, staging)
434
- manifest = self._verify_cached_harness_artifact(staging, platform, node_version)
435
- if manifest.get("artifact_id") != artifact_id:
436
- raise RuntimeError("downloaded artifact ID differs from container prepare output")
437
- destination = cache / "artifacts" / artifact_id.removeprefix("sha256:")
438
- if destination.exists():
439
- existing = self._verify_cached_harness_artifact(destination, platform, node_version)
440
- shutil.rmtree(staging)
441
- return existing, destination
442
- staging.rename(destination)
443
- return manifest, destination
444
- finally:
445
- if staging.exists():
446
- shutil.rmtree(staging, ignore_errors=True)
447
-
448
- def _verify_cached_harness_artifact(
449
- self,
450
- directory: Path,
451
- platform: str,
452
- node_version: str,
453
- ) -> dict[str, Any]:
454
- info = directory.lstat()
455
- if not stat_module.S_ISDIR(info.st_mode) or directory.is_symlink():
456
- raise RuntimeError("cached harness artifact is not a regular directory")
457
- artifact_file = directory / "artifact.json"
458
- self._assert_regular_host_file(artifact_file, "cached harness artifact manifest", 1024 * 1024)
459
- manifest = json.loads(artifact_file.read_text(encoding="utf-8"))
460
- if not isinstance(manifest, dict) or manifest.get("schema_version") != HARNESS_ARTIFACT_MANIFEST_VERSION:
461
- raise RuntimeError("cached harness artifact manifest schema is invalid")
462
- for field in ("artifact_id", "artifact_integrity", "entrypoint_integrity", "revision_identity"):
463
- if not re.fullmatch(r"sha256:[0-9a-f]{64}", str(manifest.get(field, ""))):
464
- raise RuntimeError(f"cached harness artifact {field} is invalid")
465
- if (
466
- manifest.get("artifact_id") != f"sha256:{directory.name}" and directory.parent.name == "artifacts"
467
- ):
468
- raise RuntimeError("cached harness artifact directory does not match its ID")
469
- if (
470
- manifest.get("revision_identity") != self.revision_identity
471
- or manifest.get("resolved_revision", {}).get("identity") != self.revision_identity
472
- or manifest.get("harness_id") != self.harness_ref.split("@", 1)[0]
473
- or manifest.get("adapter_version") != (self.harness_artifact or {}).get("adapter_version")
474
- or manifest.get("recipe_version") != (self.harness_artifact or {}).get("recipe_version")
475
- or not self._artifact_compatible(manifest, platform, node_version)
476
- or manifest.get("source_type") == "installed"
477
- ):
478
- raise RuntimeError("cached harness artifact identity is incompatible")
479
- entrypoint = manifest.get("entrypoint")
480
- if not isinstance(entrypoint, str) or not entrypoint or PurePosixPath(entrypoint).is_absolute():
481
- raise RuntimeError("cached harness artifact entrypoint is invalid")
482
- parts = PurePosixPath(entrypoint).parts
483
- if any(part in ("", ".", "..") for part in parts):
484
- raise RuntimeError("cached harness artifact entrypoint escapes its directory")
485
- executable = directory.joinpath(*parts)
486
- if not executable.is_file():
487
- raise RuntimeError("cached harness artifact entrypoint is missing")
488
- if self._sha256_file(executable) != manifest["entrypoint_integrity"]:
489
- raise RuntimeError("cached harness artifact entrypoint integrity mismatch")
490
- if self._artifact_directory_integrity(directory) != manifest["artifact_integrity"]:
491
- raise RuntimeError("cached harness artifact content integrity mismatch")
492
- return manifest
493
-
494
- @classmethod
495
- def _artifact_directory_integrity(cls, root: Path) -> str:
496
- digest = hashlib.sha256()
497
- resolved_root = root.resolve()
498
-
499
- def walk(directory: Path, relative: PurePosixPath, top_level: bool) -> None:
500
- for entry in sorted(os.scandir(directory), key=lambda item: item.name):
501
- if top_level and entry.name == "artifact.json":
502
- continue
503
- child_relative = relative / entry.name
504
- child = Path(entry.path)
505
- info = child.lstat()
506
- mode = info.st_mode & 0o7777
507
- encoded_relative = child_relative.as_posix()
508
- if stat_module.S_ISDIR(info.st_mode):
509
- digest.update(f"d\0{encoded_relative}\0{mode}\0".encode("utf-8"))
510
- walk(child, child_relative, False)
511
- elif stat_module.S_ISREG(info.st_mode):
512
- digest.update(f"f\0{encoded_relative}\0{mode}\0{info.st_size}\0".encode("utf-8"))
513
- with child.open("rb") as handle:
514
- while chunk := handle.read(1024 * 1024):
515
- digest.update(chunk)
516
- digest.update(b"\0")
517
- elif stat_module.S_ISLNK(info.st_mode):
518
- target = os.readlink(child)
519
- resolved_target = (child.parent / target).resolve()
520
- if resolved_target != resolved_root and resolved_root not in resolved_target.parents:
521
- raise RuntimeError("cached harness artifact symlink escapes its directory")
522
- digest.update(f"l\0{encoded_relative}\0{target}\0".encode("utf-8"))
523
- else:
524
- raise RuntimeError("cached harness artifact contains a special file")
525
-
526
- walk(root, PurePosixPath(), True)
527
- return "sha256:" + digest.hexdigest()
528
-
529
- def _quarantine_cached_artifact(self, directory: Path) -> None:
530
- cache = self.harness_artifact_cache_dir
531
- if cache is None or not directory.exists():
532
- return
533
- target = cache / "invalid" / f"{directory.name}-{uuid.uuid4().hex}"
534
- try:
535
- directory.rename(target)
536
- except OSError:
537
- pass
538
-
539
- def _verify_local_source_host(self) -> dict[str, Any]:
540
- """Validate the independent local-source handoff immediately before upload."""
541
- transport = self.local_source_transport or {}
542
- required = {
543
- "kind", "manifest_path", "payload_path", "locked_resolution_path", "harness_id",
544
- "resolution_identity", "commit", "tree", "payload_sha256", "payload_bytes",
545
- "object_count", "file_count",
546
- }
547
- if set(transport) != required:
548
- raise RuntimeError("local source transport metadata fields are invalid")
549
- manifest_path = Path(str(transport["manifest_path"]))
550
- payload_path = Path(str(transport["payload_path"]))
551
- resolution_path = Path(str(transport["locked_resolution_path"]))
552
- if not all(candidate.is_absolute() for candidate in (manifest_path, payload_path, resolution_path)):
553
- raise RuntimeError("local source transport host paths must be absolute")
554
- self._assert_regular_host_file(manifest_path, "local source manifest", 64 * 1024)
555
- payload_stat = self._assert_regular_host_file(payload_path, "local source payload", LOCAL_GIT_TRANSPORT_MAX_BYTES)
556
- self._assert_regular_host_file(resolution_path, "local source locked resolution", 1024 * 1024)
557
- try:
558
- manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
559
- resolution = json.loads(resolution_path.read_text(encoding="utf-8"))
560
- except (OSError, UnicodeError, json.JSONDecodeError) as error:
561
- raise RuntimeError(f"local source metadata is unreadable: {error}") from error
562
- self._validate_local_manifest(manifest)
563
- pinned = {
564
- "kind": manifest["kind"],
565
- "harness_id": manifest["harness_id"],
566
- "resolution_identity": manifest["resolution_identity"],
567
- "commit": manifest["commit"],
568
- "tree": manifest["tree"],
569
- "payload_sha256": manifest["payload_sha256"],
570
- "payload_bytes": manifest["payload_bytes"],
571
- "object_count": manifest["object_count"],
572
- "file_count": manifest["file_count"],
573
- }
574
- if any(transport.get(key) != value for key, value in pinned.items()):
575
- raise RuntimeError("local source transport metadata does not match its manifest")
576
- if manifest["resolution_identity"] != self.revision_identity:
577
- raise RuntimeError("local source resolution identity does not match the job-pinned identity")
578
- if self.harness_ref != f'{manifest["harness_id"]}@commit:{manifest["commit"]}':
579
- raise RuntimeError("local source commit does not match the job-pinned harness ref")
580
- if payload_stat.st_size != manifest["payload_bytes"]:
581
- raise RuntimeError("local source payload size does not match its manifest")
582
- actual_digest = self._sha256_file(payload_path)
583
- if actual_digest != manifest["payload_sha256"]:
584
- raise RuntimeError("local source payload digest does not match its manifest")
585
- if not isinstance(resolution, dict) or (
586
- resolution.get("harness_id") != manifest["harness_id"]
587
- or resolution.get("identity") != manifest["resolution_identity"]
588
- or resolution.get("revision", {}).get("commit") != manifest["commit"]
589
- or resolution.get("source", {}).get("type") != "git"
590
- or resolution.get("source", {}).get("registered") is not False
591
- ):
592
- raise RuntimeError("local source locked resolution does not match its manifest")
593
- return manifest
594
-
595
- @staticmethod
596
- def _assert_regular_host_file(path: Path, label: str, maximum: int) -> Any:
597
- try:
598
- info = path.lstat()
599
- except OSError as error:
600
- raise RuntimeError(f"{label} is missing or unreadable") from error
601
- if not stat_module.S_ISREG(info.st_mode):
602
- raise RuntimeError(f"{label} must be a regular file")
603
- if info.st_size > maximum:
604
- raise RuntimeError(f"{label} exceeds the size limit ({maximum} bytes)")
605
- return info
606
-
607
- @staticmethod
608
- def _sha256_file(path: Path) -> str:
609
- digest = hashlib.sha256()
610
- with path.open("rb") as handle:
611
- while chunk := handle.read(1024 * 1024):
612
- digest.update(chunk)
613
- return "sha256:" + digest.hexdigest()
614
-
615
- @staticmethod
616
- def _validate_local_manifest(manifest: Any) -> None:
617
- allowed = {
618
- "schema_version", "kind", "harness_id", "resolution_identity", "commit", "tree",
619
- "payload_sha256", "payload_bytes", "object_count", "file_count", "created_at",
620
- }
621
- if not isinstance(manifest, dict) or set(manifest) != allowed:
622
- raise RuntimeError("local source manifest fields are invalid")
623
- if manifest.get("schema_version") != LOCAL_GIT_TRANSPORT_MANIFEST_VERSION:
624
- raise RuntimeError("unsupported local source transport manifest schema")
625
- if manifest.get("kind") != "local-git-commit":
626
- raise RuntimeError("unsupported local source transport kind")
627
- if not isinstance(manifest.get("harness_id"), str) or not re.fullmatch(r"[a-z][a-z0-9-]*", manifest["harness_id"]):
628
- raise RuntimeError("local source manifest harness id is invalid")
629
- if not isinstance(manifest.get("resolution_identity"), str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", manifest["resolution_identity"]):
630
- raise RuntimeError("local source manifest resolution identity is invalid")
631
- commit = manifest.get("commit")
632
- if not isinstance(commit, str) or not re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", commit):
633
- raise RuntimeError("local source manifest commit is not a full lowercase OID")
634
- tree = manifest.get("tree")
635
- if not isinstance(tree, str) or not re.fullmatch(rf"[0-9a-f]{{{len(commit)}}}", tree):
636
- raise RuntimeError("local source manifest tree is invalid")
637
- if not isinstance(manifest.get("payload_sha256"), str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", manifest["payload_sha256"]):
638
- raise RuntimeError("local source manifest payload digest is invalid")
639
- for key, maximum in (
640
- ("payload_bytes", LOCAL_GIT_TRANSPORT_MAX_BYTES),
641
- ("object_count", 100_000),
642
- ("file_count", 50_000),
643
- ):
644
- value = manifest.get(key)
645
- if not isinstance(value, int) or isinstance(value, bool) or value < 0 or value > maximum:
646
- raise RuntimeError(f"local source manifest {key} is invalid")
647
- if not isinstance(manifest.get("created_at"), str) or not manifest["created_at"]:
648
- raise RuntimeError("local source manifest created_at is invalid")
649
- try:
650
- datetime.fromisoformat(manifest["created_at"].replace("Z", "+00:00"))
651
- except ValueError as error:
652
- raise RuntimeError("local source manifest created_at is invalid") from error
653
-
654
- async def _upload_and_materialize_local_source(
655
- self,
656
- environment: BaseEnvironment,
657
- manifest: dict[str, Any],
658
- ) -> None:
659
- transport = self.local_source_transport or {}
660
- await self._exec(environment, f"mkdir -p {LOCAL_GIT_REMOTE_ROOT} && chmod 700 {LOCAL_GIT_REMOTE_ROOT}")
661
- await environment.upload_file(Path(str(transport["manifest_path"])), f"{LOCAL_GIT_REMOTE_ROOT}/manifest.json")
662
- await environment.upload_file(Path(str(transport["payload_path"])), f"{LOCAL_GIT_REMOTE_ROOT}/payload.pack")
663
- await environment.upload_file(Path(str(transport["locked_resolution_path"])), f"{LOCAL_GIT_REMOTE_ROOT}/resolution.json")
664
- await self._exec(environment, f"chmod 600 {LOCAL_GIT_REMOTE_ROOT}/manifest.json {LOCAL_GIT_REMOTE_ROOT}/payload.pack {LOCAL_GIT_REMOTE_ROOT}/resolution.json")
665
- # Verify both the uploaded manifest fields and payload bytes inside the
666
- # trial before importing any Git object. All interpolated values passed
667
- # to the shell have already been restricted to digest/OID grammars.
668
- verifier = """
669
- const fs = require('node:fs');
670
- const crypto = require('node:crypto');
671
- const expected = JSON.parse(process.argv[1]);
672
- const manifest = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
673
- if (JSON.stringify(Object.keys(manifest).sort()) !== JSON.stringify(Object.keys(expected).sort())) throw new Error('manifest fields mismatch');
674
- for (const key of Object.keys(expected)) if (manifest[key] !== expected[key]) throw new Error(`manifest mismatch: ${key}`);
675
- if (fs.statSync(process.argv[3]).size !== expected.payload_bytes) throw new Error('payload size mismatch');
676
- (async () => {
677
- const hash = crypto.createHash('sha256');
678
- for await (const chunk of fs.createReadStream(process.argv[3])) hash.update(chunk);
679
- const digest = 'sha256:' + hash.digest('hex');
680
- if (digest !== expected.payload_sha256) throw new Error('payload digest mismatch');
681
- })().catch((error) => { console.error(error.message); process.exit(1); });
682
- """.strip()
683
- expected = {
684
- "schema_version": manifest["schema_version"],
685
- "kind": manifest["kind"],
686
- "harness_id": manifest["harness_id"],
687
- "resolution_identity": manifest["resolution_identity"],
688
- "commit": manifest["commit"],
689
- "tree": manifest["tree"],
690
- "payload_sha256": manifest["payload_sha256"],
691
- "payload_bytes": manifest["payload_bytes"],
692
- "object_count": manifest["object_count"],
693
- "file_count": manifest["file_count"],
694
- "created_at": manifest["created_at"],
695
- }
696
- verify_command = " ".join([
697
- self._node_prefix(),
698
- "node", "-e", shlex.quote(verifier),
699
- shlex.quote(json.dumps(expected, separators=(",", ":"))),
700
- f"{LOCAL_GIT_REMOTE_ROOT}/manifest.json",
701
- f"{LOCAL_GIT_REMOTE_ROOT}/payload.pack",
702
- ])
703
- await self._exec(environment, verify_command)
704
- commit = manifest["commit"]
705
- tree = manifest["tree"]
706
- materialize = f"""
707
- set -eu
708
- git init --bare {LOCAL_GIT_REMOTE_ROOT}/repo.git >/dev/null
709
- git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git index-pack --stdin < {LOCAL_GIT_REMOTE_ROOT}/payload.pack >/dev/null
710
- test "$(git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git rev-parse {commit}^{{commit}})" = "{commit}"
711
- test "$(git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git rev-parse {commit}^{{tree}})" = "{tree}"
712
- printf '%s\n' {commit} > {LOCAL_GIT_REMOTE_ROOT}/repo.git/shallow
713
- git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commit}
714
- """
715
- await self._exec(environment, materialize)
716
-
717
- def _local_source_cli_args(self) -> list[str]:
718
- if self._local_manifest is None or self._artifact_uploaded:
719
- return []
720
- return [
721
- "--internal-locked-resolution", f"{LOCAL_GIT_REMOTE_ROOT}/resolution.json",
722
- "--internal-local-git-manifest", f"{LOCAL_GIT_REMOTE_ROOT}/manifest.json",
723
- "--internal-local-git-source", f"{LOCAL_GIT_REMOTE_ROOT}/repo.git",
724
- ]
725
-
726
406
  def _artifact_cli_args(self) -> list[str]:
727
407
  if not self._artifact_uploaded or self._artifact_manifest is None:
728
408
  return []
@@ -832,8 +512,21 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
832
512
  instruction: str,
833
513
  environment: BaseEnvironment,
834
514
  context: AgentContext,
515
+ ) -> None:
516
+ started_ns = time.monotonic_ns()
517
+ try:
518
+ await self._run(instruction, environment, context)
519
+ finally:
520
+ self._write_phase_timing("agent", started_ns)
521
+
522
+ async def _run(
523
+ self,
524
+ instruction: str,
525
+ environment: BaseEnvironment,
526
+ context: AgentContext,
835
527
  ) -> None:
836
528
  self.logs_dir.mkdir(parents=True, exist_ok=True)
529
+ workdir = self._require_workdir()
837
530
  assigned_run_id = "run_" + uuid.uuid4().hex
838
531
  run_id = assigned_run_id
839
532
  trial_id, task_id, attempt = self._trial_identity()
@@ -896,20 +589,21 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
896
589
  if parent_temporary is not None:
897
590
  parent_temporary.unlink(missing_ok=True)
898
591
 
592
+ proxy_environment, proxy_health = await self._model_proxy_environment(environment, run_id)
899
593
  if self._entrypoint is None:
900
594
  raise RuntimeError("Hitch agent setup() must run before run() to resolve the runtime entrypoint")
901
595
  entry = self._remote_entry(self._entrypoint)
902
596
  arguments = [
903
597
  self._node_prefix(),
904
598
  "HITCH_ROOT=/tmp/hitch-state",
905
- *(["HITCH_HARBOR_INTERNAL=1"] if self._local_manifest is not None or self._artifact_uploaded or parent_payload is not None else []),
599
+ *proxy_environment,
600
+ *(["HITCH_HARBOR_INTERNAL=1"] if self._artifact_uploaded or parent_payload is not None else []),
906
601
  f"node {entry} run",
907
602
  "--harness",
908
603
  shlex.quote(self.harness_ref),
909
- *self._local_source_cli_args(),
910
604
  *self._artifact_cli_args(),
911
605
  "--cwd",
912
- shlex.quote(self.workdir),
606
+ shlex.quote(workdir),
913
607
  "--workspace-mode",
914
608
  "shared",
915
609
  "--prompt-file",
@@ -931,13 +625,15 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
931
625
  arguments.extend(["--model", shlex.quote(self.model_name)])
932
626
  for value in self.agent_args:
933
627
  arguments.extend(["--agent-arg", shlex.quote(value)])
628
+ for name in self.credential_names:
629
+ arguments.extend(["--internal-credential-name", shlex.quote(name)])
934
630
  command = (
935
631
  "set -o pipefail; "
936
632
  + " ".join(arguments)
937
633
  + " 2> >(tee /logs/agent/hitch-stderr.log >&2)"
938
634
  + " | tee /logs/agent/hitch-events.jsonl"
939
635
  )
940
- execution = await environment.exec(command, cwd=self.workdir)
636
+ execution = await environment.exec(command, cwd=workdir)
941
637
  events = self._events(execution.stdout or "")
942
638
  observed_run_id = next((
943
639
  value
@@ -989,14 +685,14 @@ mv "$stage_dir" "$target_dir"
989
685
  primary_message: str | None = None
990
686
  if execution.return_code != 0:
991
687
  primary_code = "hitch_process_failed"
992
- diagnostic = (execution.stderr or "").strip()
688
+ diagnostic = self._exec_diagnostic(execution)
993
689
  if hitch_result and isinstance(hitch_result.get("error"), dict):
994
690
  result_message = hitch_result["error"].get("message")
995
691
  if isinstance(result_message, str) and result_message.strip():
996
692
  diagnostic = result_message.strip()
997
693
  primary_message = (
998
694
  f"Hitch agent run failed with code {execution.return_code} "
999
- f"(run_id={run_id}, trial_id={trial_id}): {self._bounded_tail(diagnostic or 'no diagnostic output')}"
695
+ f"(run_id={run_id}, trial_id={trial_id}): {self._bounded_tail(diagnostic)}"
1000
696
  )
1001
697
  elif result_error_code is not None:
1002
698
  primary_code = result_error_code
@@ -1022,30 +718,23 @@ mv "$stage_dir" "$target_dir"
1022
718
  "controller_runtime_id": self.controller_runtime_id,
1023
719
  "hitch_run_id": run_id,
1024
720
  "hitch_run_bundle": "hitch-run-bundle",
721
+ "hitch_workdir": workdir,
722
+ "hitch_workdir_source": self._workdir_source,
1025
723
  "eval_id": self.eval_id,
1026
724
  "trial_id": trial_id,
1027
725
  "task_id": task_id,
1028
726
  "attempt": attempt,
727
+ "model_capture_health": proxy_health,
1029
728
  "hitch_status": hitch_result.get("status") if hitch_result else None,
1030
729
  "hitch_artifact_id": hitch_result.get("artifact_id") if hitch_result else None,
1031
730
  }
1032
- if self._local_manifest is not None:
1033
- context.metadata["local_source_transport"] = {
1034
- "kind": self._local_manifest["kind"],
1035
- "commit": self._local_manifest["commit"],
1036
- "tree": self._local_manifest["tree"],
1037
- "resolution_identity": self._local_manifest["resolution_identity"],
1038
- "payload_sha256": self._local_manifest["payload_sha256"],
1039
- "payload_bytes": self._local_manifest["payload_bytes"],
1040
- "status": "verified",
1041
- }
1042
731
  if self._artifact_manifest is not None:
1043
732
  context.metadata["harness_artifact_transport"] = {
1044
733
  "artifact_id": self._artifact_manifest["artifact_id"],
1045
734
  "artifact_integrity": self._artifact_manifest["artifact_integrity"],
1046
735
  "platform": self._artifact_manifest["platform"],
1047
736
  "node_version": self._artifact_manifest.get("toolchain", {}).get("node"),
1048
- "status": self._artifact_transport_status or "container_prepare",
737
+ "status": self._artifact_transport_status or "dedicated_builder_upload",
1049
738
  }
1050
739
  if primary_code is not None:
1051
740
  context.metadata["hitch_bridge_error_code"] = primary_code
@@ -1069,6 +758,32 @@ mv "$stage_dir" "$target_dir"
1069
758
  await self._write_bridge_error(environment, evidence)
1070
759
  raise HitchBridgeError(primary_code, primary_message or primary_code, evidence)
1071
760
 
761
+ async def _model_proxy_environment(
762
+ self,
763
+ environment: BaseEnvironment,
764
+ run_id: str,
765
+ ) -> tuple[list[str], str]:
766
+ if self.model_capture is None:
767
+ return [], "not-configured"
768
+ health = self.model_capture["health_url_template"].replace("{run_id}", run_id)
769
+ script = (
770
+ "fetch(process.argv[1],{signal:AbortSignal.timeout(5000)})"
771
+ ".then(r=>{if(!r.ok)throw new Error('status '+r.status)})"
772
+ ".catch(()=>{console.error('model proxy health check failed');process.exit(1)})"
773
+ )
774
+ probe = await environment.exec(
775
+ f"{self._node_prefix()} node -e {shlex.quote(script)} {shlex.quote(health)}"
776
+ )
777
+ if probe.return_code != 0:
778
+ if self.model_capture["required"]:
779
+ raise RuntimeError("hitch-model-proxy-health: required model proxy is unreachable")
780
+ return [], "degraded-unreachable"
781
+ base = self.model_capture["base_url_template"].replace("{run_id}", run_id)
782
+ return [
783
+ f"OPENAI_BASE_URL={shlex.quote(base.replace('{provider}', 'openai'))}",
784
+ f"ANTHROPIC_BASE_URL={shlex.quote(base.replace('{provider}', 'anthropic'))}",
785
+ ], "healthy"
786
+
1072
787
  @staticmethod
1073
788
  def _parse_hitch_result(
1074
789
  result: ExecResult,
@@ -1226,7 +941,8 @@ mv "$stage_dir" "$target_dir"
1226
941
  async def _write_bridge_error(environment: BaseEnvironment, evidence: dict[str, Any]) -> None:
1227
942
  payload = json.dumps(evidence, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
1228
943
  await environment.exec(
1229
- f"umask 077; printf '%s\\n' {shlex.quote(payload)} > {HITCH_BRIDGE_ERROR_LOG}"
944
+ f"umask 077; printf '%s\\n' {shlex.quote(payload)} > {HITCH_BRIDGE_ERROR_LOG}",
945
+ cwd="/",
1230
946
  )
1231
947
 
1232
948
  def _trial_identity(self) -> tuple[str, str, int]:
@@ -1276,6 +992,7 @@ mv "$stage_dir" "$target_dir"
1276
992
  return Path(handle.name)
1277
993
 
1278
994
  async def _workspace_digest(self, environment: BaseEnvironment) -> str:
995
+ workdir = self._require_workdir()
1279
996
  script = r"""
1280
997
  const fs = require('node:fs');
1281
998
  const path = require('node:path');
@@ -1296,8 +1013,8 @@ walk(process.argv[1]);
1296
1013
  process.stdout.write('sha256:' + hash.digest('hex'));
1297
1014
  """.strip()
1298
1015
  result = await environment.exec(
1299
- " ".join([self._node_prefix(), "node", "-e", shlex.quote(script), shlex.quote(self.workdir)]),
1300
- cwd=self.workdir,
1016
+ " ".join([self._node_prefix(), "node", "-e", shlex.quote(script), shlex.quote(workdir)]),
1017
+ cwd=workdir,
1301
1018
  )
1302
1019
  digest = (result.stdout or "").strip()
1303
1020
  if result.return_code == 0 and re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
@@ -1308,40 +1025,36 @@ process.stdout.write('sha256:' + hash.digest('hex'));
1308
1025
 
1309
1026
  async def _ensure_node(self, environment: BaseEnvironment) -> None:
1310
1027
  probe = await environment.exec(
1311
- "node -e 'process.exit(Number(process.versions.node.split(\".\")[0]) >= 22 ? 0 : 1)'"
1028
+ f"node -e 'process.exit(process.version === \"{self.required_node_version}\" ? 0 : 1)'"
1312
1029
  )
1313
- needs_git = "@commit:" in self.harness_ref
1314
- git_probe = await environment.exec("command -v git >/dev/null 2>&1")
1315
- if probe.return_code == 0 and (not needs_git or git_probe.return_code == 0):
1030
+ if probe.return_code == 0:
1316
1031
  return
1317
1032
  prerequisites = """
1318
1033
  set -eu
1319
- if command -v curl >/dev/null 2>&1 && command -v git >/dev/null 2>&1; then exit 0; fi
1034
+ if command -v curl >/dev/null 2>&1; then exit 0; fi
1320
1035
  if command -v apt-get >/dev/null 2>&1; then
1321
- apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates git
1036
+ apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates
1322
1037
  elif command -v apk >/dev/null 2>&1; then
1323
- apk add --no-cache curl ca-certificates git bash
1038
+ apk add --no-cache curl ca-certificates bash
1324
1039
  elif command -v dnf >/dev/null 2>&1; then
1325
- dnf install -y curl ca-certificates git
1040
+ dnf install -y curl ca-certificates
1326
1041
  elif command -v yum >/dev/null 2>&1; then
1327
- yum install -y curl ca-certificates git
1042
+ yum install -y curl ca-certificates
1328
1043
  else
1329
- echo 'Hitch requires Node.js 22+ and could not install curl in this task image' >&2
1044
+ echo 'Hitch could not install the pinned Node.js runtime in this task image' >&2
1330
1045
  exit 1
1331
1046
  fi
1332
1047
  """
1333
1048
  await self._exec(environment, prerequisites, user=0)
1334
- if probe.return_code == 0:
1335
- return
1336
- install = """
1049
+ install = f"""
1337
1050
  set -eu
1338
1051
  export NVM_DIR=/opt/hitch-node
1339
1052
  mkdir -p "$NVM_DIR"
1340
1053
  curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
1341
1054
  . "$NVM_DIR/nvm.sh"
1342
- nvm install 22
1343
- nvm alias default 22
1344
- node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)'
1055
+ nvm install {self.required_node_version.removeprefix("v")}
1056
+ nvm alias default {self.required_node_version.removeprefix("v")}
1057
+ node -e 'process.exit(process.version === "{self.required_node_version}" ? 0 : 1)'
1345
1058
  """
1346
1059
  await self._exec(environment, install, user=0)
1347
1060
 
@@ -1373,6 +1086,46 @@ node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)
1373
1086
  raise RuntimeError(f"container setup command failed ({result.return_code}): {diagnostic}")
1374
1087
  return result
1375
1088
 
1089
+ @staticmethod
1090
+ def _exec_diagnostic(result: ExecResult) -> str:
1091
+ for value in (result.stderr, result.stdout):
1092
+ if value and value.strip():
1093
+ return value.strip()
1094
+ return "no diagnostic output"
1095
+
1096
+
1097
+ def _validate_model_capture(value: dict[str, Any] | None) -> dict[str, Any] | None:
1098
+ if value is None:
1099
+ return None
1100
+ if not isinstance(value, dict) or set(value) != {
1101
+ "schema_version", "mode", "required", "topology", "base_url_template", "health_url_template"
1102
+ }:
1103
+ raise ValueError("model_capture fields are invalid")
1104
+ if (
1105
+ value.get("schema_version") != "1"
1106
+ or value.get("mode") not in {"proxy", "hybrid"}
1107
+ or not isinstance(value.get("required"), bool)
1108
+ or value.get("topology") != "host-side"
1109
+ ):
1110
+ raise ValueError("model_capture identity is invalid")
1111
+ for field, provider_count in (("base_url_template", 1), ("health_url_template", 0)):
1112
+ template = value.get(field)
1113
+ if (
1114
+ not isinstance(template, str)
1115
+ or not template
1116
+ or len(template) > 2048
1117
+ or any(character in template for character in ("\x00", "\r", "\n"))
1118
+ or template.count("{run_id}") != 1
1119
+ or template.count("{provider}") != provider_count
1120
+ ):
1121
+ raise ValueError(f"model_capture {field} is invalid")
1122
+ parsed = urlparse(
1123
+ template.replace("{run_id}", "run_" + "a" * 32).replace("{provider}", "openai")
1124
+ )
1125
+ if parsed.scheme not in {"http", "https"} or not parsed.hostname or parsed.username or parsed.password:
1126
+ raise ValueError(f"model_capture {field} URL is invalid")
1127
+ return dict(value)
1128
+
1376
1129
 
1377
1130
  def canonical_manifest_json(manifest: dict[str, Any]) -> str:
1378
1131
  """Canonically encode the runtime identity `{ schema_version, node_range,