agent-hitch 0.2.6 → 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 (386) 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 +53 -103
  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/index.js +4 -1
  55. package/dist/src/backends/index.js.map +1 -1
  56. package/dist/src/cli/commands/daemon.js +135 -8
  57. package/dist/src/cli/commands/daemon.js.map +1 -1
  58. package/dist/src/cli/commands/eval.js +214 -4
  59. package/dist/src/cli/commands/eval.js.map +1 -1
  60. package/dist/src/cli/commands/images.js +40 -0
  61. package/dist/src/cli/commands/images.js.map +1 -0
  62. package/dist/src/cli/commands/run.js +6 -3
  63. package/dist/src/cli/commands/run.js.map +1 -1
  64. package/dist/src/cli/commands/runs.js +25 -3
  65. package/dist/src/cli/commands/runs.js.map +1 -1
  66. package/dist/src/cli/commands/worker.js +79 -0
  67. package/dist/src/cli/commands/worker.js.map +1 -0
  68. package/dist/src/cli/main.js +4 -0
  69. package/dist/src/cli/main.js.map +1 -1
  70. package/dist/src/cli/output.js +71 -3
  71. package/dist/src/cli/output.js.map +1 -1
  72. package/dist/src/control-plane/build-admission.js +69 -0
  73. package/dist/src/control-plane/build-admission.js.map +1 -0
  74. package/dist/src/control-plane/build-records.js +6 -0
  75. package/dist/src/control-plane/build-records.js.map +1 -0
  76. package/dist/src/control-plane/collisions.js +40 -0
  77. package/dist/src/control-plane/collisions.js.map +1 -0
  78. package/dist/src/control-plane/eval-control-state.js +22 -0
  79. package/dist/src/control-plane/eval-control-state.js.map +1 -0
  80. package/dist/src/control-plane/eval-control-work.js +39 -0
  81. package/dist/src/control-plane/eval-control-work.js.map +1 -0
  82. package/dist/src/control-plane/eval-image-resolution.js +52 -0
  83. package/dist/src/control-plane/eval-image-resolution.js.map +1 -0
  84. package/dist/src/control-plane/eval-image-services.js +30 -0
  85. package/dist/src/control-plane/eval-image-services.js.map +1 -0
  86. package/dist/src/control-plane/eval-records.js +252 -0
  87. package/dist/src/control-plane/eval-records.js.map +1 -0
  88. package/dist/src/control-plane/eval-recovery.js +181 -0
  89. package/dist/src/control-plane/eval-recovery.js.map +1 -0
  90. package/dist/src/control-plane/eval-scheduler.js +447 -0
  91. package/dist/src/control-plane/eval-scheduler.js.map +1 -0
  92. package/dist/src/control-plane/index.js +20 -0
  93. package/dist/src/control-plane/index.js.map +1 -0
  94. package/dist/src/control-plane/local-worker.js +49 -0
  95. package/dist/src/control-plane/local-worker.js.map +1 -0
  96. package/dist/src/control-plane/model-capture-planning.js +12 -0
  97. package/dist/src/control-plane/model-capture-planning.js.map +1 -0
  98. package/dist/src/control-plane/remote-result-transport.js +172 -0
  99. package/dist/src/control-plane/remote-result-transport.js.map +1 -0
  100. package/dist/src/control-plane/remote-work-coordinator.js +332 -0
  101. package/dist/src/control-plane/remote-work-coordinator.js.map +1 -0
  102. package/dist/src/control-plane/remote-work-inputs.js +266 -0
  103. package/dist/src/control-plane/remote-work-inputs.js.map +1 -0
  104. package/dist/src/control-plane/remote-work-recovery.js +181 -0
  105. package/dist/src/control-plane/remote-work-recovery.js.map +1 -0
  106. package/dist/src/control-plane/remote-worker-artifacts.js +135 -0
  107. package/dist/src/control-plane/remote-worker-artifacts.js.map +1 -0
  108. package/dist/src/control-plane/remote-worker-client.js +202 -0
  109. package/dist/src/control-plane/remote-worker-client.js.map +1 -0
  110. package/dist/src/control-plane/remote-worker-configuration.js +34 -0
  111. package/dist/src/control-plane/remote-worker-configuration.js.map +1 -0
  112. package/dist/src/control-plane/remote-worker-credentials.js +58 -0
  113. package/dist/src/control-plane/remote-worker-credentials.js.map +1 -0
  114. package/dist/src/control-plane/remote-worker-protocol.js +510 -0
  115. package/dist/src/control-plane/remote-worker-protocol.js.map +1 -0
  116. package/dist/src/control-plane/remote-worker-runner.js +214 -0
  117. package/dist/src/control-plane/remote-worker-runner.js.map +1 -0
  118. package/dist/src/control-plane/remote-workers.js +377 -0
  119. package/dist/src/control-plane/remote-workers.js.map +1 -0
  120. package/dist/src/control-plane/rerun-scheduler.js +411 -0
  121. package/dist/src/control-plane/rerun-scheduler.js.map +1 -0
  122. package/dist/src/control-plane/rerun-submission.js +56 -0
  123. package/dist/src/control-plane/rerun-submission.js.map +1 -0
  124. package/dist/src/control-plane/resources.js +182 -0
  125. package/dist/src/control-plane/resources.js.map +1 -0
  126. package/dist/src/control-plane/scheduler-eval-entry.js +32 -0
  127. package/dist/src/control-plane/scheduler-eval-entry.js.map +1 -0
  128. package/dist/src/control-plane/synthetic-result.js +21 -0
  129. package/dist/src/control-plane/synthetic-result.js.map +1 -0
  130. package/dist/src/control-plane/work-admission.js +21 -0
  131. package/dist/src/control-plane/work-admission.js.map +1 -0
  132. package/dist/src/control-plane/work-dispatcher.js +229 -0
  133. package/dist/src/control-plane/work-dispatcher.js.map +1 -0
  134. package/dist/src/controller-runtime/hash.js +12 -1
  135. package/dist/src/controller-runtime/hash.js.map +1 -1
  136. package/dist/src/controller-runtime/index.js +1 -1
  137. package/dist/src/controller-runtime/index.js.map +1 -1
  138. package/dist/src/controller-runtime/store.js +15 -6
  139. package/dist/src/controller-runtime/store.js.map +1 -1
  140. package/dist/src/daemon/client.js.map +1 -1
  141. package/dist/src/daemon/event-stream.js +46 -0
  142. package/dist/src/daemon/event-stream.js.map +1 -0
  143. package/dist/src/daemon/health.js +66 -0
  144. package/dist/src/daemon/health.js.map +1 -0
  145. package/dist/src/daemon/launcher.js +28 -1
  146. package/dist/src/daemon/launcher.js.map +1 -1
  147. package/dist/src/daemon/logging.js +23 -0
  148. package/dist/src/daemon/logging.js.map +1 -0
  149. package/dist/src/daemon/scheduler.js +26 -5
  150. package/dist/src/daemon/scheduler.js.map +1 -1
  151. package/dist/src/daemon/server.js +255 -98
  152. package/dist/src/daemon/server.js.map +1 -1
  153. package/dist/src/daemon/telemetry.js +163 -0
  154. package/dist/src/daemon/telemetry.js.map +1 -0
  155. package/dist/src/daemon/worker-routes.js +219 -0
  156. package/dist/src/daemon/worker-routes.js.map +1 -0
  157. package/dist/src/domain/bundles.js +2 -0
  158. package/dist/src/domain/bundles.js.map +1 -0
  159. package/dist/src/domain/control-plane.js +2 -0
  160. package/dist/src/domain/control-plane.js.map +1 -0
  161. package/dist/src/domain/execution-evidence.js +2 -0
  162. package/dist/src/domain/execution-evidence.js.map +1 -0
  163. package/dist/src/domain/execution-plan.js +2 -0
  164. package/dist/src/domain/execution-plan.js.map +1 -0
  165. package/dist/src/domain/images.js +2 -0
  166. package/dist/src/domain/images.js.map +1 -0
  167. package/dist/src/domain/index.js +9 -0
  168. package/dist/src/domain/index.js.map +1 -1
  169. package/dist/src/domain/interactions.js +2 -0
  170. package/dist/src/domain/interactions.js.map +1 -0
  171. package/dist/src/domain/providers.js +2 -0
  172. package/dist/src/domain/providers.js.map +1 -0
  173. package/dist/src/domain/resources.js +2 -0
  174. package/dist/src/domain/resources.js.map +1 -0
  175. package/dist/src/domain/workers.js +2 -0
  176. package/dist/src/domain/workers.js.map +1 -0
  177. package/dist/src/evals/collect-only-rerun.js +182 -0
  178. package/dist/src/evals/collect-only-rerun.js.map +1 -0
  179. package/dist/src/evals/directory.js +30 -0
  180. package/dist/src/evals/directory.js.map +1 -0
  181. package/dist/src/evals/docker-engine-stats.js +89 -0
  182. package/dist/src/evals/docker-engine-stats.js.map +1 -0
  183. package/dist/src/evals/docker-ownership.js +51 -0
  184. package/dist/src/evals/docker-ownership.js.map +1 -0
  185. package/dist/src/evals/docker-reaper.js +144 -0
  186. package/dist/src/evals/docker-reaper.js.map +1 -0
  187. package/dist/src/evals/docker-resource-observer.js +207 -0
  188. package/dist/src/evals/docker-resource-observer.js.map +1 -0
  189. package/dist/src/evals/environment-image-planning.js +164 -0
  190. package/dist/src/evals/environment-image-planning.js.map +1 -0
  191. package/dist/src/evals/eval-artifact-planning.js +56 -0
  192. package/dist/src/evals/eval-artifact-planning.js.map +1 -0
  193. package/dist/src/evals/eval-finalization.js +10 -0
  194. package/dist/src/evals/eval-finalization.js.map +1 -0
  195. package/dist/src/evals/eval-lifecycle-events.js +16 -0
  196. package/dist/src/evals/eval-lifecycle-events.js.map +1 -0
  197. package/dist/src/evals/events.js +53 -5
  198. package/dist/src/evals/events.js.map +1 -1
  199. package/dist/src/evals/execution-evidence.js +116 -0
  200. package/dist/src/evals/execution-evidence.js.map +1 -0
  201. package/dist/src/evals/execution-leases.js +342 -0
  202. package/dist/src/evals/execution-leases.js.map +1 -0
  203. package/dist/src/evals/execution-plan-artifacts.js +72 -0
  204. package/dist/src/evals/execution-plan-artifacts.js.map +1 -0
  205. package/dist/src/evals/execution-plan-images.js +69 -0
  206. package/dist/src/evals/execution-plan-images.js.map +1 -0
  207. package/dist/src/evals/execution-plan-resources.js +174 -0
  208. package/dist/src/evals/execution-plan-resources.js.map +1 -0
  209. package/dist/src/evals/execution-plan.js +415 -0
  210. package/dist/src/evals/execution-plan.js.map +1 -0
  211. package/dist/src/evals/harbor-artifact-builder-image.js +99 -0
  212. package/dist/src/evals/harbor-artifact-builder-image.js.map +1 -0
  213. package/dist/src/evals/harbor-artifact-builder.js +354 -0
  214. package/dist/src/evals/harbor-artifact-builder.js.map +1 -0
  215. package/dist/src/evals/harbor-bridge-error.js +18 -3
  216. package/dist/src/evals/harbor-bridge-error.js.map +1 -1
  217. package/dist/src/evals/harbor-phase-timings.js +60 -0
  218. package/dist/src/evals/harbor-phase-timings.js.map +1 -0
  219. package/dist/src/evals/index.js +25 -2
  220. package/dist/src/evals/index.js.map +1 -1
  221. package/dist/src/evals/infrastructure-retry.js +100 -63
  222. package/dist/src/evals/infrastructure-retry.js.map +1 -1
  223. package/dist/src/evals/interaction-capture-import.js +49 -0
  224. package/dist/src/evals/interaction-capture-import.js.map +1 -0
  225. package/dist/src/evals/local-docker-provider.js +372 -0
  226. package/dist/src/evals/local-docker-provider.js.map +1 -0
  227. package/dist/src/evals/local-eval-planning.js +80 -0
  228. package/dist/src/evals/local-eval-planning.js.map +1 -0
  229. package/dist/src/evals/model-capture-plan.js +22 -0
  230. package/dist/src/evals/model-capture-plan.js.map +1 -0
  231. package/dist/src/evals/model-capture-runtime.js +114 -0
  232. package/dist/src/evals/model-capture-runtime.js.map +1 -0
  233. package/dist/src/evals/model-proxy-runtime-state.js +53 -0
  234. package/dist/src/evals/model-proxy-runtime-state.js.map +1 -0
  235. package/dist/src/evals/planned-execution-support.js +54 -0
  236. package/dist/src/evals/planned-execution-support.js.map +1 -0
  237. package/dist/src/evals/planned-execution.js +461 -0
  238. package/dist/src/evals/planned-execution.js.map +1 -0
  239. package/dist/src/evals/planned-remote-execution.js +40 -0
  240. package/dist/src/evals/planned-remote-execution.js.map +1 -0
  241. package/dist/src/evals/planned-retry-lifecycle.js +186 -0
  242. package/dist/src/evals/planned-retry-lifecycle.js.map +1 -0
  243. package/dist/src/evals/prepared-harness.js +76 -0
  244. package/dist/src/evals/prepared-harness.js.map +1 -0
  245. package/dist/src/evals/progress.js +4 -4
  246. package/dist/src/evals/progress.js.map +1 -1
  247. package/dist/src/evals/records.js +20 -3
  248. package/dist/src/evals/records.js.map +1 -1
  249. package/dist/src/evals/recovery.js +192 -0
  250. package/dist/src/evals/recovery.js.map +1 -0
  251. package/dist/src/evals/remote-infrastructure-retry.js +99 -0
  252. package/dist/src/evals/remote-infrastructure-retry.js.map +1 -0
  253. package/dist/src/evals/rerun-inputs.js +89 -0
  254. package/dist/src/evals/rerun-inputs.js.map +1 -0
  255. package/dist/src/evals/rerun-slots.js +4 -3
  256. package/dist/src/evals/rerun-slots.js.map +1 -1
  257. package/dist/src/evals/rerun-types.js +49 -0
  258. package/dist/src/evals/rerun-types.js.map +1 -0
  259. package/dist/src/evals/rerun.js +113 -75
  260. package/dist/src/evals/rerun.js.map +1 -1
  261. package/dist/src/evals/result-helpers.js +65 -0
  262. package/dist/src/evals/result-helpers.js.map +1 -0
  263. package/dist/src/evals/resume-state.js +36 -0
  264. package/dist/src/evals/resume-state.js.map +1 -0
  265. package/dist/src/evals/service-types.js +2 -0
  266. package/dist/src/evals/service-types.js.map +1 -0
  267. package/dist/src/evals/service.js +292 -257
  268. package/dist/src/evals/service.js.map +1 -1
  269. package/dist/src/evals/task-resources.js +209 -0
  270. package/dist/src/evals/task-resources.js.map +1 -0
  271. package/dist/src/evals/trial-environment-evidence.js +87 -0
  272. package/dist/src/evals/trial-environment-evidence.js.map +1 -0
  273. package/dist/src/evals/trial-execution-evidence.js +13 -0
  274. package/dist/src/evals/trial-execution-evidence.js.map +1 -0
  275. package/dist/src/evals/trial-import-identity.js +31 -0
  276. package/dist/src/evals/trial-import-identity.js.map +1 -0
  277. package/dist/src/evals/trial-import.js +143 -145
  278. package/dist/src/evals/trial-import.js.map +1 -1
  279. package/dist/src/evals/trial-publication-recovery.js +104 -0
  280. package/dist/src/evals/trial-publication-recovery.js.map +1 -0
  281. package/dist/src/evals/trial-publication.js +25 -0
  282. package/dist/src/evals/trial-publication.js.map +1 -0
  283. package/dist/src/evals/work-item-artifacts.js +23 -0
  284. package/dist/src/evals/work-item-artifacts.js.map +1 -0
  285. package/dist/src/foundation/config.js +17 -0
  286. package/dist/src/foundation/config.js.map +1 -1
  287. package/dist/src/foundation/credential-redaction.js +137 -0
  288. package/dist/src/foundation/credential-redaction.js.map +1 -0
  289. package/dist/src/foundation/environment-image-references.js +59 -0
  290. package/dist/src/foundation/environment-image-references.js.map +1 -0
  291. package/dist/src/foundation/executable.js +45 -5
  292. package/dist/src/foundation/executable.js.map +1 -1
  293. package/dist/src/foundation/index.js +5 -2
  294. package/dist/src/foundation/index.js.map +1 -1
  295. package/dist/src/foundation/process-identity.js +69 -0
  296. package/dist/src/foundation/process-identity.js.map +1 -0
  297. package/dist/src/images/context.js +47 -0
  298. package/dist/src/images/context.js.map +1 -0
  299. package/dist/src/images/docker-buildkit.js +137 -0
  300. package/dist/src/images/docker-buildkit.js.map +1 -0
  301. package/dist/src/images/dockerfile.js +67 -0
  302. package/dist/src/images/dockerfile.js.map +1 -0
  303. package/dist/src/images/gc.js +261 -0
  304. package/dist/src/images/gc.js.map +1 -0
  305. package/dist/src/images/index.js +10 -0
  306. package/dist/src/images/index.js.map +1 -0
  307. package/dist/src/images/manifest.js +109 -0
  308. package/dist/src/images/manifest.js.map +1 -0
  309. package/dist/src/images/ownership.js +5 -0
  310. package/dist/src/images/ownership.js.map +1 -0
  311. package/dist/src/images/records.js +32 -0
  312. package/dist/src/images/records.js.map +1 -0
  313. package/dist/src/images/registry.js +154 -0
  314. package/dist/src/images/registry.js.map +1 -0
  315. package/dist/src/images/service.js +224 -0
  316. package/dist/src/images/service.js.map +1 -0
  317. package/dist/src/model-access/capture.js +255 -0
  318. package/dist/src/model-access/capture.js.map +1 -0
  319. package/dist/src/model-access/index.js +5 -0
  320. package/dist/src/model-access/index.js.map +1 -0
  321. package/dist/src/model-access/policy.js +49 -0
  322. package/dist/src/model-access/policy.js.map +1 -0
  323. package/dist/src/model-access/proxy.js +336 -0
  324. package/dist/src/model-access/proxy.js.map +1 -0
  325. package/dist/src/model-access/records.js +167 -0
  326. package/dist/src/model-access/records.js.map +1 -0
  327. package/dist/src/runs/bundle.js +450 -0
  328. package/dist/src/runs/bundle.js.map +1 -0
  329. package/dist/src/runs/events.js +12 -1
  330. package/dist/src/runs/events.js.map +1 -1
  331. package/dist/src/runs/executor.js +24 -17
  332. package/dist/src/runs/executor.js.map +1 -1
  333. package/dist/src/runs/index.js +3 -1
  334. package/dist/src/runs/index.js.map +1 -1
  335. package/dist/src/runs/manifest.js +5 -5
  336. package/dist/src/runs/manifest.js.map +1 -1
  337. package/dist/src/runs/request.js +6 -1
  338. package/dist/src/runs/request.js.map +1 -1
  339. package/dist/src/runs/training-candidate.js +161 -0
  340. package/dist/src/runs/training-candidate.js.map +1 -0
  341. package/dist/src/trajectories/provider-capture.js +15 -24
  342. package/dist/src/trajectories/provider-capture.js.map +1 -1
  343. package/dist/src/trajectories/providers/deepseek.js +3 -3
  344. package/dist/src/trajectories/providers/deepseek.js.map +1 -1
  345. package/dist/src/workers/index.js +3 -0
  346. package/dist/src/workers/index.js.map +1 -0
  347. package/dist/src/workers/remote-harbor-work-spec.js +97 -0
  348. package/dist/src/workers/remote-harbor-work-spec.js.map +1 -0
  349. package/dist/src/workers/remote-harbor-worker.js +220 -0
  350. package/dist/src/workers/remote-harbor-worker.js.map +1 -0
  351. package/docs/schemas/environment-build-index.schema.json +13 -0
  352. package/docs/schemas/environment-build-record.schema.json +25 -0
  353. package/docs/schemas/environment-image-manifest.schema.json +62 -0
  354. package/docs/schemas/eval-control.schema.json +36 -0
  355. package/docs/schemas/eval-execution-plan.schema.json +216 -0
  356. package/docs/schemas/eval-rerun-result.schema.json +53 -0
  357. package/docs/schemas/eval-rerun-state.schema.json +61 -0
  358. package/docs/schemas/eval-rerun-submission.schema.json +58 -0
  359. package/docs/schemas/eval-result.schema.json +34 -1
  360. package/docs/schemas/eval-submission-input.schema.json +14 -0
  361. package/docs/schemas/eval-submission.schema.json +68 -0
  362. package/docs/schemas/eval-trial-publication.schema.json +39 -0
  363. package/docs/schemas/execution-evidence.schema.json +96 -0
  364. package/docs/schemas/execution-lease.schema.json +50 -0
  365. package/docs/schemas/execution-provider-status.schema.json +65 -0
  366. package/docs/schemas/execution-worker.schema.json +53 -0
  367. package/docs/schemas/harbor-process-exit.schema.json +14 -0
  368. package/docs/schemas/interaction-capture-ref.schema.json +45 -0
  369. package/docs/schemas/local-provider-execution.schema.json +33 -0
  370. package/docs/schemas/model-interaction.schema.json +41 -0
  371. package/docs/schemas/remote-result-envelope.schema.json +32 -0
  372. package/docs/schemas/remote-tree-envelope.schema.json +25 -0
  373. package/docs/schemas/remote-work-offer.schema.json +58 -0
  374. package/docs/schemas/remote-work-receipt.schema.json +50 -0
  375. package/docs/schemas/remote-worker-artifact.schema.json +17 -0
  376. package/docs/schemas/remote-worker-event.schema.json +18 -0
  377. package/docs/schemas/remote-worker-heartbeat.schema.json +37 -0
  378. package/docs/schemas/remote-worker-registration.schema.json +52 -0
  379. package/docs/schemas/result-bundle-index.schema.json +133 -0
  380. package/docs/schemas/run-environment-images.schema.json +39 -0
  381. package/docs/schemas/training-data-candidate.schema.json +46 -0
  382. package/integrations/harbor/hitch_harbor_agent.py +171 -539
  383. package/integrations/harbor/hitch_harbor_environment.py +331 -0
  384. package/integrations/harbor/hitch_harbor_task_resources.py +275 -0
  385. package/integrations/harbor/hitch_harbor_verifier.py +23 -0
  386. package/package.json +14 -1
