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.
- package/README.md +14 -1
- package/dist/scripts/canary-buildkit-secrets.js +166 -0
- package/dist/scripts/canary-buildkit-secrets.js.map +1 -0
- package/dist/scripts/canary-collision-domains.js +77 -0
- package/dist/scripts/canary-collision-domains.js.map +1 -0
- package/dist/scripts/canary-docker-image-observation.js +181 -0
- package/dist/scripts/canary-docker-image-observation.js.map +1 -0
- package/dist/scripts/canary-gpu-hardware.js +95 -0
- package/dist/scripts/canary-gpu-hardware.js.map +1 -0
- package/dist/scripts/canary-harbor-load.js +365 -0
- package/dist/scripts/canary-harbor-load.js.map +1 -0
- package/dist/scripts/canary-packaged-harness.js +75 -0
- package/dist/scripts/canary-packaged-harness.js.map +1 -0
- package/dist/scripts/canary-resource-load.js +188 -0
- package/dist/scripts/canary-resource-load.js.map +1 -0
- package/dist/scripts/check-architecture.js +8 -4
- package/dist/scripts/check-architecture.js.map +1 -1
- package/dist/scripts/docker-reaper-crash-child.js +21 -0
- package/dist/scripts/docker-reaper-crash-child.js.map +1 -0
- package/dist/src/adapters/catalog.js +1 -0
- package/dist/src/adapters/catalog.js.map +1 -1
- package/dist/src/adapters/discovery.js +1 -1
- package/dist/src/adapters/discovery.js.map +1 -1
- package/dist/src/adapters/providers/claude.js +6 -0
- package/dist/src/adapters/providers/claude.js.map +1 -1
- package/dist/src/adapters/providers/codex.js +6 -0
- package/dist/src/adapters/providers/codex.js.map +1 -1
- package/dist/src/adapters/providers/deepseek.js +6 -0
- package/dist/src/adapters/providers/deepseek.js.map +1 -1
- package/dist/src/adapters/providers/opencode.js +6 -0
- package/dist/src/adapters/providers/opencode.js.map +1 -1
- package/dist/src/adapters/providers/pi.js +6 -0
- package/dist/src/adapters/providers/pi.js.map +1 -1
- package/dist/src/artifacts/index.js +1 -0
- package/dist/src/artifacts/index.js.map +1 -1
- package/dist/src/backends/harbor/backend.js +53 -103
- package/dist/src/backends/harbor/backend.js.map +1 -1
- package/dist/src/backends/harbor/environment-config.js +92 -0
- package/dist/src/backends/harbor/environment-config.js.map +1 -0
- package/dist/src/backends/harbor/index.js +5 -1
- package/dist/src/backends/harbor/index.js.map +1 -1
- package/dist/src/backends/harbor/model-proxy-config.js +23 -0
- package/dist/src/backends/harbor/model-proxy-config.js.map +1 -0
- package/dist/src/backends/harbor/process.js +177 -0
- package/dist/src/backends/harbor/process.js.map +1 -0
- package/dist/src/backends/harbor/runtime-toolchain.js +30 -0
- package/dist/src/backends/harbor/runtime-toolchain.js.map +1 -0
- package/dist/src/backends/harbor/supervisor.js +74 -0
- package/dist/src/backends/harbor/supervisor.js.map +1 -0
- package/dist/src/backends/harbor/task-resources.js +206 -0
- package/dist/src/backends/harbor/task-resources.js.map +1 -0
- package/dist/src/backends/harbor/tools.js +6 -29
- package/dist/src/backends/harbor/tools.js.map +1 -1
- package/dist/src/backends/index.js +4 -1
- package/dist/src/backends/index.js.map +1 -1
- package/dist/src/cli/commands/daemon.js +135 -8
- package/dist/src/cli/commands/daemon.js.map +1 -1
- package/dist/src/cli/commands/eval.js +214 -4
- package/dist/src/cli/commands/eval.js.map +1 -1
- package/dist/src/cli/commands/images.js +40 -0
- package/dist/src/cli/commands/images.js.map +1 -0
- package/dist/src/cli/commands/run.js +6 -3
- package/dist/src/cli/commands/run.js.map +1 -1
- package/dist/src/cli/commands/runs.js +25 -3
- package/dist/src/cli/commands/runs.js.map +1 -1
- package/dist/src/cli/commands/worker.js +79 -0
- package/dist/src/cli/commands/worker.js.map +1 -0
- package/dist/src/cli/main.js +4 -0
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/output.js +71 -3
- package/dist/src/cli/output.js.map +1 -1
- package/dist/src/control-plane/build-admission.js +69 -0
- package/dist/src/control-plane/build-admission.js.map +1 -0
- package/dist/src/control-plane/build-records.js +6 -0
- package/dist/src/control-plane/build-records.js.map +1 -0
- package/dist/src/control-plane/collisions.js +40 -0
- package/dist/src/control-plane/collisions.js.map +1 -0
- package/dist/src/control-plane/eval-control-state.js +22 -0
- package/dist/src/control-plane/eval-control-state.js.map +1 -0
- package/dist/src/control-plane/eval-control-work.js +39 -0
- package/dist/src/control-plane/eval-control-work.js.map +1 -0
- package/dist/src/control-plane/eval-image-resolution.js +52 -0
- package/dist/src/control-plane/eval-image-resolution.js.map +1 -0
- package/dist/src/control-plane/eval-image-services.js +30 -0
- package/dist/src/control-plane/eval-image-services.js.map +1 -0
- package/dist/src/control-plane/eval-records.js +252 -0
- package/dist/src/control-plane/eval-records.js.map +1 -0
- package/dist/src/control-plane/eval-recovery.js +181 -0
- package/dist/src/control-plane/eval-recovery.js.map +1 -0
- package/dist/src/control-plane/eval-scheduler.js +447 -0
- package/dist/src/control-plane/eval-scheduler.js.map +1 -0
- package/dist/src/control-plane/index.js +20 -0
- package/dist/src/control-plane/index.js.map +1 -0
- package/dist/src/control-plane/local-worker.js +49 -0
- package/dist/src/control-plane/local-worker.js.map +1 -0
- package/dist/src/control-plane/model-capture-planning.js +12 -0
- package/dist/src/control-plane/model-capture-planning.js.map +1 -0
- package/dist/src/control-plane/remote-result-transport.js +172 -0
- package/dist/src/control-plane/remote-result-transport.js.map +1 -0
- package/dist/src/control-plane/remote-work-coordinator.js +332 -0
- package/dist/src/control-plane/remote-work-coordinator.js.map +1 -0
- package/dist/src/control-plane/remote-work-inputs.js +266 -0
- package/dist/src/control-plane/remote-work-inputs.js.map +1 -0
- package/dist/src/control-plane/remote-work-recovery.js +181 -0
- package/dist/src/control-plane/remote-work-recovery.js.map +1 -0
- package/dist/src/control-plane/remote-worker-artifacts.js +135 -0
- package/dist/src/control-plane/remote-worker-artifacts.js.map +1 -0
- package/dist/src/control-plane/remote-worker-client.js +202 -0
- package/dist/src/control-plane/remote-worker-client.js.map +1 -0
- package/dist/src/control-plane/remote-worker-configuration.js +34 -0
- package/dist/src/control-plane/remote-worker-configuration.js.map +1 -0
- package/dist/src/control-plane/remote-worker-credentials.js +58 -0
- package/dist/src/control-plane/remote-worker-credentials.js.map +1 -0
- package/dist/src/control-plane/remote-worker-protocol.js +510 -0
- package/dist/src/control-plane/remote-worker-protocol.js.map +1 -0
- package/dist/src/control-plane/remote-worker-runner.js +214 -0
- package/dist/src/control-plane/remote-worker-runner.js.map +1 -0
- package/dist/src/control-plane/remote-workers.js +377 -0
- package/dist/src/control-plane/remote-workers.js.map +1 -0
- package/dist/src/control-plane/rerun-scheduler.js +411 -0
- package/dist/src/control-plane/rerun-scheduler.js.map +1 -0
- package/dist/src/control-plane/rerun-submission.js +56 -0
- package/dist/src/control-plane/rerun-submission.js.map +1 -0
- package/dist/src/control-plane/resources.js +182 -0
- package/dist/src/control-plane/resources.js.map +1 -0
- package/dist/src/control-plane/scheduler-eval-entry.js +32 -0
- package/dist/src/control-plane/scheduler-eval-entry.js.map +1 -0
- package/dist/src/control-plane/synthetic-result.js +21 -0
- package/dist/src/control-plane/synthetic-result.js.map +1 -0
- package/dist/src/control-plane/work-admission.js +21 -0
- package/dist/src/control-plane/work-admission.js.map +1 -0
- package/dist/src/control-plane/work-dispatcher.js +229 -0
- package/dist/src/control-plane/work-dispatcher.js.map +1 -0
- package/dist/src/controller-runtime/hash.js +12 -1
- package/dist/src/controller-runtime/hash.js.map +1 -1
- package/dist/src/controller-runtime/index.js +1 -1
- package/dist/src/controller-runtime/index.js.map +1 -1
- package/dist/src/controller-runtime/store.js +15 -6
- package/dist/src/controller-runtime/store.js.map +1 -1
- package/dist/src/daemon/client.js.map +1 -1
- package/dist/src/daemon/event-stream.js +46 -0
- package/dist/src/daemon/event-stream.js.map +1 -0
- package/dist/src/daemon/health.js +66 -0
- package/dist/src/daemon/health.js.map +1 -0
- package/dist/src/daemon/launcher.js +28 -1
- package/dist/src/daemon/launcher.js.map +1 -1
- package/dist/src/daemon/logging.js +23 -0
- package/dist/src/daemon/logging.js.map +1 -0
- package/dist/src/daemon/scheduler.js +26 -5
- package/dist/src/daemon/scheduler.js.map +1 -1
- package/dist/src/daemon/server.js +255 -98
- package/dist/src/daemon/server.js.map +1 -1
- package/dist/src/daemon/telemetry.js +163 -0
- package/dist/src/daemon/telemetry.js.map +1 -0
- package/dist/src/daemon/worker-routes.js +219 -0
- package/dist/src/daemon/worker-routes.js.map +1 -0
- package/dist/src/domain/bundles.js +2 -0
- package/dist/src/domain/bundles.js.map +1 -0
- package/dist/src/domain/control-plane.js +2 -0
- package/dist/src/domain/control-plane.js.map +1 -0
- package/dist/src/domain/execution-evidence.js +2 -0
- package/dist/src/domain/execution-evidence.js.map +1 -0
- package/dist/src/domain/execution-plan.js +2 -0
- package/dist/src/domain/execution-plan.js.map +1 -0
- package/dist/src/domain/images.js +2 -0
- package/dist/src/domain/images.js.map +1 -0
- package/dist/src/domain/index.js +9 -0
- package/dist/src/domain/index.js.map +1 -1
- package/dist/src/domain/interactions.js +2 -0
- package/dist/src/domain/interactions.js.map +1 -0
- package/dist/src/domain/providers.js +2 -0
- package/dist/src/domain/providers.js.map +1 -0
- package/dist/src/domain/resources.js +2 -0
- package/dist/src/domain/resources.js.map +1 -0
- package/dist/src/domain/workers.js +2 -0
- package/dist/src/domain/workers.js.map +1 -0
- package/dist/src/evals/collect-only-rerun.js +182 -0
- package/dist/src/evals/collect-only-rerun.js.map +1 -0
- package/dist/src/evals/directory.js +30 -0
- package/dist/src/evals/directory.js.map +1 -0
- package/dist/src/evals/docker-engine-stats.js +89 -0
- package/dist/src/evals/docker-engine-stats.js.map +1 -0
- package/dist/src/evals/docker-ownership.js +51 -0
- package/dist/src/evals/docker-ownership.js.map +1 -0
- package/dist/src/evals/docker-reaper.js +144 -0
- package/dist/src/evals/docker-reaper.js.map +1 -0
- package/dist/src/evals/docker-resource-observer.js +207 -0
- package/dist/src/evals/docker-resource-observer.js.map +1 -0
- package/dist/src/evals/environment-image-planning.js +164 -0
- package/dist/src/evals/environment-image-planning.js.map +1 -0
- package/dist/src/evals/eval-artifact-planning.js +56 -0
- package/dist/src/evals/eval-artifact-planning.js.map +1 -0
- package/dist/src/evals/eval-finalization.js +10 -0
- package/dist/src/evals/eval-finalization.js.map +1 -0
- package/dist/src/evals/eval-lifecycle-events.js +16 -0
- package/dist/src/evals/eval-lifecycle-events.js.map +1 -0
- package/dist/src/evals/events.js +53 -5
- package/dist/src/evals/events.js.map +1 -1
- package/dist/src/evals/execution-evidence.js +116 -0
- package/dist/src/evals/execution-evidence.js.map +1 -0
- package/dist/src/evals/execution-leases.js +342 -0
- package/dist/src/evals/execution-leases.js.map +1 -0
- package/dist/src/evals/execution-plan-artifacts.js +72 -0
- package/dist/src/evals/execution-plan-artifacts.js.map +1 -0
- package/dist/src/evals/execution-plan-images.js +69 -0
- package/dist/src/evals/execution-plan-images.js.map +1 -0
- package/dist/src/evals/execution-plan-resources.js +174 -0
- package/dist/src/evals/execution-plan-resources.js.map +1 -0
- package/dist/src/evals/execution-plan.js +415 -0
- package/dist/src/evals/execution-plan.js.map +1 -0
- package/dist/src/evals/harbor-artifact-builder-image.js +99 -0
- package/dist/src/evals/harbor-artifact-builder-image.js.map +1 -0
- package/dist/src/evals/harbor-artifact-builder.js +354 -0
- package/dist/src/evals/harbor-artifact-builder.js.map +1 -0
- package/dist/src/evals/harbor-bridge-error.js +18 -3
- package/dist/src/evals/harbor-bridge-error.js.map +1 -1
- package/dist/src/evals/harbor-phase-timings.js +60 -0
- package/dist/src/evals/harbor-phase-timings.js.map +1 -0
- package/dist/src/evals/index.js +25 -2
- package/dist/src/evals/index.js.map +1 -1
- package/dist/src/evals/infrastructure-retry.js +100 -63
- package/dist/src/evals/infrastructure-retry.js.map +1 -1
- package/dist/src/evals/interaction-capture-import.js +49 -0
- package/dist/src/evals/interaction-capture-import.js.map +1 -0
- package/dist/src/evals/local-docker-provider.js +372 -0
- package/dist/src/evals/local-docker-provider.js.map +1 -0
- package/dist/src/evals/local-eval-planning.js +80 -0
- package/dist/src/evals/local-eval-planning.js.map +1 -0
- package/dist/src/evals/model-capture-plan.js +22 -0
- package/dist/src/evals/model-capture-plan.js.map +1 -0
- package/dist/src/evals/model-capture-runtime.js +114 -0
- package/dist/src/evals/model-capture-runtime.js.map +1 -0
- package/dist/src/evals/model-proxy-runtime-state.js +53 -0
- package/dist/src/evals/model-proxy-runtime-state.js.map +1 -0
- package/dist/src/evals/planned-execution-support.js +54 -0
- package/dist/src/evals/planned-execution-support.js.map +1 -0
- package/dist/src/evals/planned-execution.js +461 -0
- package/dist/src/evals/planned-execution.js.map +1 -0
- package/dist/src/evals/planned-remote-execution.js +40 -0
- package/dist/src/evals/planned-remote-execution.js.map +1 -0
- package/dist/src/evals/planned-retry-lifecycle.js +186 -0
- package/dist/src/evals/planned-retry-lifecycle.js.map +1 -0
- package/dist/src/evals/prepared-harness.js +76 -0
- package/dist/src/evals/prepared-harness.js.map +1 -0
- package/dist/src/evals/progress.js +4 -4
- package/dist/src/evals/progress.js.map +1 -1
- package/dist/src/evals/records.js +20 -3
- package/dist/src/evals/records.js.map +1 -1
- package/dist/src/evals/recovery.js +192 -0
- package/dist/src/evals/recovery.js.map +1 -0
- package/dist/src/evals/remote-infrastructure-retry.js +99 -0
- package/dist/src/evals/remote-infrastructure-retry.js.map +1 -0
- package/dist/src/evals/rerun-inputs.js +89 -0
- package/dist/src/evals/rerun-inputs.js.map +1 -0
- package/dist/src/evals/rerun-slots.js +4 -3
- package/dist/src/evals/rerun-slots.js.map +1 -1
- package/dist/src/evals/rerun-types.js +49 -0
- package/dist/src/evals/rerun-types.js.map +1 -0
- package/dist/src/evals/rerun.js +113 -75
- package/dist/src/evals/rerun.js.map +1 -1
- package/dist/src/evals/result-helpers.js +65 -0
- package/dist/src/evals/result-helpers.js.map +1 -0
- package/dist/src/evals/resume-state.js +36 -0
- package/dist/src/evals/resume-state.js.map +1 -0
- package/dist/src/evals/service-types.js +2 -0
- package/dist/src/evals/service-types.js.map +1 -0
- package/dist/src/evals/service.js +292 -257
- package/dist/src/evals/service.js.map +1 -1
- package/dist/src/evals/task-resources.js +209 -0
- package/dist/src/evals/task-resources.js.map +1 -0
- package/dist/src/evals/trial-environment-evidence.js +87 -0
- package/dist/src/evals/trial-environment-evidence.js.map +1 -0
- package/dist/src/evals/trial-execution-evidence.js +13 -0
- package/dist/src/evals/trial-execution-evidence.js.map +1 -0
- package/dist/src/evals/trial-import-identity.js +31 -0
- package/dist/src/evals/trial-import-identity.js.map +1 -0
- package/dist/src/evals/trial-import.js +143 -145
- package/dist/src/evals/trial-import.js.map +1 -1
- package/dist/src/evals/trial-publication-recovery.js +104 -0
- package/dist/src/evals/trial-publication-recovery.js.map +1 -0
- package/dist/src/evals/trial-publication.js +25 -0
- package/dist/src/evals/trial-publication.js.map +1 -0
- package/dist/src/evals/work-item-artifacts.js +23 -0
- package/dist/src/evals/work-item-artifacts.js.map +1 -0
- package/dist/src/foundation/config.js +17 -0
- package/dist/src/foundation/config.js.map +1 -1
- package/dist/src/foundation/credential-redaction.js +137 -0
- package/dist/src/foundation/credential-redaction.js.map +1 -0
- package/dist/src/foundation/environment-image-references.js +59 -0
- package/dist/src/foundation/environment-image-references.js.map +1 -0
- package/dist/src/foundation/executable.js +45 -5
- package/dist/src/foundation/executable.js.map +1 -1
- package/dist/src/foundation/index.js +5 -2
- package/dist/src/foundation/index.js.map +1 -1
- package/dist/src/foundation/process-identity.js +69 -0
- package/dist/src/foundation/process-identity.js.map +1 -0
- package/dist/src/images/context.js +47 -0
- package/dist/src/images/context.js.map +1 -0
- package/dist/src/images/docker-buildkit.js +137 -0
- package/dist/src/images/docker-buildkit.js.map +1 -0
- package/dist/src/images/dockerfile.js +67 -0
- package/dist/src/images/dockerfile.js.map +1 -0
- package/dist/src/images/gc.js +261 -0
- package/dist/src/images/gc.js.map +1 -0
- package/dist/src/images/index.js +10 -0
- package/dist/src/images/index.js.map +1 -0
- package/dist/src/images/manifest.js +109 -0
- package/dist/src/images/manifest.js.map +1 -0
- package/dist/src/images/ownership.js +5 -0
- package/dist/src/images/ownership.js.map +1 -0
- package/dist/src/images/records.js +32 -0
- package/dist/src/images/records.js.map +1 -0
- package/dist/src/images/registry.js +154 -0
- package/dist/src/images/registry.js.map +1 -0
- package/dist/src/images/service.js +224 -0
- package/dist/src/images/service.js.map +1 -0
- package/dist/src/model-access/capture.js +255 -0
- package/dist/src/model-access/capture.js.map +1 -0
- package/dist/src/model-access/index.js +5 -0
- package/dist/src/model-access/index.js.map +1 -0
- package/dist/src/model-access/policy.js +49 -0
- package/dist/src/model-access/policy.js.map +1 -0
- package/dist/src/model-access/proxy.js +336 -0
- package/dist/src/model-access/proxy.js.map +1 -0
- package/dist/src/model-access/records.js +167 -0
- package/dist/src/model-access/records.js.map +1 -0
- package/dist/src/runs/bundle.js +450 -0
- package/dist/src/runs/bundle.js.map +1 -0
- package/dist/src/runs/events.js +12 -1
- package/dist/src/runs/events.js.map +1 -1
- package/dist/src/runs/executor.js +24 -17
- package/dist/src/runs/executor.js.map +1 -1
- package/dist/src/runs/index.js +3 -1
- package/dist/src/runs/index.js.map +1 -1
- package/dist/src/runs/manifest.js +5 -5
- package/dist/src/runs/manifest.js.map +1 -1
- package/dist/src/runs/request.js +6 -1
- package/dist/src/runs/request.js.map +1 -1
- package/dist/src/runs/training-candidate.js +161 -0
- package/dist/src/runs/training-candidate.js.map +1 -0
- package/dist/src/trajectories/provider-capture.js +15 -24
- package/dist/src/trajectories/provider-capture.js.map +1 -1
- package/dist/src/trajectories/providers/deepseek.js +3 -3
- package/dist/src/trajectories/providers/deepseek.js.map +1 -1
- package/dist/src/workers/index.js +3 -0
- package/dist/src/workers/index.js.map +1 -0
- package/dist/src/workers/remote-harbor-work-spec.js +97 -0
- package/dist/src/workers/remote-harbor-work-spec.js.map +1 -0
- package/dist/src/workers/remote-harbor-worker.js +220 -0
- package/dist/src/workers/remote-harbor-worker.js.map +1 -0
- package/docs/schemas/environment-build-index.schema.json +13 -0
- package/docs/schemas/environment-build-record.schema.json +25 -0
- package/docs/schemas/environment-image-manifest.schema.json +62 -0
- package/docs/schemas/eval-control.schema.json +36 -0
- package/docs/schemas/eval-execution-plan.schema.json +216 -0
- package/docs/schemas/eval-rerun-result.schema.json +53 -0
- package/docs/schemas/eval-rerun-state.schema.json +61 -0
- package/docs/schemas/eval-rerun-submission.schema.json +58 -0
- package/docs/schemas/eval-result.schema.json +34 -1
- package/docs/schemas/eval-submission-input.schema.json +14 -0
- package/docs/schemas/eval-submission.schema.json +68 -0
- package/docs/schemas/eval-trial-publication.schema.json +39 -0
- package/docs/schemas/execution-evidence.schema.json +96 -0
- package/docs/schemas/execution-lease.schema.json +50 -0
- package/docs/schemas/execution-provider-status.schema.json +65 -0
- package/docs/schemas/execution-worker.schema.json +53 -0
- package/docs/schemas/harbor-process-exit.schema.json +14 -0
- package/docs/schemas/interaction-capture-ref.schema.json +45 -0
- package/docs/schemas/local-provider-execution.schema.json +33 -0
- package/docs/schemas/model-interaction.schema.json +41 -0
- package/docs/schemas/remote-result-envelope.schema.json +32 -0
- package/docs/schemas/remote-tree-envelope.schema.json +25 -0
- package/docs/schemas/remote-work-offer.schema.json +58 -0
- package/docs/schemas/remote-work-receipt.schema.json +50 -0
- package/docs/schemas/remote-worker-artifact.schema.json +17 -0
- package/docs/schemas/remote-worker-event.schema.json +18 -0
- package/docs/schemas/remote-worker-heartbeat.schema.json +37 -0
- package/docs/schemas/remote-worker-registration.schema.json +52 -0
- package/docs/schemas/result-bundle-index.schema.json +133 -0
- package/docs/schemas/run-environment-images.schema.json +39 -0
- package/docs/schemas/training-data-candidate.schema.json +46 -0
- package/integrations/harbor/hitch_harbor_agent.py +171 -539
- package/integrations/harbor/hitch_harbor_environment.py +331 -0
- package/integrations/harbor/hitch_harbor_task_resources.py +275 -0
- package/integrations/harbor/hitch_harbor_verifier.py +23 -0
- 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
|
|