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