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
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import asyncio
|
|
6
|
-
import errno
|
|
7
5
|
import hashlib
|
|
8
6
|
import json
|
|
9
|
-
import os
|
|
10
7
|
import re
|
|
11
8
|
import shlex
|
|
12
|
-
import shutil
|
|
13
9
|
import stat as stat_module
|
|
14
10
|
import tempfile
|
|
11
|
+
import time
|
|
15
12
|
import uuid
|
|
13
|
+
from urllib.parse import urlparse
|
|
16
14
|
from datetime import datetime, timezone
|
|
17
15
|
from pathlib import Path, PurePosixPath
|
|
18
16
|
from typing import Any
|
|
@@ -22,12 +20,7 @@ from harbor.environments.base import BaseEnvironment, ExecResult
|
|
|
22
20
|
from harbor.models.agent.context import AgentContext
|
|
23
21
|
|
|
24
22
|
CONTROLLER_RUNTIME_MANIFEST_VERSION = "2"
|
|
25
|
-
LOCAL_GIT_TRANSPORT_MANIFEST_VERSION = "1"
|
|
26
|
-
HARNESS_ARTIFACT_MANIFEST_VERSION = "1"
|
|
27
|
-
LOCAL_GIT_TRANSPORT_MAX_BYTES = 512 * 1024 * 1024
|
|
28
|
-
LOCAL_GIT_REMOTE_ROOT = "/opt/hitch-local-source"
|
|
29
23
|
HARNESS_ARTIFACT_REMOTE_ROOT = "/opt/hitch-harness-artifact"
|
|
30
|
-
HITCH_CONTAINER_STATE_ROOT = "/tmp/hitch-state"
|
|
31
24
|
HITCH_BRIDGE_ERROR_LOG = "/logs/agent/hitch-bridge-error.json"
|
|
32
25
|
HITCH_DIAGNOSTIC_MAX_BYTES = 8 * 1024
|
|
33
26
|
HITCH_BRIDGE_ERROR_MAX_BYTES = 64 * 1024
|
|
@@ -56,16 +49,22 @@ class HitchHarborAgent(BaseAgent):
|
|
|
56
49
|
candidate_id: str = "candidate-1",
|
|
57
50
|
controller_runtime_id: str | None = None,
|
|
58
51
|
harness_artifact: dict[str, Any] | None = None,
|
|
52
|
+
node_version: str = "v22.23.0",
|
|
53
|
+
# Legacy parameters are accepted only so old persisted jobs fail at
|
|
54
|
+
# the explicit artifact handoff boundary instead of leaking unknown
|
|
55
|
+
# kwargs into Harbor's BaseAgent. New jobs pass neither value.
|
|
59
56
|
harness_artifact_cache_dir: str | None = None,
|
|
60
57
|
local_source_transport: dict[str, Any] | None = None,
|
|
61
58
|
hitch_timeout_ms: int = 900_000,
|
|
62
59
|
agent_args: list[str] | None = None,
|
|
60
|
+
credential_names: list[str] | None = None,
|
|
63
61
|
workdir: str | None = None,
|
|
64
62
|
eval_id: str | None = None,
|
|
65
63
|
benchmark_id: str | None = None,
|
|
66
64
|
benchmark_revision: str | None = None,
|
|
67
65
|
verifier_identity: str | None = None,
|
|
68
66
|
logical_attempt: int | None = None,
|
|
67
|
+
model_capture: dict[str, Any] | None = None,
|
|
69
68
|
**kwargs: Any,
|
|
70
69
|
) -> None:
|
|
71
70
|
super().__init__(logs_dir=logs_dir, **kwargs)
|
|
@@ -74,11 +73,23 @@ class HitchHarborAgent(BaseAgent):
|
|
|
74
73
|
self.hitch_runtime_dir = Path(hitch_runtime_dir)
|
|
75
74
|
self.controller_runtime_id = controller_runtime_id
|
|
76
75
|
self.harness_artifact = dict(harness_artifact) if harness_artifact else None
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
if harness_artifact_cache_dir is not None:
|
|
77
|
+
raise ValueError("trial-side harness artifact caches are no longer supported")
|
|
78
|
+
if not isinstance(node_version, str) or re.fullmatch(r"v\d+\.\d+\.\d+", node_version) is None:
|
|
79
|
+
raise ValueError("node_version must be an exact stable Node.js version")
|
|
80
|
+
self.required_node_version = node_version
|
|
81
|
+
if local_source_transport is not None:
|
|
82
|
+
raise ValueError("trial-side local source transports are no longer supported")
|
|
79
83
|
self.candidate_id = candidate_id
|
|
80
84
|
self.hitch_timeout_ms = int(hitch_timeout_ms)
|
|
81
85
|
self.agent_args = list(agent_args or [])
|
|
86
|
+
raw_credential_names = list(credential_names or [])
|
|
87
|
+
if (
|
|
88
|
+
any(not isinstance(name, str) or re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name) is None for name in raw_credential_names)
|
|
89
|
+
or len(set(raw_credential_names)) != len(raw_credential_names)
|
|
90
|
+
):
|
|
91
|
+
raise ValueError("credential_names must contain unique environment variable names")
|
|
92
|
+
self.credential_names = sorted(raw_credential_names)
|
|
82
93
|
if workdir is not None and (not isinstance(workdir, str) or not workdir.strip()):
|
|
83
94
|
raise ValueError("workdir must be a non-empty string when provided")
|
|
84
95
|
self.workdir = workdir.strip() if isinstance(workdir, str) else None
|
|
@@ -90,13 +101,13 @@ class HitchHarborAgent(BaseAgent):
|
|
|
90
101
|
if logical_attempt is not None and (isinstance(logical_attempt, bool) or not isinstance(logical_attempt, int) or logical_attempt < 1):
|
|
91
102
|
raise ValueError("logical_attempt must be a positive integer")
|
|
92
103
|
self.logical_attempt = logical_attempt
|
|
104
|
+
self.model_capture = _validate_model_capture(model_capture)
|
|
93
105
|
self._hitch_version: str | None = None
|
|
94
106
|
self._entrypoint: str | None = None
|
|
95
107
|
self._artifact_manifest: dict[str, Any] | None = None
|
|
96
108
|
self._artifact_host_directory: Path | None = None
|
|
97
109
|
self._artifact_uploaded = False
|
|
98
110
|
self._artifact_transport_status: str | None = None
|
|
99
|
-
self._local_manifest: dict[str, Any] | None = None
|
|
100
111
|
|
|
101
112
|
@staticmethod
|
|
102
113
|
def name() -> str:
|
|
@@ -106,6 +117,13 @@ class HitchHarborAgent(BaseAgent):
|
|
|
106
117
|
return self._hitch_version
|
|
107
118
|
|
|
108
119
|
async def setup(self, environment: BaseEnvironment) -> None:
|
|
120
|
+
started_ns = time.monotonic_ns()
|
|
121
|
+
try:
|
|
122
|
+
await self._setup(environment)
|
|
123
|
+
finally:
|
|
124
|
+
self._write_phase_timing("setup", started_ns)
|
|
125
|
+
|
|
126
|
+
async def _setup(self, environment: BaseEnvironment) -> None:
|
|
109
127
|
if not self.hitch_runtime_dir.is_dir():
|
|
110
128
|
raise RuntimeError(f"Hitch runtime directory does not exist: {self.hitch_runtime_dir}")
|
|
111
129
|
# The manifest declares the CLI entrypoint (relative to the upload
|
|
@@ -129,22 +147,13 @@ class HitchHarborAgent(BaseAgent):
|
|
|
129
147
|
# and the actual container upload, spec §4.6).
|
|
130
148
|
self._verify_manifest_identity(manifest)
|
|
131
149
|
self._verify_payload(manifest)
|
|
132
|
-
if self.harness_artifact is
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
try:
|
|
140
|
-
self._verify_harness_artifact_cache_host()
|
|
141
|
-
except Exception as error:
|
|
142
|
-
raise RuntimeError(f"hitch-artifact-cache: {error}") from error
|
|
143
|
-
if self.local_source_transport is not None:
|
|
144
|
-
try:
|
|
145
|
-
self._local_manifest = self._verify_local_source_host()
|
|
146
|
-
except Exception as error:
|
|
147
|
-
raise RuntimeError(f"hitch-local-source-materialize: {error}") from error
|
|
150
|
+
if self.harness_artifact is None:
|
|
151
|
+
raise RuntimeError("hitch-artifact-materialize: Harbor requires a dedicated-builder artifact")
|
|
152
|
+
try:
|
|
153
|
+
self._artifact_manifest = self._verify_harness_artifact_host()
|
|
154
|
+
self._artifact_host_directory = Path(str(self.harness_artifact["directory"]))
|
|
155
|
+
except Exception as error:
|
|
156
|
+
raise RuntimeError(f"hitch-artifact-materialize: {error}") from error
|
|
148
157
|
payload_dir = self.hitch_runtime_dir / "payload"
|
|
149
158
|
if not payload_dir.is_dir():
|
|
150
159
|
raise RuntimeError(f"Hitch runtime bundle has no payload directory: {self.hitch_runtime_dir}")
|
|
@@ -156,46 +165,18 @@ class HitchHarborAgent(BaseAgent):
|
|
|
156
165
|
await self._ensure_node(environment)
|
|
157
166
|
platform = await self._container_platform(environment)
|
|
158
167
|
node_version = await self._container_node_version(environment)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
await self._release_artifact_cache_lock(cache_lock)
|
|
172
|
-
cache_lock = None
|
|
173
|
-
await self._upload_harness_artifact(environment, self._artifact_host_directory)
|
|
174
|
-
self._artifact_transport_status = "host_cache_hit"
|
|
175
|
-
|
|
176
|
-
if self._local_manifest is not None and not self._artifact_uploaded:
|
|
177
|
-
try:
|
|
178
|
-
await self._upload_and_materialize_local_source(environment, self._local_manifest)
|
|
179
|
-
except Exception as error:
|
|
180
|
-
raise RuntimeError(f"hitch-local-source-materialize: {error}") from error
|
|
181
|
-
entry = self._remote_entry(entrypoint)
|
|
182
|
-
version = await self._exec(environment, f"{self._node_prefix()} node {entry} --version")
|
|
183
|
-
self._hitch_version = (version.stdout or "").strip() or None
|
|
184
|
-
if not self._artifact_uploaded:
|
|
185
|
-
prepared = await self._prepare_harness_in_container(environment, entry)
|
|
186
|
-
if cache_lock is not None:
|
|
187
|
-
try:
|
|
188
|
-
self._artifact_manifest, self._artifact_host_directory = await self._cache_container_artifact(
|
|
189
|
-
environment, prepared, platform, node_version
|
|
190
|
-
)
|
|
191
|
-
self._artifact_transport_status = "host_cache_populated"
|
|
192
|
-
except Exception:
|
|
193
|
-
self._artifact_transport_status = "host_cache_write_failed"
|
|
194
|
-
else:
|
|
195
|
-
self._artifact_transport_status = "container_prepare"
|
|
196
|
-
finally:
|
|
197
|
-
if cache_lock is not None:
|
|
198
|
-
await self._release_artifact_cache_lock(cache_lock)
|
|
168
|
+
if self._artifact_manifest is None or not self._artifact_compatible(
|
|
169
|
+
self._artifact_manifest, platform, node_version
|
|
170
|
+
):
|
|
171
|
+
raise RuntimeError(
|
|
172
|
+
"hitch-artifact-platform: dedicated-builder artifact is incompatible with "
|
|
173
|
+
f"trial runtime {platform}/{node_version}"
|
|
174
|
+
)
|
|
175
|
+
await self._upload_harness_artifact(environment, self._artifact_host_directory)
|
|
176
|
+
self._artifact_transport_status = "dedicated_builder_upload"
|
|
177
|
+
entry = self._remote_entry(entrypoint)
|
|
178
|
+
version = await self._exec(environment, f"{self._node_prefix()} node {entry} --version")
|
|
179
|
+
self._hitch_version = (version.stdout or "").strip() or None
|
|
199
180
|
|
|
200
181
|
async def _resolve_workdir(self, environment: BaseEnvironment) -> str:
|
|
201
182
|
"""Resolve Harbor's effective task directory and prove it is usable.
|
|
@@ -302,6 +283,26 @@ class HitchHarborAgent(BaseAgent):
|
|
|
302
283
|
raise RuntimeError("Hitch agent setup() must resolve the Harbor task working directory before run()")
|
|
303
284
|
return self.workdir
|
|
304
285
|
|
|
286
|
+
def _write_phase_timing(self, phase: str, started_ns: int) -> None:
|
|
287
|
+
if phase not in {"setup", "agent"}:
|
|
288
|
+
raise ValueError("invalid Harbor agent phase timing")
|
|
289
|
+
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
target = self.logs_dir / "hitch-phase-timings.json"
|
|
291
|
+
value: dict[str, Any] = {"schema_version": "1", "phases": {}}
|
|
292
|
+
try:
|
|
293
|
+
existing = json.loads(target.read_text(encoding="utf-8")) if target.is_file() else None
|
|
294
|
+
if isinstance(existing, dict) and existing.get("schema_version") == "1" and isinstance(existing.get("phases"), dict):
|
|
295
|
+
value = existing
|
|
296
|
+
except (OSError, json.JSONDecodeError):
|
|
297
|
+
pass
|
|
298
|
+
value["phases"][phase] = {
|
|
299
|
+
"duration_ms": max(0, (time.monotonic_ns() - started_ns) // 1_000_000),
|
|
300
|
+
"completed_at": datetime.now(timezone.utc).isoformat(),
|
|
301
|
+
}
|
|
302
|
+
temporary = target.with_suffix(".json.tmp")
|
|
303
|
+
temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
304
|
+
temporary.replace(target)
|
|
305
|
+
|
|
305
306
|
def _verify_harness_artifact_host(self) -> dict[str, Any]:
|
|
306
307
|
"""Pin host artifact metadata before Harbor copies the directory."""
|
|
307
308
|
transport = self.harness_artifact or {}
|
|
@@ -356,6 +357,18 @@ class HitchHarborAgent(BaseAgent):
|
|
|
356
357
|
raise RuntimeError("prepared artifact Node.js version is invalid")
|
|
357
358
|
return manifest
|
|
358
359
|
|
|
360
|
+
@staticmethod
|
|
361
|
+
def _assert_regular_host_file(path: Path, label: str, maximum: int) -> Any:
|
|
362
|
+
try:
|
|
363
|
+
info = path.lstat()
|
|
364
|
+
except OSError as error:
|
|
365
|
+
raise RuntimeError(f"{label} is missing or unreadable") from error
|
|
366
|
+
if not stat_module.S_ISREG(info.st_mode):
|
|
367
|
+
raise RuntimeError(f"{label} must be a regular file")
|
|
368
|
+
if info.st_size > maximum:
|
|
369
|
+
raise RuntimeError(f"{label} exceeds the size limit ({maximum} bytes)")
|
|
370
|
+
return info
|
|
371
|
+
|
|
359
372
|
async def _container_platform(self, environment: BaseEnvironment) -> str:
|
|
360
373
|
result = await self._exec(
|
|
361
374
|
environment,
|
|
@@ -390,448 +403,6 @@ class HitchHarborAgent(BaseAgent):
|
|
|
390
403
|
await environment.upload_dir(directory, HARNESS_ARTIFACT_REMOTE_ROOT)
|
|
391
404
|
self._artifact_uploaded = True
|
|
392
405
|
|
|
393
|
-
async def _prepare_harness_in_container(
|
|
394
|
-
self,
|
|
395
|
-
environment: BaseEnvironment,
|
|
396
|
-
entry: str,
|
|
397
|
-
) -> ExecResult:
|
|
398
|
-
prepare = " ".join(
|
|
399
|
-
[
|
|
400
|
-
self._node_prefix(),
|
|
401
|
-
f"HITCH_ROOT={HITCH_CONTAINER_STATE_ROOT}",
|
|
402
|
-
*(["HITCH_HARBOR_INTERNAL=1"] if self._local_manifest is not None else []),
|
|
403
|
-
f"node {entry} prepare",
|
|
404
|
-
shlex.quote(self.harness_ref),
|
|
405
|
-
*self._local_source_cli_args(),
|
|
406
|
-
"--json",
|
|
407
|
-
]
|
|
408
|
-
)
|
|
409
|
-
return await self._exec(environment, prepare)
|
|
410
|
-
|
|
411
|
-
def _verify_harness_artifact_cache_host(self) -> None:
|
|
412
|
-
cache = self.harness_artifact_cache_dir
|
|
413
|
-
if cache is None or not cache.is_absolute():
|
|
414
|
-
raise RuntimeError("prepared artifact cache path must be absolute")
|
|
415
|
-
try:
|
|
416
|
-
info = cache.lstat()
|
|
417
|
-
except OSError as error:
|
|
418
|
-
raise RuntimeError("prepared artifact cache is missing or unreadable") from error
|
|
419
|
-
if not stat_module.S_ISDIR(info.st_mode) or cache.is_symlink():
|
|
420
|
-
raise RuntimeError("prepared artifact cache must be a regular directory")
|
|
421
|
-
for name in ("artifacts", "locks", "tmp", "invalid"):
|
|
422
|
-
child = cache / name
|
|
423
|
-
child.mkdir(mode=0o700, exist_ok=True)
|
|
424
|
-
child_info = child.lstat()
|
|
425
|
-
if not stat_module.S_ISDIR(child_info.st_mode) or child.is_symlink():
|
|
426
|
-
raise RuntimeError(f"prepared artifact cache {name} path must be a regular directory")
|
|
427
|
-
|
|
428
|
-
def _artifact_cache_key(self, platform: str, node_version: str) -> str:
|
|
429
|
-
payload = json.dumps(
|
|
430
|
-
[
|
|
431
|
-
self.revision_identity,
|
|
432
|
-
self.harness_artifact.get("adapter_version") if self.harness_artifact else None,
|
|
433
|
-
self.harness_artifact.get("recipe_version") if self.harness_artifact else None,
|
|
434
|
-
platform,
|
|
435
|
-
node_version,
|
|
436
|
-
],
|
|
437
|
-
separators=(",", ":"),
|
|
438
|
-
).encode("utf-8")
|
|
439
|
-
return hashlib.sha256(payload).hexdigest()
|
|
440
|
-
|
|
441
|
-
async def _acquire_artifact_cache_lock(self, platform: str, node_version: str) -> Any:
|
|
442
|
-
cache = self.harness_artifact_cache_dir
|
|
443
|
-
if cache is None:
|
|
444
|
-
raise RuntimeError("prepared artifact cache is unavailable")
|
|
445
|
-
handle = (cache / "locks" / f"{self._artifact_cache_key(platform, node_version)}.lock").open("a+b")
|
|
446
|
-
try:
|
|
447
|
-
while True:
|
|
448
|
-
try:
|
|
449
|
-
self._try_lock_file(handle)
|
|
450
|
-
return handle
|
|
451
|
-
except OSError as error:
|
|
452
|
-
if error.errno not in (errno.EACCES, errno.EAGAIN):
|
|
453
|
-
raise
|
|
454
|
-
await asyncio.sleep(0.1)
|
|
455
|
-
except BaseException:
|
|
456
|
-
handle.close()
|
|
457
|
-
raise
|
|
458
|
-
|
|
459
|
-
@staticmethod
|
|
460
|
-
def _try_lock_file(handle: Any) -> None:
|
|
461
|
-
if os.name == "nt":
|
|
462
|
-
import msvcrt
|
|
463
|
-
handle.seek(0)
|
|
464
|
-
if not handle.read(1):
|
|
465
|
-
handle.write(b"\0")
|
|
466
|
-
handle.flush()
|
|
467
|
-
handle.seek(0)
|
|
468
|
-
msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
|
|
469
|
-
return
|
|
470
|
-
import fcntl
|
|
471
|
-
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
472
|
-
|
|
473
|
-
@staticmethod
|
|
474
|
-
async def _release_artifact_cache_lock(handle: Any) -> None:
|
|
475
|
-
try:
|
|
476
|
-
if os.name == "nt":
|
|
477
|
-
import msvcrt
|
|
478
|
-
handle.seek(0)
|
|
479
|
-
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
|
480
|
-
else:
|
|
481
|
-
import fcntl
|
|
482
|
-
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
483
|
-
finally:
|
|
484
|
-
handle.close()
|
|
485
|
-
|
|
486
|
-
def _find_cached_harness_artifact(
|
|
487
|
-
self,
|
|
488
|
-
platform: str,
|
|
489
|
-
node_version: str,
|
|
490
|
-
) -> tuple[dict[str, Any], Path] | None:
|
|
491
|
-
cache = self.harness_artifact_cache_dir
|
|
492
|
-
if cache is None:
|
|
493
|
-
return None
|
|
494
|
-
requested = self.harness_artifact or {}
|
|
495
|
-
for directory in sorted((cache / "artifacts").iterdir(), key=lambda item: item.name):
|
|
496
|
-
if not re.fullmatch(r"[0-9a-f]{64}", directory.name):
|
|
497
|
-
continue
|
|
498
|
-
try:
|
|
499
|
-
manifest = json.loads((directory / "artifact.json").read_text(encoding="utf-8"))
|
|
500
|
-
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
501
|
-
continue
|
|
502
|
-
if not isinstance(manifest, dict) or (
|
|
503
|
-
manifest.get("revision_identity") != self.revision_identity
|
|
504
|
-
or manifest.get("adapter_version") != requested.get("adapter_version")
|
|
505
|
-
or manifest.get("recipe_version") != requested.get("recipe_version")
|
|
506
|
-
or not self._artifact_compatible(manifest, platform, node_version)
|
|
507
|
-
):
|
|
508
|
-
continue
|
|
509
|
-
try:
|
|
510
|
-
return self._verify_cached_harness_artifact(directory, platform, node_version), directory
|
|
511
|
-
except Exception:
|
|
512
|
-
self._quarantine_cached_artifact(directory)
|
|
513
|
-
return None
|
|
514
|
-
|
|
515
|
-
async def _cache_container_artifact(
|
|
516
|
-
self,
|
|
517
|
-
environment: BaseEnvironment,
|
|
518
|
-
prepared: ExecResult,
|
|
519
|
-
platform: str,
|
|
520
|
-
node_version: str,
|
|
521
|
-
) -> tuple[dict[str, Any], Path]:
|
|
522
|
-
try:
|
|
523
|
-
payload = json.loads(prepared.stdout or "")
|
|
524
|
-
artifact = payload["artifact"]
|
|
525
|
-
except (KeyError, TypeError, json.JSONDecodeError) as error:
|
|
526
|
-
raise RuntimeError("container prepare returned invalid artifact metadata") from error
|
|
527
|
-
artifact_id = artifact.get("artifact_id") if isinstance(artifact, dict) else None
|
|
528
|
-
if not isinstance(artifact_id, str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", artifact_id):
|
|
529
|
-
raise RuntimeError("container prepare returned an invalid artifact ID")
|
|
530
|
-
if (
|
|
531
|
-
artifact.get("revision_identity") != self.revision_identity
|
|
532
|
-
or not self._artifact_compatible(artifact, platform, node_version)
|
|
533
|
-
):
|
|
534
|
-
raise RuntimeError("container prepare returned an incompatible artifact")
|
|
535
|
-
cache = self.harness_artifact_cache_dir
|
|
536
|
-
if cache is None:
|
|
537
|
-
raise RuntimeError("prepared artifact cache is unavailable")
|
|
538
|
-
staging = cache / "tmp" / f"artifact-{uuid.uuid4().hex}"
|
|
539
|
-
staging.mkdir(mode=0o700)
|
|
540
|
-
try:
|
|
541
|
-
remote = f'{HITCH_CONTAINER_STATE_ROOT}/store/artifacts/{artifact_id.removeprefix("sha256:")}'
|
|
542
|
-
await environment.download_dir(remote, staging)
|
|
543
|
-
manifest = self._verify_cached_harness_artifact(staging, platform, node_version)
|
|
544
|
-
if manifest.get("artifact_id") != artifact_id:
|
|
545
|
-
raise RuntimeError("downloaded artifact ID differs from container prepare output")
|
|
546
|
-
destination = cache / "artifacts" / artifact_id.removeprefix("sha256:")
|
|
547
|
-
if destination.exists():
|
|
548
|
-
existing = self._verify_cached_harness_artifact(destination, platform, node_version)
|
|
549
|
-
shutil.rmtree(staging)
|
|
550
|
-
return existing, destination
|
|
551
|
-
staging.rename(destination)
|
|
552
|
-
return manifest, destination
|
|
553
|
-
finally:
|
|
554
|
-
if staging.exists():
|
|
555
|
-
shutil.rmtree(staging, ignore_errors=True)
|
|
556
|
-
|
|
557
|
-
def _verify_cached_harness_artifact(
|
|
558
|
-
self,
|
|
559
|
-
directory: Path,
|
|
560
|
-
platform: str,
|
|
561
|
-
node_version: str,
|
|
562
|
-
) -> dict[str, Any]:
|
|
563
|
-
info = directory.lstat()
|
|
564
|
-
if not stat_module.S_ISDIR(info.st_mode) or directory.is_symlink():
|
|
565
|
-
raise RuntimeError("cached harness artifact is not a regular directory")
|
|
566
|
-
artifact_file = directory / "artifact.json"
|
|
567
|
-
self._assert_regular_host_file(artifact_file, "cached harness artifact manifest", 1024 * 1024)
|
|
568
|
-
manifest = json.loads(artifact_file.read_text(encoding="utf-8"))
|
|
569
|
-
if not isinstance(manifest, dict) or manifest.get("schema_version") != HARNESS_ARTIFACT_MANIFEST_VERSION:
|
|
570
|
-
raise RuntimeError("cached harness artifact manifest schema is invalid")
|
|
571
|
-
for field in ("artifact_id", "artifact_integrity", "entrypoint_integrity", "revision_identity"):
|
|
572
|
-
if not re.fullmatch(r"sha256:[0-9a-f]{64}", str(manifest.get(field, ""))):
|
|
573
|
-
raise RuntimeError(f"cached harness artifact {field} is invalid")
|
|
574
|
-
if (
|
|
575
|
-
manifest.get("artifact_id") != f"sha256:{directory.name}" and directory.parent.name == "artifacts"
|
|
576
|
-
):
|
|
577
|
-
raise RuntimeError("cached harness artifact directory does not match its ID")
|
|
578
|
-
if (
|
|
579
|
-
manifest.get("revision_identity") != self.revision_identity
|
|
580
|
-
or manifest.get("resolved_revision", {}).get("identity") != self.revision_identity
|
|
581
|
-
or manifest.get("harness_id") != self.harness_ref.split("@", 1)[0]
|
|
582
|
-
or manifest.get("adapter_version") != (self.harness_artifact or {}).get("adapter_version")
|
|
583
|
-
or manifest.get("recipe_version") != (self.harness_artifact or {}).get("recipe_version")
|
|
584
|
-
or not self._artifact_compatible(manifest, platform, node_version)
|
|
585
|
-
or manifest.get("source_type") == "installed"
|
|
586
|
-
):
|
|
587
|
-
raise RuntimeError("cached harness artifact identity is incompatible")
|
|
588
|
-
entrypoint = manifest.get("entrypoint")
|
|
589
|
-
if not isinstance(entrypoint, str) or not entrypoint or PurePosixPath(entrypoint).is_absolute():
|
|
590
|
-
raise RuntimeError("cached harness artifact entrypoint is invalid")
|
|
591
|
-
parts = PurePosixPath(entrypoint).parts
|
|
592
|
-
if any(part in ("", ".", "..") for part in parts):
|
|
593
|
-
raise RuntimeError("cached harness artifact entrypoint escapes its directory")
|
|
594
|
-
executable = directory.joinpath(*parts)
|
|
595
|
-
if not executable.is_file():
|
|
596
|
-
raise RuntimeError("cached harness artifact entrypoint is missing")
|
|
597
|
-
if self._sha256_file(executable) != manifest["entrypoint_integrity"]:
|
|
598
|
-
raise RuntimeError("cached harness artifact entrypoint integrity mismatch")
|
|
599
|
-
if self._artifact_directory_integrity(directory) != manifest["artifact_integrity"]:
|
|
600
|
-
raise RuntimeError("cached harness artifact content integrity mismatch")
|
|
601
|
-
return manifest
|
|
602
|
-
|
|
603
|
-
@classmethod
|
|
604
|
-
def _artifact_directory_integrity(cls, root: Path) -> str:
|
|
605
|
-
digest = hashlib.sha256()
|
|
606
|
-
resolved_root = root.resolve()
|
|
607
|
-
|
|
608
|
-
def walk(directory: Path, relative: PurePosixPath, top_level: bool) -> None:
|
|
609
|
-
for entry in sorted(os.scandir(directory), key=lambda item: item.name):
|
|
610
|
-
if top_level and entry.name == "artifact.json":
|
|
611
|
-
continue
|
|
612
|
-
child_relative = relative / entry.name
|
|
613
|
-
child = Path(entry.path)
|
|
614
|
-
info = child.lstat()
|
|
615
|
-
mode = info.st_mode & 0o7777
|
|
616
|
-
encoded_relative = child_relative.as_posix()
|
|
617
|
-
if stat_module.S_ISDIR(info.st_mode):
|
|
618
|
-
digest.update(f"d\0{encoded_relative}\0{mode}\0".encode("utf-8"))
|
|
619
|
-
walk(child, child_relative, False)
|
|
620
|
-
elif stat_module.S_ISREG(info.st_mode):
|
|
621
|
-
digest.update(f"f\0{encoded_relative}\0{mode}\0{info.st_size}\0".encode("utf-8"))
|
|
622
|
-
with child.open("rb") as handle:
|
|
623
|
-
while chunk := handle.read(1024 * 1024):
|
|
624
|
-
digest.update(chunk)
|
|
625
|
-
digest.update(b"\0")
|
|
626
|
-
elif stat_module.S_ISLNK(info.st_mode):
|
|
627
|
-
target = os.readlink(child)
|
|
628
|
-
resolved_target = (child.parent / target).resolve()
|
|
629
|
-
if resolved_target != resolved_root and resolved_root not in resolved_target.parents:
|
|
630
|
-
raise RuntimeError("cached harness artifact symlink escapes its directory")
|
|
631
|
-
digest.update(f"l\0{encoded_relative}\0{target}\0".encode("utf-8"))
|
|
632
|
-
else:
|
|
633
|
-
raise RuntimeError("cached harness artifact contains a special file")
|
|
634
|
-
|
|
635
|
-
walk(root, PurePosixPath(), True)
|
|
636
|
-
return "sha256:" + digest.hexdigest()
|
|
637
|
-
|
|
638
|
-
def _quarantine_cached_artifact(self, directory: Path) -> None:
|
|
639
|
-
cache = self.harness_artifact_cache_dir
|
|
640
|
-
if cache is None or not directory.exists():
|
|
641
|
-
return
|
|
642
|
-
target = cache / "invalid" / f"{directory.name}-{uuid.uuid4().hex}"
|
|
643
|
-
try:
|
|
644
|
-
directory.rename(target)
|
|
645
|
-
except OSError:
|
|
646
|
-
pass
|
|
647
|
-
|
|
648
|
-
def _verify_local_source_host(self) -> dict[str, Any]:
|
|
649
|
-
"""Validate the independent local-source handoff immediately before upload."""
|
|
650
|
-
transport = self.local_source_transport or {}
|
|
651
|
-
required = {
|
|
652
|
-
"kind", "manifest_path", "payload_path", "locked_resolution_path", "harness_id",
|
|
653
|
-
"resolution_identity", "commit", "tree", "payload_sha256", "payload_bytes",
|
|
654
|
-
"object_count", "file_count",
|
|
655
|
-
}
|
|
656
|
-
if set(transport) != required:
|
|
657
|
-
raise RuntimeError("local source transport metadata fields are invalid")
|
|
658
|
-
manifest_path = Path(str(transport["manifest_path"]))
|
|
659
|
-
payload_path = Path(str(transport["payload_path"]))
|
|
660
|
-
resolution_path = Path(str(transport["locked_resolution_path"]))
|
|
661
|
-
if not all(candidate.is_absolute() for candidate in (manifest_path, payload_path, resolution_path)):
|
|
662
|
-
raise RuntimeError("local source transport host paths must be absolute")
|
|
663
|
-
self._assert_regular_host_file(manifest_path, "local source manifest", 64 * 1024)
|
|
664
|
-
payload_stat = self._assert_regular_host_file(payload_path, "local source payload", LOCAL_GIT_TRANSPORT_MAX_BYTES)
|
|
665
|
-
self._assert_regular_host_file(resolution_path, "local source locked resolution", 1024 * 1024)
|
|
666
|
-
try:
|
|
667
|
-
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
668
|
-
resolution = json.loads(resolution_path.read_text(encoding="utf-8"))
|
|
669
|
-
except (OSError, UnicodeError, json.JSONDecodeError) as error:
|
|
670
|
-
raise RuntimeError(f"local source metadata is unreadable: {error}") from error
|
|
671
|
-
self._validate_local_manifest(manifest)
|
|
672
|
-
pinned = {
|
|
673
|
-
"kind": manifest["kind"],
|
|
674
|
-
"harness_id": manifest["harness_id"],
|
|
675
|
-
"resolution_identity": manifest["resolution_identity"],
|
|
676
|
-
"commit": manifest["commit"],
|
|
677
|
-
"tree": manifest["tree"],
|
|
678
|
-
"payload_sha256": manifest["payload_sha256"],
|
|
679
|
-
"payload_bytes": manifest["payload_bytes"],
|
|
680
|
-
"object_count": manifest["object_count"],
|
|
681
|
-
"file_count": manifest["file_count"],
|
|
682
|
-
}
|
|
683
|
-
if any(transport.get(key) != value for key, value in pinned.items()):
|
|
684
|
-
raise RuntimeError("local source transport metadata does not match its manifest")
|
|
685
|
-
if manifest["resolution_identity"] != self.revision_identity:
|
|
686
|
-
raise RuntimeError("local source resolution identity does not match the job-pinned identity")
|
|
687
|
-
if self.harness_ref != f'{manifest["harness_id"]}@commit:{manifest["commit"]}':
|
|
688
|
-
raise RuntimeError("local source commit does not match the job-pinned harness ref")
|
|
689
|
-
if payload_stat.st_size != manifest["payload_bytes"]:
|
|
690
|
-
raise RuntimeError("local source payload size does not match its manifest")
|
|
691
|
-
actual_digest = self._sha256_file(payload_path)
|
|
692
|
-
if actual_digest != manifest["payload_sha256"]:
|
|
693
|
-
raise RuntimeError("local source payload digest does not match its manifest")
|
|
694
|
-
if not isinstance(resolution, dict) or (
|
|
695
|
-
resolution.get("harness_id") != manifest["harness_id"]
|
|
696
|
-
or resolution.get("identity") != manifest["resolution_identity"]
|
|
697
|
-
or resolution.get("revision", {}).get("commit") != manifest["commit"]
|
|
698
|
-
or resolution.get("source", {}).get("type") != "git"
|
|
699
|
-
or resolution.get("source", {}).get("registered") is not False
|
|
700
|
-
):
|
|
701
|
-
raise RuntimeError("local source locked resolution does not match its manifest")
|
|
702
|
-
return manifest
|
|
703
|
-
|
|
704
|
-
@staticmethod
|
|
705
|
-
def _assert_regular_host_file(path: Path, label: str, maximum: int) -> Any:
|
|
706
|
-
try:
|
|
707
|
-
info = path.lstat()
|
|
708
|
-
except OSError as error:
|
|
709
|
-
raise RuntimeError(f"{label} is missing or unreadable") from error
|
|
710
|
-
if not stat_module.S_ISREG(info.st_mode):
|
|
711
|
-
raise RuntimeError(f"{label} must be a regular file")
|
|
712
|
-
if info.st_size > maximum:
|
|
713
|
-
raise RuntimeError(f"{label} exceeds the size limit ({maximum} bytes)")
|
|
714
|
-
return info
|
|
715
|
-
|
|
716
|
-
@staticmethod
|
|
717
|
-
def _sha256_file(path: Path) -> str:
|
|
718
|
-
digest = hashlib.sha256()
|
|
719
|
-
with path.open("rb") as handle:
|
|
720
|
-
while chunk := handle.read(1024 * 1024):
|
|
721
|
-
digest.update(chunk)
|
|
722
|
-
return "sha256:" + digest.hexdigest()
|
|
723
|
-
|
|
724
|
-
@staticmethod
|
|
725
|
-
def _validate_local_manifest(manifest: Any) -> None:
|
|
726
|
-
allowed = {
|
|
727
|
-
"schema_version", "kind", "harness_id", "resolution_identity", "commit", "tree",
|
|
728
|
-
"payload_sha256", "payload_bytes", "object_count", "file_count", "created_at",
|
|
729
|
-
}
|
|
730
|
-
if not isinstance(manifest, dict) or set(manifest) != allowed:
|
|
731
|
-
raise RuntimeError("local source manifest fields are invalid")
|
|
732
|
-
if manifest.get("schema_version") != LOCAL_GIT_TRANSPORT_MANIFEST_VERSION:
|
|
733
|
-
raise RuntimeError("unsupported local source transport manifest schema")
|
|
734
|
-
if manifest.get("kind") != "local-git-commit":
|
|
735
|
-
raise RuntimeError("unsupported local source transport kind")
|
|
736
|
-
if not isinstance(manifest.get("harness_id"), str) or not re.fullmatch(r"[a-z][a-z0-9-]*", manifest["harness_id"]):
|
|
737
|
-
raise RuntimeError("local source manifest harness id is invalid")
|
|
738
|
-
if not isinstance(manifest.get("resolution_identity"), str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", manifest["resolution_identity"]):
|
|
739
|
-
raise RuntimeError("local source manifest resolution identity is invalid")
|
|
740
|
-
commit = manifest.get("commit")
|
|
741
|
-
if not isinstance(commit, str) or not re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", commit):
|
|
742
|
-
raise RuntimeError("local source manifest commit is not a full lowercase OID")
|
|
743
|
-
tree = manifest.get("tree")
|
|
744
|
-
if not isinstance(tree, str) or not re.fullmatch(rf"[0-9a-f]{{{len(commit)}}}", tree):
|
|
745
|
-
raise RuntimeError("local source manifest tree is invalid")
|
|
746
|
-
if not isinstance(manifest.get("payload_sha256"), str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", manifest["payload_sha256"]):
|
|
747
|
-
raise RuntimeError("local source manifest payload digest is invalid")
|
|
748
|
-
for key, maximum in (
|
|
749
|
-
("payload_bytes", LOCAL_GIT_TRANSPORT_MAX_BYTES),
|
|
750
|
-
("object_count", 100_000),
|
|
751
|
-
("file_count", 50_000),
|
|
752
|
-
):
|
|
753
|
-
value = manifest.get(key)
|
|
754
|
-
if not isinstance(value, int) or isinstance(value, bool) or value < 0 or value > maximum:
|
|
755
|
-
raise RuntimeError(f"local source manifest {key} is invalid")
|
|
756
|
-
if not isinstance(manifest.get("created_at"), str) or not manifest["created_at"]:
|
|
757
|
-
raise RuntimeError("local source manifest created_at is invalid")
|
|
758
|
-
try:
|
|
759
|
-
datetime.fromisoformat(manifest["created_at"].replace("Z", "+00:00"))
|
|
760
|
-
except ValueError as error:
|
|
761
|
-
raise RuntimeError("local source manifest created_at is invalid") from error
|
|
762
|
-
|
|
763
|
-
async def _upload_and_materialize_local_source(
|
|
764
|
-
self,
|
|
765
|
-
environment: BaseEnvironment,
|
|
766
|
-
manifest: dict[str, Any],
|
|
767
|
-
) -> None:
|
|
768
|
-
transport = self.local_source_transport or {}
|
|
769
|
-
await self._exec(environment, f"mkdir -p {LOCAL_GIT_REMOTE_ROOT} && chmod 700 {LOCAL_GIT_REMOTE_ROOT}")
|
|
770
|
-
await environment.upload_file(Path(str(transport["manifest_path"])), f"{LOCAL_GIT_REMOTE_ROOT}/manifest.json")
|
|
771
|
-
await environment.upload_file(Path(str(transport["payload_path"])), f"{LOCAL_GIT_REMOTE_ROOT}/payload.pack")
|
|
772
|
-
await environment.upload_file(Path(str(transport["locked_resolution_path"])), f"{LOCAL_GIT_REMOTE_ROOT}/resolution.json")
|
|
773
|
-
await self._exec(environment, f"chmod 600 {LOCAL_GIT_REMOTE_ROOT}/manifest.json {LOCAL_GIT_REMOTE_ROOT}/payload.pack {LOCAL_GIT_REMOTE_ROOT}/resolution.json")
|
|
774
|
-
# Verify both the uploaded manifest fields and payload bytes inside the
|
|
775
|
-
# trial before importing any Git object. All interpolated values passed
|
|
776
|
-
# to the shell have already been restricted to digest/OID grammars.
|
|
777
|
-
verifier = """
|
|
778
|
-
const fs = require('node:fs');
|
|
779
|
-
const crypto = require('node:crypto');
|
|
780
|
-
const expected = JSON.parse(process.argv[1]);
|
|
781
|
-
const manifest = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
782
|
-
if (JSON.stringify(Object.keys(manifest).sort()) !== JSON.stringify(Object.keys(expected).sort())) throw new Error('manifest fields mismatch');
|
|
783
|
-
for (const key of Object.keys(expected)) if (manifest[key] !== expected[key]) throw new Error(`manifest mismatch: ${key}`);
|
|
784
|
-
if (fs.statSync(process.argv[3]).size !== expected.payload_bytes) throw new Error('payload size mismatch');
|
|
785
|
-
(async () => {
|
|
786
|
-
const hash = crypto.createHash('sha256');
|
|
787
|
-
for await (const chunk of fs.createReadStream(process.argv[3])) hash.update(chunk);
|
|
788
|
-
const digest = 'sha256:' + hash.digest('hex');
|
|
789
|
-
if (digest !== expected.payload_sha256) throw new Error('payload digest mismatch');
|
|
790
|
-
})().catch((error) => { console.error(error.message); process.exit(1); });
|
|
791
|
-
""".strip()
|
|
792
|
-
expected = {
|
|
793
|
-
"schema_version": manifest["schema_version"],
|
|
794
|
-
"kind": manifest["kind"],
|
|
795
|
-
"harness_id": manifest["harness_id"],
|
|
796
|
-
"resolution_identity": manifest["resolution_identity"],
|
|
797
|
-
"commit": manifest["commit"],
|
|
798
|
-
"tree": manifest["tree"],
|
|
799
|
-
"payload_sha256": manifest["payload_sha256"],
|
|
800
|
-
"payload_bytes": manifest["payload_bytes"],
|
|
801
|
-
"object_count": manifest["object_count"],
|
|
802
|
-
"file_count": manifest["file_count"],
|
|
803
|
-
"created_at": manifest["created_at"],
|
|
804
|
-
}
|
|
805
|
-
verify_command = " ".join([
|
|
806
|
-
self._node_prefix(),
|
|
807
|
-
"node", "-e", shlex.quote(verifier),
|
|
808
|
-
shlex.quote(json.dumps(expected, separators=(",", ":"))),
|
|
809
|
-
f"{LOCAL_GIT_REMOTE_ROOT}/manifest.json",
|
|
810
|
-
f"{LOCAL_GIT_REMOTE_ROOT}/payload.pack",
|
|
811
|
-
])
|
|
812
|
-
await self._exec(environment, verify_command)
|
|
813
|
-
commit = manifest["commit"]
|
|
814
|
-
tree = manifest["tree"]
|
|
815
|
-
materialize = f"""
|
|
816
|
-
set -eu
|
|
817
|
-
git init --bare {LOCAL_GIT_REMOTE_ROOT}/repo.git >/dev/null
|
|
818
|
-
git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git index-pack --stdin < {LOCAL_GIT_REMOTE_ROOT}/payload.pack >/dev/null
|
|
819
|
-
test "$(git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git rev-parse {commit}^{{commit}})" = "{commit}"
|
|
820
|
-
test "$(git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git rev-parse {commit}^{{tree}})" = "{tree}"
|
|
821
|
-
printf '%s\n' {commit} > {LOCAL_GIT_REMOTE_ROOT}/repo.git/shallow
|
|
822
|
-
git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commit}
|
|
823
|
-
"""
|
|
824
|
-
await self._exec(environment, materialize)
|
|
825
|
-
|
|
826
|
-
def _local_source_cli_args(self) -> list[str]:
|
|
827
|
-
if self._local_manifest is None or self._artifact_uploaded:
|
|
828
|
-
return []
|
|
829
|
-
return [
|
|
830
|
-
"--internal-locked-resolution", f"{LOCAL_GIT_REMOTE_ROOT}/resolution.json",
|
|
831
|
-
"--internal-local-git-manifest", f"{LOCAL_GIT_REMOTE_ROOT}/manifest.json",
|
|
832
|
-
"--internal-local-git-source", f"{LOCAL_GIT_REMOTE_ROOT}/repo.git",
|
|
833
|
-
]
|
|
834
|
-
|
|
835
406
|
def _artifact_cli_args(self) -> list[str]:
|
|
836
407
|
if not self._artifact_uploaded or self._artifact_manifest is None:
|
|
837
408
|
return []
|
|
@@ -941,6 +512,18 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
|
|
|
941
512
|
instruction: str,
|
|
942
513
|
environment: BaseEnvironment,
|
|
943
514
|
context: AgentContext,
|
|
515
|
+
) -> None:
|
|
516
|
+
started_ns = time.monotonic_ns()
|
|
517
|
+
try:
|
|
518
|
+
await self._run(instruction, environment, context)
|
|
519
|
+
finally:
|
|
520
|
+
self._write_phase_timing("agent", started_ns)
|
|
521
|
+
|
|
522
|
+
async def _run(
|
|
523
|
+
self,
|
|
524
|
+
instruction: str,
|
|
525
|
+
environment: BaseEnvironment,
|
|
526
|
+
context: AgentContext,
|
|
944
527
|
) -> None:
|
|
945
528
|
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
|
946
529
|
workdir = self._require_workdir()
|
|
@@ -1006,17 +589,18 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
|
|
|
1006
589
|
if parent_temporary is not None:
|
|
1007
590
|
parent_temporary.unlink(missing_ok=True)
|
|
1008
591
|
|
|
592
|
+
proxy_environment, proxy_health = await self._model_proxy_environment(environment, run_id)
|
|
1009
593
|
if self._entrypoint is None:
|
|
1010
594
|
raise RuntimeError("Hitch agent setup() must run before run() to resolve the runtime entrypoint")
|
|
1011
595
|
entry = self._remote_entry(self._entrypoint)
|
|
1012
596
|
arguments = [
|
|
1013
597
|
self._node_prefix(),
|
|
1014
598
|
"HITCH_ROOT=/tmp/hitch-state",
|
|
1015
|
-
*
|
|
599
|
+
*proxy_environment,
|
|
600
|
+
*(["HITCH_HARBOR_INTERNAL=1"] if self._artifact_uploaded or parent_payload is not None else []),
|
|
1016
601
|
f"node {entry} run",
|
|
1017
602
|
"--harness",
|
|
1018
603
|
shlex.quote(self.harness_ref),
|
|
1019
|
-
*self._local_source_cli_args(),
|
|
1020
604
|
*self._artifact_cli_args(),
|
|
1021
605
|
"--cwd",
|
|
1022
606
|
shlex.quote(workdir),
|
|
@@ -1041,6 +625,8 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
|
|
|
1041
625
|
arguments.extend(["--model", shlex.quote(self.model_name)])
|
|
1042
626
|
for value in self.agent_args:
|
|
1043
627
|
arguments.extend(["--agent-arg", shlex.quote(value)])
|
|
628
|
+
for name in self.credential_names:
|
|
629
|
+
arguments.extend(["--internal-credential-name", shlex.quote(name)])
|
|
1044
630
|
command = (
|
|
1045
631
|
"set -o pipefail; "
|
|
1046
632
|
+ " ".join(arguments)
|
|
@@ -1138,26 +724,17 @@ mv "$stage_dir" "$target_dir"
|
|
|
1138
724
|
"trial_id": trial_id,
|
|
1139
725
|
"task_id": task_id,
|
|
1140
726
|
"attempt": attempt,
|
|
727
|
+
"model_capture_health": proxy_health,
|
|
1141
728
|
"hitch_status": hitch_result.get("status") if hitch_result else None,
|
|
1142
729
|
"hitch_artifact_id": hitch_result.get("artifact_id") if hitch_result else None,
|
|
1143
730
|
}
|
|
1144
|
-
if self._local_manifest is not None:
|
|
1145
|
-
context.metadata["local_source_transport"] = {
|
|
1146
|
-
"kind": self._local_manifest["kind"],
|
|
1147
|
-
"commit": self._local_manifest["commit"],
|
|
1148
|
-
"tree": self._local_manifest["tree"],
|
|
1149
|
-
"resolution_identity": self._local_manifest["resolution_identity"],
|
|
1150
|
-
"payload_sha256": self._local_manifest["payload_sha256"],
|
|
1151
|
-
"payload_bytes": self._local_manifest["payload_bytes"],
|
|
1152
|
-
"status": "verified",
|
|
1153
|
-
}
|
|
1154
731
|
if self._artifact_manifest is not None:
|
|
1155
732
|
context.metadata["harness_artifact_transport"] = {
|
|
1156
733
|
"artifact_id": self._artifact_manifest["artifact_id"],
|
|
1157
734
|
"artifact_integrity": self._artifact_manifest["artifact_integrity"],
|
|
1158
735
|
"platform": self._artifact_manifest["platform"],
|
|
1159
736
|
"node_version": self._artifact_manifest.get("toolchain", {}).get("node"),
|
|
1160
|
-
"status": self._artifact_transport_status or "
|
|
737
|
+
"status": self._artifact_transport_status or "dedicated_builder_upload",
|
|
1161
738
|
}
|
|
1162
739
|
if primary_code is not None:
|
|
1163
740
|
context.metadata["hitch_bridge_error_code"] = primary_code
|
|
@@ -1181,6 +758,32 @@ mv "$stage_dir" "$target_dir"
|
|
|
1181
758
|
await self._write_bridge_error(environment, evidence)
|
|
1182
759
|
raise HitchBridgeError(primary_code, primary_message or primary_code, evidence)
|
|
1183
760
|
|
|
761
|
+
async def _model_proxy_environment(
|
|
762
|
+
self,
|
|
763
|
+
environment: BaseEnvironment,
|
|
764
|
+
run_id: str,
|
|
765
|
+
) -> tuple[list[str], str]:
|
|
766
|
+
if self.model_capture is None:
|
|
767
|
+
return [], "not-configured"
|
|
768
|
+
health = self.model_capture["health_url_template"].replace("{run_id}", run_id)
|
|
769
|
+
script = (
|
|
770
|
+
"fetch(process.argv[1],{signal:AbortSignal.timeout(5000)})"
|
|
771
|
+
".then(r=>{if(!r.ok)throw new Error('status '+r.status)})"
|
|
772
|
+
".catch(()=>{console.error('model proxy health check failed');process.exit(1)})"
|
|
773
|
+
)
|
|
774
|
+
probe = await environment.exec(
|
|
775
|
+
f"{self._node_prefix()} node -e {shlex.quote(script)} {shlex.quote(health)}"
|
|
776
|
+
)
|
|
777
|
+
if probe.return_code != 0:
|
|
778
|
+
if self.model_capture["required"]:
|
|
779
|
+
raise RuntimeError("hitch-model-proxy-health: required model proxy is unreachable")
|
|
780
|
+
return [], "degraded-unreachable"
|
|
781
|
+
base = self.model_capture["base_url_template"].replace("{run_id}", run_id)
|
|
782
|
+
return [
|
|
783
|
+
f"OPENAI_BASE_URL={shlex.quote(base.replace('{provider}', 'openai'))}",
|
|
784
|
+
f"ANTHROPIC_BASE_URL={shlex.quote(base.replace('{provider}', 'anthropic'))}",
|
|
785
|
+
], "healthy"
|
|
786
|
+
|
|
1184
787
|
@staticmethod
|
|
1185
788
|
def _parse_hitch_result(
|
|
1186
789
|
result: ExecResult,
|
|
@@ -1422,40 +1025,36 @@ process.stdout.write('sha256:' + hash.digest('hex'));
|
|
|
1422
1025
|
|
|
1423
1026
|
async def _ensure_node(self, environment: BaseEnvironment) -> None:
|
|
1424
1027
|
probe = await environment.exec(
|
|
1425
|
-
"node -e 'process.exit(
|
|
1028
|
+
f"node -e 'process.exit(process.version === \"{self.required_node_version}\" ? 0 : 1)'"
|
|
1426
1029
|
)
|
|
1427
|
-
|
|
1428
|
-
git_probe = await environment.exec("command -v git >/dev/null 2>&1")
|
|
1429
|
-
if probe.return_code == 0 and (not needs_git or git_probe.return_code == 0):
|
|
1030
|
+
if probe.return_code == 0:
|
|
1430
1031
|
return
|
|
1431
1032
|
prerequisites = """
|
|
1432
1033
|
set -eu
|
|
1433
|
-
if command -v curl >/dev/null 2>&1
|
|
1034
|
+
if command -v curl >/dev/null 2>&1; then exit 0; fi
|
|
1434
1035
|
if command -v apt-get >/dev/null 2>&1; then
|
|
1435
|
-
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates
|
|
1036
|
+
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates
|
|
1436
1037
|
elif command -v apk >/dev/null 2>&1; then
|
|
1437
|
-
apk add --no-cache curl ca-certificates
|
|
1038
|
+
apk add --no-cache curl ca-certificates bash
|
|
1438
1039
|
elif command -v dnf >/dev/null 2>&1; then
|
|
1439
|
-
dnf install -y curl ca-certificates
|
|
1040
|
+
dnf install -y curl ca-certificates
|
|
1440
1041
|
elif command -v yum >/dev/null 2>&1; then
|
|
1441
|
-
yum install -y curl ca-certificates
|
|
1042
|
+
yum install -y curl ca-certificates
|
|
1442
1043
|
else
|
|
1443
|
-
echo 'Hitch
|
|
1044
|
+
echo 'Hitch could not install the pinned Node.js runtime in this task image' >&2
|
|
1444
1045
|
exit 1
|
|
1445
1046
|
fi
|
|
1446
1047
|
"""
|
|
1447
1048
|
await self._exec(environment, prerequisites, user=0)
|
|
1448
|
-
|
|
1449
|
-
return
|
|
1450
|
-
install = """
|
|
1049
|
+
install = f"""
|
|
1451
1050
|
set -eu
|
|
1452
1051
|
export NVM_DIR=/opt/hitch-node
|
|
1453
1052
|
mkdir -p "$NVM_DIR"
|
|
1454
1053
|
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
|
|
1455
1054
|
. "$NVM_DIR/nvm.sh"
|
|
1456
|
-
nvm install
|
|
1457
|
-
nvm alias default
|
|
1458
|
-
node -e 'process.exit(
|
|
1055
|
+
nvm install {self.required_node_version.removeprefix("v")}
|
|
1056
|
+
nvm alias default {self.required_node_version.removeprefix("v")}
|
|
1057
|
+
node -e 'process.exit(process.version === "{self.required_node_version}" ? 0 : 1)'
|
|
1459
1058
|
"""
|
|
1460
1059
|
await self._exec(environment, install, user=0)
|
|
1461
1060
|
|
|
@@ -1495,6 +1094,39 @@ node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)
|
|
|
1495
1094
|
return "no diagnostic output"
|
|
1496
1095
|
|
|
1497
1096
|
|
|
1097
|
+
def _validate_model_capture(value: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
1098
|
+
if value is None:
|
|
1099
|
+
return None
|
|
1100
|
+
if not isinstance(value, dict) or set(value) != {
|
|
1101
|
+
"schema_version", "mode", "required", "topology", "base_url_template", "health_url_template"
|
|
1102
|
+
}:
|
|
1103
|
+
raise ValueError("model_capture fields are invalid")
|
|
1104
|
+
if (
|
|
1105
|
+
value.get("schema_version") != "1"
|
|
1106
|
+
or value.get("mode") not in {"proxy", "hybrid"}
|
|
1107
|
+
or not isinstance(value.get("required"), bool)
|
|
1108
|
+
or value.get("topology") != "host-side"
|
|
1109
|
+
):
|
|
1110
|
+
raise ValueError("model_capture identity is invalid")
|
|
1111
|
+
for field, provider_count in (("base_url_template", 1), ("health_url_template", 0)):
|
|
1112
|
+
template = value.get(field)
|
|
1113
|
+
if (
|
|
1114
|
+
not isinstance(template, str)
|
|
1115
|
+
or not template
|
|
1116
|
+
or len(template) > 2048
|
|
1117
|
+
or any(character in template for character in ("\x00", "\r", "\n"))
|
|
1118
|
+
or template.count("{run_id}") != 1
|
|
1119
|
+
or template.count("{provider}") != provider_count
|
|
1120
|
+
):
|
|
1121
|
+
raise ValueError(f"model_capture {field} is invalid")
|
|
1122
|
+
parsed = urlparse(
|
|
1123
|
+
template.replace("{run_id}", "run_" + "a" * 32).replace("{provider}", "openai")
|
|
1124
|
+
)
|
|
1125
|
+
if parsed.scheme not in {"http", "https"} or not parsed.hostname or parsed.username or parsed.password:
|
|
1126
|
+
raise ValueError(f"model_capture {field} URL is invalid")
|
|
1127
|
+
return dict(value)
|
|
1128
|
+
|
|
1129
|
+
|
|
1498
1130
|
def canonical_manifest_json(manifest: dict[str, Any]) -> str:
|
|
1499
1131
|
"""Canonically encode the runtime identity `{ schema_version, node_range,
|
|
1500
1132
|
entrypoints, files }` with sorted object keys and no insignificant
|