@@ -0,0 +1,331 @@
1
+ """Harbor Docker environment that stamps lease ownership on Compose resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any, Mapping
10
+
11
+ import yaml
12
+ from harbor.constants import MAIN_SERVICE_NAME
13
+ from harbor.environments.docker.docker import DockerEnvironment
14
+
15
+ _LABEL_ROOT = "io.hitch.root-id"
16
+ _LABEL_PROVIDER = "io.hitch.provider"
17
+ _LABEL_EVAL = "io.hitch.eval-id"
18
+ _LABEL_WORK = "io.hitch.work-id"
19
+ _LABEL_LEASE = "io.hitch.lease-id"
20
+ _LABEL_EPOCH = "io.hitch.lease-epoch"
21
+ _LABEL_TASK = "io.hitch.task-id"
22
+ _REQUIRED_LABELS = {
23
+ _LABEL_ROOT,
24
+ _LABEL_PROVIDER,
25
+ _LABEL_EVAL,
26
+ _LABEL_WORK,
27
+ _LABEL_LEASE,
28
+ _LABEL_EPOCH,
29
+ }
30
+ _ALLOWED_LABELS = _REQUIRED_LABELS | {_LABEL_TASK}
31
+ _COMPOSE_RESET_MARKER = "__HITCH_COMPOSE_RESET_NULL__"
32
+ _GPU_OVERRIDE_PREFIX = "__HITCH_GPU_OVERRIDE_"
33
+
34
+
35
+ class HitchHarborDockerEnvironment(DockerEnvironment):
36
+ """Use Harbor's Docker semantics with a final ownership-label overlay."""
37
+
38
+ def __init__(
39
+ self,
40
+ *args: Any,
41
+ hitch_ownership_labels: Mapping[str, str] | None = None,
42
+ hitch_service_resource_limits: Mapping[str, Mapping[str, int]] | None = None,
43
+ hitch_resolved_images: Mapping[str, str] | None = None,
44
+ hitch_prebuilt_task_image: str | None = None,
45
+ hitch_model_proxy_host_gateway: bool = False,
46
+ hitch_main_gpu_count: int = 0,
47
+ **kwargs: Any,
48
+ ) -> None:
49
+ self._hitch_ownership_labels = _validate_labels(hitch_ownership_labels)
50
+ self._hitch_service_resource_limits = _validate_resource_limits(
51
+ hitch_service_resource_limits
52
+ )
53
+ self._hitch_resolved_images = _validate_resolved_images(hitch_resolved_images)
54
+ self._hitch_prebuilt_task_image = _validate_prebuilt_task_image(
55
+ hitch_prebuilt_task_image
56
+ )
57
+ if not isinstance(hitch_model_proxy_host_gateway, bool):
58
+ raise ValueError("Hitch model proxy host gateway flag is invalid")
59
+ self._hitch_model_proxy_host_gateway = hitch_model_proxy_host_gateway
60
+ if isinstance(hitch_main_gpu_count, bool) or not isinstance(hitch_main_gpu_count, int) or hitch_main_gpu_count < 0:
61
+ raise ValueError("Hitch main GPU count is invalid")
62
+ self._hitch_main_gpu_count = hitch_main_gpu_count
63
+ self._hitch_ownership_temp_dir: tempfile.TemporaryDirectory[str] | None = None
64
+ self._hitch_ownership_compose_path: Path | None = None
65
+ positional = list(args)
66
+ task_env_config = kwargs.get("task_env_config")
67
+ task_env_position: int | None = None
68
+ if task_env_config is None and len(positional) >= 5:
69
+ task_env_position = 4
70
+ task_env_config = positional[task_env_position]
71
+ if task_env_config is not None:
72
+ requested = getattr(task_env_config, "docker_image", None)
73
+ resolved = self._hitch_prebuilt_task_image or self._hitch_resolved_images.get(requested)
74
+ if resolved is not None:
75
+ updated = task_env_config.model_copy(update={"docker_image": resolved})
76
+ if task_env_position is None:
77
+ kwargs["task_env_config"] = updated
78
+ else:
79
+ positional[task_env_position] = updated
80
+ super().__init__(*positional, **kwargs)
81
+ if (
82
+ self._hitch_ownership_labels
83
+ or self._hitch_service_resource_limits
84
+ or self._hitch_resolved_images
85
+ or self._hitch_prebuilt_task_image
86
+ or self._hitch_model_proxy_host_gateway
87
+ or self._hitch_main_gpu_count > 0
88
+ ):
89
+ self._hitch_ownership_compose_path = self._write_ownership_overlay()
90
+
91
+ @property
92
+ def _docker_compose_paths(self) -> list[Path]:
93
+ paths = list(super()._docker_compose_paths)
94
+ if self._hitch_ownership_compose_path is not None:
95
+ paths.append(self._hitch_ownership_compose_path)
96
+ return paths
97
+
98
+ def _write_ownership_overlay(self) -> Path:
99
+ services = {MAIN_SERVICE_NAME}
100
+ if self._enable_egress_control:
101
+ services.add(self._EGRESS_CONTROL_SERVICE_NAME)
102
+ networks = {"default"}
103
+ volumes: set[str] = set()
104
+ external_networks: set[str] = set()
105
+ external_volumes: set[str] = set()
106
+ build_services: set[str] = set()
107
+ image_overlays: dict[str, str] = {}
108
+ sources = [self._environment_docker_compose_path, *self.extra_docker_compose_paths]
109
+ for source in sources:
110
+ if not source.exists():
111
+ continue
112
+ document = yaml.safe_load(source.read_text(encoding="utf-8"))
113
+ if document is None:
114
+ continue
115
+ if not isinstance(document, dict):
116
+ raise ValueError(f"Docker Compose ownership source must be a mapping: {source}")
117
+ services.update(_mapping_names(document.get("services"), "services", source))
118
+ for name, config in (document.get("services") or {}).items():
119
+ if isinstance(config, dict):
120
+ if "build" in config and config.get("build") is not None:
121
+ build_services.add(name)
122
+ reference = config.get("image")
123
+ resolved = self._hitch_resolved_images.get(reference)
124
+ if resolved is not None:
125
+ image_overlays[name] = resolved
126
+ elif reference is not None:
127
+ image_overlays.pop(name, None)
128
+ _collect_resources(document.get("networks"), networks, external_networks, "networks", source)
129
+ _collect_resources(document.get("volumes"), volumes, external_volumes, "volumes", source)
130
+
131
+ labels = dict(self._hitch_ownership_labels)
132
+ service_overlays: dict[str, dict[str, Any]] = {}
133
+ for name in sorted(services):
134
+ config: dict[str, Any] = {}
135
+ if labels:
136
+ config["labels"] = labels
137
+ resolved_image = image_overlays.get(name)
138
+ if name == MAIN_SERVICE_NAME and self._hitch_prebuilt_task_image:
139
+ resolved_image = self._hitch_prebuilt_task_image
140
+ if resolved_image is not None:
141
+ config["image"] = resolved_image
142
+ if (
143
+ name == MAIN_SERVICE_NAME
144
+ and self._hitch_prebuilt_task_image
145
+ and name in build_services
146
+ ):
147
+ # Compose's normal merge semantics retain an earlier build stanza
148
+ # when an overlay only supplies image (or even build: null). The
149
+ # !reset tag is the supported way to make a successful Hitch
150
+ # prebuild authoritative and prevent a second mutable-context build.
151
+ config["build"] = _COMPOSE_RESET_MARKER
152
+ if name == MAIN_SERVICE_NAME and self._hitch_model_proxy_host_gateway:
153
+ config["extra_hosts"] = ["host.docker.internal:host-gateway"]
154
+ if name == MAIN_SERVICE_NAME and self._hitch_main_gpu_count > 0:
155
+ config["deploy"] = {
156
+ "resources": {
157
+ "reservations": {
158
+ "devices": f"{_GPU_OVERRIDE_PREFIX}{self._hitch_main_gpu_count}__"
159
+ }
160
+ }
161
+ }
162
+ if name != MAIN_SERVICE_NAME and self._hitch_service_resource_limits:
163
+ limits = self._hitch_service_resource_limits.get(name)
164
+ if limits is None:
165
+ raise ValueError(
166
+ f"Docker Compose sidecar has no Hitch resource limit: {name}"
167
+ )
168
+ config.update({
169
+ "cpus": limits["cpu_millis"] / 1000,
170
+ "mem_limit": limits["memory_bytes"],
171
+ })
172
+ if limits.get("gpu_count", 0) > 0:
173
+ config["deploy"] = {
174
+ "resources": {
175
+ "reservations": {
176
+ "devices": f"{_GPU_OVERRIDE_PREFIX}{limits['gpu_count']}__"
177
+ }
178
+ }
179
+ }
180
+ service_overlays[name] = config
181
+ overlay = {
182
+ "services": service_overlays,
183
+ "networks": {
184
+ name: {"labels": labels}
185
+ for name in sorted(networks - external_networks)
186
+ },
187
+ "volumes": {
188
+ name: {"labels": labels}
189
+ for name in sorted(volumes - external_volumes)
190
+ },
191
+ }
192
+ self._hitch_ownership_temp_dir = tempfile.TemporaryDirectory(
193
+ prefix="hitch-harbor-ownership-"
194
+ )
195
+ target = Path(self._hitch_ownership_temp_dir.name) / "docker-compose-hitch-ownership.yaml"
196
+ contents = json.dumps(overlay, indent=2, sort_keys=True)
197
+ contents = contents.replace(
198
+ f'"build": "{_COMPOSE_RESET_MARKER}"',
199
+ '"build": !reset null',
200
+ )
201
+ contents = re.sub(
202
+ rf'"devices": "{_GPU_OVERRIDE_PREFIX}([1-9][0-9]*)__"',
203
+ lambda match: '"devices": !override [{"capabilities": ["gpu"], "count": ' + match.group(1) + '}]',
204
+ contents,
205
+ )
206
+ target.write_text(contents, encoding="utf-8")
207
+ return target
208
+
209
+
210
+ def _mapping_names(value: Any, label: str, source: Path) -> set[str]:
211
+ if value is None:
212
+ return set()
213
+ if not isinstance(value, dict) or any(not isinstance(name, str) or not name for name in value):
214
+ raise ValueError(f"Docker Compose {label} must be a named mapping: {source}")
215
+ return set(value)
216
+
217
+
218
+ def _collect_resources(
219
+ value: Any,
220
+ names: set[str],
221
+ external: set[str],
222
+ label: str,
223
+ source: Path,
224
+ ) -> None:
225
+ for name in _mapping_names(value, label, source):
226
+ names.add(name)
227
+ config = value[name]
228
+ if isinstance(config, dict) and bool(config.get("external")):
229
+ external.add(name)
230
+
231
+
232
+ def _validate_labels(value: Mapping[str, str] | None) -> dict[str, str]:
233
+ if value is None:
234
+ return {}
235
+ if not isinstance(value, Mapping) or set(value) - _ALLOWED_LABELS or not _REQUIRED_LABELS <= set(value):
236
+ raise ValueError("Hitch Docker ownership label fields are invalid")
237
+ labels = dict(value)
238
+ if (
239
+ not re.fullmatch(r"[a-f0-9]{24}", labels.get(_LABEL_ROOT, ""))
240
+ or labels.get(_LABEL_PROVIDER) != "local-docker"
241
+ or not re.fullmatch(r"eval_[a-f0-9]{32}", labels.get(_LABEL_EVAL, ""))
242
+ or not re.fullmatch(r"work_[a-f0-9]{32}", labels.get(_LABEL_WORK, ""))
243
+ or not re.fullmatch(r"lease_[a-f0-9]{32}", labels.get(_LABEL_LEASE, ""))
244
+ or not re.fullmatch(r"[1-9][0-9]*", labels.get(_LABEL_EPOCH, ""))
245
+ or any(not isinstance(entry, str) or not entry or len(entry) > 4096 or "\x00" in entry or "\n" in entry or "\r" in entry for entry in labels.values())
246
+ ):
247
+ raise ValueError("Hitch Docker ownership label values are invalid")
248
+ return dict(sorted(labels.items()))
249
+
250
+
251
+ def _validate_resource_limits(
252
+ value: Mapping[str, Mapping[str, int]] | None,
253
+ ) -> dict[str, dict[str, int]]:
254
+ if value is None:
255
+ return {}
256
+ if not isinstance(value, Mapping):
257
+ raise ValueError("Hitch Docker service resource limits are invalid")
258
+ result: dict[str, dict[str, int]] = {}
259
+ for name, resources in value.items():
260
+ if (
261
+ not isinstance(name, str)
262
+ or not name
263
+ or name == MAIN_SERVICE_NAME
264
+ or len(name) > 255
265
+ or any(character in name for character in ("\x00", "\n", "\r"))
266
+ or not isinstance(resources, Mapping)
267
+ or set(resources) - {"cpu_millis", "memory_bytes", "gpu_count"}
268
+ or not {"cpu_millis", "memory_bytes"} <= set(resources)
269
+ or isinstance(resources.get("cpu_millis"), bool)
270
+ or not isinstance(resources.get("cpu_millis"), int)
271
+ or resources["cpu_millis"] < 1
272
+ or isinstance(resources.get("memory_bytes"), bool)
273
+ or not isinstance(resources.get("memory_bytes"), int)
274
+ or resources["memory_bytes"] < 1
275
+ or ("gpu_count" in resources and (
276
+ isinstance(resources.get("gpu_count"), bool)
277
+ or not isinstance(resources.get("gpu_count"), int)
278
+ or resources["gpu_count"] < 1
279
+ ))
280
+ ):
281
+ raise ValueError("Hitch Docker service resource limits are invalid")
282
+ result[name] = {
283
+ "cpu_millis": resources["cpu_millis"],
284
+ "memory_bytes": resources["memory_bytes"],
285
+ **({"gpu_count": resources["gpu_count"]} if "gpu_count" in resources else {}),
286
+ }
287
+ return dict(sorted(result.items()))
288
+
289
+
290
+ def _validate_resolved_images(value: Mapping[str, str] | None) -> dict[str, str]:
291
+ if value is None:
292
+ return {}
293
+ if not isinstance(value, Mapping):
294
+ raise ValueError("Hitch resolved image mapping is invalid")
295
+ result: dict[str, str] = {}
296
+ for requested, resolved in value.items():
297
+ if (
298
+ not _valid_image_reference(requested)
299
+ or not _valid_image_reference(resolved)
300
+ or re.search(r"@sha256:[a-f0-9]{64}$", resolved) is None
301
+ or _image_repository(requested) != _image_repository(resolved)
302
+ ):
303
+ raise ValueError("Hitch resolved image mapping is invalid")
304
+ result[requested] = resolved
305
+ return dict(sorted(result.items()))
306
+
307
+
308
+ def _validate_prebuilt_task_image(value: str | None) -> str | None:
309
+ if value is None:
310
+ return None
311
+ if not isinstance(value, str) or re.fullmatch(r"sha256:[a-f0-9]{64}", value) is None:
312
+ raise ValueError("Hitch prebuilt task image is invalid")
313
+ return value
314
+
315
+
316
+ def _valid_image_reference(value: Any) -> bool:
317
+ return (
318
+ isinstance(value, str)
319
+ and bool(value)
320
+ and len(value) <= 1024
321
+ and not any(character.isspace() or character == "\x00" for character in value)
322
+ and "://" not in value
323
+ and "$" not in value
324
+ )
325
+
326
+
327
+ def _image_repository(reference: str) -> str:
328
+ without_digest = reference.split("@", 1)[0]
329
+ slash = without_digest.rfind("/")
330
+ colon = without_digest.rfind(":")
331
+ return without_digest[:colon] if colon > slash else without_digest
@@ -0,0 +1,275 @@
1
+ """Inspect Harbor task and Compose resource declarations without starting Docker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import sys
8
+ import tomllib
9
+ from decimal import Decimal, InvalidOperation
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import yaml
14
+ from harbor.models.task.config import TaskConfig, VerifierEnvironmentMode
15
+
16
+
17
+ def inspect_task(task_dir: Path) -> dict[str, Any]:
18
+ task_file = task_dir / "task.toml"
19
+ if not task_file.is_file():
20
+ raise ValueError(f"task.toml is missing: {task_file}")
21
+ with task_file.open("rb") as handle:
22
+ config = TaskConfig.model_validate(tomllib.load(handle))
23
+ services = _compose_services(task_dir / "environment" / "docker-compose.yaml")
24
+ runtime_platform = _compose_runtime_platform(
25
+ task_dir / "environment" / "docker-compose.yaml"
26
+ )
27
+ if "main" not in {service["name"] for service in services}:
28
+ services.append({"name": "main", "replicas": 1})
29
+ services.sort(key=lambda service: service["name"].encode("utf-8"))
30
+
31
+ verifier_mode = config.verifier.environment_mode
32
+ separate = verifier_mode == VerifierEnvironmentMode.SEPARATE or (
33
+ verifier_mode is None and config.verifier.environment is not None
34
+ )
35
+ verifier_environment = config.verifier.environment
36
+ if separate and verifier_environment is None:
37
+ verifier_environment = config.environment
38
+ main_egress = config.environment.os.value == "linux" and any(
39
+ mode is not None and mode.value != "public"
40
+ for mode in (
41
+ config.environment.network_mode,
42
+ config.agent.network_mode,
43
+ None if separate else config.verifier.network_mode,
44
+ )
45
+ )
46
+ verifier_egress = bool(
47
+ separate
48
+ and verifier_environment is not None
49
+ and verifier_environment.os.value == "linux"
50
+ and any(
51
+ mode is not None and mode.value != "public"
52
+ for mode in (verifier_environment.network_mode, config.verifier.network_mode)
53
+ )
54
+ )
55
+ images, fallbacks, builds = _environment_images(
56
+ config.environment, "task", task_dir / "environment"
57
+ )
58
+ if separate and verifier_environment is not None:
59
+ verifier_images, verifier_fallbacks, verifier_builds = _environment_images(
60
+ verifier_environment, "verifier", None
61
+ )
62
+ images.extend(verifier_images)
63
+ fallbacks.extend(verifier_fallbacks)
64
+ builds.extend(verifier_builds)
65
+ compose_images, compose_fallbacks = _compose_images(
66
+ task_dir / "environment" / "docker-compose.yaml"
67
+ )
68
+ images.extend(compose_images)
69
+ fallbacks.extend(compose_fallbacks)
70
+ return {
71
+ "schema_version": "1",
72
+ **({"runtime_platform": runtime_platform} if runtime_platform is not None else {}),
73
+ "task": _environment_resources(config.environment),
74
+ "verifier": {
75
+ "separate": separate,
76
+ **({"environment": _environment_resources(verifier_environment)} if verifier_environment is not None else {}),
77
+ },
78
+ "compose_services": services,
79
+ "provider_sidecars": {
80
+ "main_egress": main_egress,
81
+ "verifier_egress": verifier_egress,
82
+ },
83
+ "environment_images": sorted(
84
+ images, key=lambda entry: (entry["source"], entry["service"])
85
+ ),
86
+ "environment_image_fallbacks": sorted(
87
+ fallbacks, key=lambda entry: (entry["source"], entry["service"])
88
+ ),
89
+ "environment_builds": sorted(
90
+ builds, key=lambda entry: (entry["source"], entry["service"])
91
+ ),
92
+ }
93
+
94
+
95
+ def _compose_runtime_platform(compose_file: Path) -> str | None:
96
+ if not compose_file.exists():
97
+ return None
98
+ document = yaml.safe_load(compose_file.read_text(encoding="utf-8"))
99
+ if not isinstance(document, dict):
100
+ return None
101
+ services = document.get("services")
102
+ main = services.get("main") if isinstance(services, dict) else None
103
+ raw = main.get("platform") if isinstance(main, dict) else None
104
+ if raw is None:
105
+ return None
106
+ if not isinstance(raw, str):
107
+ raise ValueError("Compose main platform must be a string")
108
+ normalized = {
109
+ "linux/x86_64": "linux/amd64",
110
+ "linux/aarch64": "linux/arm64",
111
+ }.get(raw.strip().lower(), raw.strip().lower())
112
+ if normalized not in {"linux/amd64", "linux/arm64"}:
113
+ raise ValueError(f"Compose main platform is unsupported: {raw}")
114
+ return normalized
115
+
116
+
117
+ def _environment_resources(environment: Any) -> dict[str, Any]:
118
+ return {
119
+ **({"cpu_millis": environment.cpus * 1000} if environment.cpus is not None else {}),
120
+ **({"memory_bytes": environment.memory_mb * 1024 * 1024} if environment.memory_mb is not None else {}),
121
+ }
122
+
123
+
124
+ def _environment_images(
125
+ environment: Any, source: str, context_dir: Path | None
126
+ ) -> tuple[list[dict[str, str]], list[dict[str, str]], list[dict[str, str]]]:
127
+ reference = getattr(environment, "docker_image", None)
128
+ if reference is None:
129
+ if context_dir is not None and (context_dir / "Dockerfile").is_file():
130
+ return [], [], [{
131
+ "source": source,
132
+ "service": "main",
133
+ "context": "environment",
134
+ "dockerfile": "Dockerfile",
135
+ }]
136
+ return [], [{"source": source, "service": "main", "code": "backend-build"}], []
137
+ if not isinstance(reference, str) or not reference or "$" in reference:
138
+ return [], [{"source": source, "service": "main", "code": "dynamic-image"}], []
139
+ return [{"source": source, "service": "main", "reference": reference}], [], []
140
+
141
+
142
+ def _compose_images(
143
+ compose_file: Path,
144
+ ) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
145
+ if not compose_file.exists():
146
+ return [], []
147
+ document = yaml.safe_load(compose_file.read_text(encoding="utf-8"))
148
+ if document is None or not isinstance(document, dict):
149
+ return [], []
150
+ raw_services = document.get("services")
151
+ if not isinstance(raw_services, dict):
152
+ return [], []
153
+ images: list[dict[str, str]] = []
154
+ fallbacks: list[dict[str, str]] = []
155
+ for name, raw in raw_services.items():
156
+ if not isinstance(name, str) or not isinstance(raw, dict):
157
+ continue
158
+ reference = raw.get("image")
159
+ if raw.get("build") is not None:
160
+ fallbacks.append({"source": "compose", "service": name, "code": "backend-build"})
161
+ elif isinstance(reference, str) and reference and "$" not in reference:
162
+ images.append({"source": "compose", "service": name, "reference": reference})
163
+ elif reference is not None:
164
+ fallbacks.append({"source": "compose", "service": name, "code": "dynamic-image"})
165
+ return images, fallbacks
166
+
167
+
168
+ def _compose_services(compose_file: Path) -> list[dict[str, Any]]:
169
+ if not compose_file.exists():
170
+ return []
171
+ document = yaml.safe_load(compose_file.read_text(encoding="utf-8"))
172
+ if document is None:
173
+ return []
174
+ if not isinstance(document, dict) or "include" in document:
175
+ raise ValueError("Compose resource discovery requires one mapping without include")
176
+ raw_services = document.get("services")
177
+ if not isinstance(raw_services, dict):
178
+ raise ValueError("Compose services must be a mapping")
179
+ result: list[dict[str, Any]] = []
180
+ for name, raw in raw_services.items():
181
+ if not isinstance(name, str) or not name or not isinstance(raw, dict):
182
+ raise ValueError("Compose service declaration is invalid")
183
+ if raw.get("profiles") or raw.get("extends"):
184
+ raise ValueError(f"Compose resource discovery does not support profiles/extends: {name}")
185
+ deploy = raw.get("deploy") if isinstance(raw.get("deploy"), dict) else {}
186
+ deploy_resources = deploy.get("resources") if isinstance(deploy.get("resources"), dict) else {}
187
+ limits = deploy_resources.get("limits") if isinstance(deploy_resources.get("limits"), dict) else {}
188
+ reservations = deploy_resources.get("reservations") if isinstance(deploy_resources.get("reservations"), dict) else {}
189
+ cpu_values = [_cpu_millis(value, name) for value in (raw.get("cpus"), limits.get("cpus")) if value is not None]
190
+ memory_values = [_memory_bytes(value, name) for value in (raw.get("mem_limit"), limits.get("memory")) if value is not None]
191
+ gpu_values = [_gpu_count(value, name) for value in (raw.get("gpus"), reservations.get("devices")) if value is not None]
192
+ replicas = raw.get("scale", deploy.get("replicas", 1))
193
+ if isinstance(replicas, bool) or not isinstance(replicas, int) or replicas < 1:
194
+ raise ValueError(f"Compose service replicas must be a positive integer: {name}")
195
+ result.append({
196
+ "name": name,
197
+ "replicas": replicas,
198
+ **({"cpu_millis": max(cpu_values)} if cpu_values else {}),
199
+ **({"memory_bytes": max(memory_values)} if memory_values else {}),
200
+ **({"gpu_count": max(gpu_values)} if gpu_values else {}),
201
+ })
202
+ return result
203
+
204
+
205
+ def _gpu_count(value: Any, service: str) -> int:
206
+ if value == "all":
207
+ raise ValueError(f"Compose GPU request must use a fixed count: {service}")
208
+ if not isinstance(value, list) or not value:
209
+ raise ValueError(f"Compose GPU request is invalid: {service}")
210
+ total = 0
211
+ for request in value:
212
+ if not isinstance(request, dict):
213
+ raise ValueError(f"Compose GPU request is invalid: {service}")
214
+ capabilities = request.get("capabilities")
215
+ if not isinstance(capabilities, list) or "gpu" not in capabilities:
216
+ continue
217
+ count = request.get("count")
218
+ device_ids = request.get("device_ids")
219
+ if count is not None and device_ids is not None:
220
+ raise ValueError(f"Compose GPU request cannot set count and device_ids: {service}")
221
+ if device_ids is not None:
222
+ if not isinstance(device_ids, list) or not device_ids or any(not isinstance(item, str) or not item for item in device_ids):
223
+ raise ValueError(f"Compose GPU device_ids are invalid: {service}")
224
+ total += len(device_ids)
225
+ elif isinstance(count, int) and not isinstance(count, bool) and count > 0:
226
+ total += count
227
+ else:
228
+ raise ValueError(f"Compose GPU request must use a fixed count: {service}")
229
+ if total < 1:
230
+ raise ValueError(f"Compose GPU request has no gpu capability: {service}")
231
+ return total
232
+
233
+
234
+ def _cpu_millis(value: Any, service: str) -> int:
235
+ if isinstance(value, bool) or not isinstance(value, (int, float, str)):
236
+ raise ValueError(f"Compose CPU limit is not numeric: {service}")
237
+ try:
238
+ decimal = Decimal(str(value).strip()) * 1000
239
+ except InvalidOperation as error:
240
+ raise ValueError(f"Compose CPU limit is not numeric: {service}") from error
241
+ if decimal != decimal.to_integral_value() or decimal <= 0:
242
+ raise ValueError(f"Compose CPU limit must be positive whole millicores: {service}")
243
+ return int(decimal)
244
+
245
+
246
+ def _memory_bytes(value: Any, service: str) -> int:
247
+ if isinstance(value, bool):
248
+ raise ValueError(f"Compose memory limit is invalid: {service}")
249
+ if isinstance(value, int):
250
+ if value <= 0:
251
+ raise ValueError(f"Compose memory limit must be positive: {service}")
252
+ return value
253
+ if not isinstance(value, str):
254
+ raise ValueError(f"Compose memory limit is invalid: {service}")
255
+ match = re.fullmatch(r"\s*([0-9]+(?:\.[0-9]+)?)\s*([kmgtpe]?i?b?)?\s*", value, re.IGNORECASE)
256
+ if not match:
257
+ raise ValueError(f"Compose memory limit is not a fixed size: {service}")
258
+ unit = (match.group(2) or "b").lower()
259
+ powers = {"b": 0, "k": 1, "kb": 1, "ki": 1, "kib": 1, "m": 2, "mb": 2, "mi": 2, "mib": 2,
260
+ "g": 3, "gb": 3, "gi": 3, "gib": 3, "t": 4, "tb": 4, "ti": 4, "tib": 4,
261
+ "p": 5, "pb": 5, "pi": 5, "pib": 5, "e": 6, "eb": 6, "ei": 6, "eib": 6}
262
+ bytes_value = Decimal(match.group(1)) * (1024 ** powers[unit])
263
+ if bytes_value != bytes_value.to_integral_value() or bytes_value <= 0:
264
+ raise ValueError(f"Compose memory limit is not a whole positive byte size: {service}")
265
+ return int(bytes_value)
266
+
267
+
268
+ def main() -> None:
269
+ if len(sys.argv) != 2:
270
+ raise SystemExit("usage: hitch_harbor_task_resources.py <task-directory>")
271
+ print(json.dumps(inspect_task(Path(sys.argv[1]).resolve()), separators=(",", ":"), sort_keys=True))
272
+
273
+
274
+ if __name__ == "__main__":
275
+ main()
@@ -11,7 +11,9 @@ import asyncio
11
11
  import json
12
12
  import re
13
13
  import shutil
14
+ import time
14
15
  from dataclasses import dataclass
16
+ from datetime import datetime, timezone
15
17
  from pathlib import Path
16
18
  from typing import Any
17
19
 
@@ -117,6 +119,13 @@ class HitchRetryingVerifier(Verifier):
117
119
  self.infrastructure_retry_backoff_ms = infrastructure_retry_backoff_ms
118
120
 
119
121
  async def verify(self) -> VerifierResult:
122
+ started_ns = time.monotonic_ns()
123
+ try:
124
+ return await self._verify_with_retries()
125
+ finally:
126
+ self._write_phase_timing(started_ns)
127
+
128
+ async def _verify_with_retries(self) -> VerifierResult:
120
129
  # Do not trust a control file left by the candidate or a prior phase.
121
130
  await self._remove_files(CONTROL_NAMES)
122
131
  attempts: list[dict[str, Any]] = []
@@ -179,6 +188,20 @@ class HitchRetryingVerifier(Verifier):
179
188
 
180
189
  raise AssertionError("unreachable verifier retry state")
181
190
 
191
+ def _write_phase_timing(self, started_ns: int) -> None:
192
+ self._write_json(
193
+ "hitch-phase-timings.json",
194
+ {
195
+ "schema_version": "1",
196
+ "phases": {
197
+ "verifier": {
198
+ "duration_ms": max(0, (time.monotonic_ns() - started_ns) // 1_000_000),
199
+ "completed_at": datetime.now(timezone.utc).isoformat(),
200
+ }
201
+ },
202
+ },
203
+ )
204
+
182
205
  async def _clear_outputs(self) -> None:
183
206
  await self._remove_files(OUTPUT_NAMES)
184
207