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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { HitchError, safeDiagnosticMessage } from "../foundation/index.js";
|
|
2
|
+
import { ResourceLedger } from "./resources.js";
|
|
3
|
+
const REQUIRED_INPUTS = new Set(["work-spec", "harness-artifact", "controller-runtime", "task-input"]);
|
|
4
|
+
export class RemoteWorkerRunner {
|
|
5
|
+
client;
|
|
6
|
+
execute;
|
|
7
|
+
signal;
|
|
8
|
+
once;
|
|
9
|
+
pollIntervalMs;
|
|
10
|
+
heartbeatIntervalMs;
|
|
11
|
+
retryIntervalMs;
|
|
12
|
+
onError;
|
|
13
|
+
releaseUnknown;
|
|
14
|
+
ledger;
|
|
15
|
+
jobs = new Map();
|
|
16
|
+
handled = 0;
|
|
17
|
+
lastHeartbeat = 0;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.client = options.client;
|
|
20
|
+
this.execute = options.execute;
|
|
21
|
+
this.signal = options.signal;
|
|
22
|
+
this.once = options.once ?? false;
|
|
23
|
+
this.pollIntervalMs = interval(options.pollIntervalMs ?? 1_000, "poll");
|
|
24
|
+
this.heartbeatIntervalMs = interval(options.heartbeatIntervalMs ?? 10_000, "heartbeat");
|
|
25
|
+
this.retryIntervalMs = interval(options.retryIntervalMs ?? 1_000, "retry");
|
|
26
|
+
this.onError = options.onError ?? (() => undefined);
|
|
27
|
+
this.releaseUnknown = options.releaseUnknown ?? (() => Promise.resolve());
|
|
28
|
+
this.ledger = new ResourceLedger(options.capacity);
|
|
29
|
+
}
|
|
30
|
+
async run() {
|
|
31
|
+
while (!this.signal?.aborted) {
|
|
32
|
+
try {
|
|
33
|
+
await this.tick();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
this.onError(error);
|
|
37
|
+
}
|
|
38
|
+
if (this.once && this.handled > 0 && this.jobs.size === 0)
|
|
39
|
+
return;
|
|
40
|
+
await delay(this.pollIntervalMs, this.signal);
|
|
41
|
+
}
|
|
42
|
+
for (const job of this.jobs.values())
|
|
43
|
+
job.controller.abort(this.signal?.reason);
|
|
44
|
+
await Promise.allSettled([...this.jobs.values()].map((job) => job.promise));
|
|
45
|
+
}
|
|
46
|
+
async tick() {
|
|
47
|
+
const offers = await this.client.listOffers();
|
|
48
|
+
const visible = new Map(offers.map((offer) => [offer.offer_id, offer]));
|
|
49
|
+
const released = new Set();
|
|
50
|
+
for (const job of this.jobs.values()) {
|
|
51
|
+
const current = visible.get(job.offer.offer_id);
|
|
52
|
+
if (!current)
|
|
53
|
+
continue;
|
|
54
|
+
job.offer = current;
|
|
55
|
+
if (current.state === "cancel-requested")
|
|
56
|
+
job.controller.abort(new Error("remote work cancellation requested"));
|
|
57
|
+
if (current.state === "release-requested") {
|
|
58
|
+
await this.releaseJob(job);
|
|
59
|
+
released.add(current.offer_id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const offer of offers) {
|
|
63
|
+
if (this.jobs.has(offer.offer_id) || released.has(offer.offer_id))
|
|
64
|
+
continue;
|
|
65
|
+
if (offer.state === "release-requested" || offer.state === "cancel-requested") {
|
|
66
|
+
await this.releaseUnknown(offer);
|
|
67
|
+
await this.client.release(offer);
|
|
68
|
+
this.handled += 1;
|
|
69
|
+
}
|
|
70
|
+
else if (offer.state === "offered" && (!this.once || this.handled === 0)) {
|
|
71
|
+
this.startOffer(offer);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (Date.now() - this.lastHeartbeat >= this.heartbeatIntervalMs)
|
|
75
|
+
await this.heartbeat();
|
|
76
|
+
}
|
|
77
|
+
async heartbeat(health = "healthy") {
|
|
78
|
+
const active = [...this.jobs.values()].filter((job) => job.accepted).map((job) => ({
|
|
79
|
+
lease_id: job.offer.lease.lease_id, epoch: job.offer.lease.epoch,
|
|
80
|
+
}));
|
|
81
|
+
await this.client.heartbeat(this.ledger.snapshot().allocated, active, health);
|
|
82
|
+
this.lastHeartbeat = Date.now();
|
|
83
|
+
}
|
|
84
|
+
startOffer(offer) {
|
|
85
|
+
const allocation = this.ledger.tryAcquire(offer.lease.lease_id, "eval", offer.lease.reservation);
|
|
86
|
+
if (!allocation)
|
|
87
|
+
return;
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const job = {
|
|
90
|
+
offer, controller, allocation, accepted: false, settled: false, cleanup: undefined,
|
|
91
|
+
promise: Promise.resolve(),
|
|
92
|
+
};
|
|
93
|
+
this.jobs.set(offer.offer_id, job);
|
|
94
|
+
job.promise = this.runOffer(job).catch((error) => {
|
|
95
|
+
this.onError(error);
|
|
96
|
+
if (!job.accepted)
|
|
97
|
+
this.finishJob(job);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async runOffer(job) {
|
|
101
|
+
let inputs;
|
|
102
|
+
try {
|
|
103
|
+
inputs = await this.downloadInputs(job.offer);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
await this.client.reject(job.offer, rejectionCode(error));
|
|
107
|
+
this.handled += 1;
|
|
108
|
+
this.finishJob(job);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const accepted = await retry(() => this.client.accept(job.offer), this.retryIntervalMs, this.signal);
|
|
112
|
+
if (accepted.state !== "accepted") {
|
|
113
|
+
this.handled += 1;
|
|
114
|
+
this.finishJob(job);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
job.offer = accepted;
|
|
118
|
+
job.accepted = true;
|
|
119
|
+
await this.heartbeat();
|
|
120
|
+
const credentials = new Map(Object.entries((await retry(() => this.client.credentials(job.offer), this.retryIntervalMs, job.controller.signal)).credentials));
|
|
121
|
+
let sequence = 0;
|
|
122
|
+
const emit = (type, payload) => retry(() => this.client.emit(job.offer, ++sequence, type, payload), this.retryIntervalMs, job.controller.signal);
|
|
123
|
+
let result;
|
|
124
|
+
try {
|
|
125
|
+
result = await this.execute({ offer: job.offer, inputs, credentials, signal: job.controller.signal, emit });
|
|
126
|
+
validateResult(result);
|
|
127
|
+
job.cleanup = result.release;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
// Executor setup can fail before it has a chance to return its normal
|
|
131
|
+
// release callback. Keep the offer releasable through the same
|
|
132
|
+
// ownership-fenced recovery path used after a worker restart.
|
|
133
|
+
job.cleanup = () => this.releaseUnknown(job.offer);
|
|
134
|
+
result = {
|
|
135
|
+
status: job.controller.signal.aborted ? "cancelled" : "failed",
|
|
136
|
+
artifacts: [{ kind: "diagnostic", body: diagnostic(error, [...credentials.values()]) }],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
credentials.clear();
|
|
141
|
+
}
|
|
142
|
+
const artifacts = [];
|
|
143
|
+
for (const artifact of result.artifacts ?? []) {
|
|
144
|
+
artifacts.push(await retry(() => this.client.uploadArtifact(job.offer, artifact.kind, artifact.body), this.retryIntervalMs, this.signal));
|
|
145
|
+
}
|
|
146
|
+
job.offer = await retry(() => this.client.complete(job.offer, result.status, artifacts), this.retryIntervalMs, this.signal);
|
|
147
|
+
job.settled = true;
|
|
148
|
+
await this.heartbeat();
|
|
149
|
+
}
|
|
150
|
+
async downloadInputs(offer) {
|
|
151
|
+
const refs = offer.inputs ?? [];
|
|
152
|
+
if (refs.length !== REQUIRED_INPUTS.size || refs.some((ref) => !REQUIRED_INPUTS.has(ref.kind))
|
|
153
|
+
|| new Set(refs.map((ref) => ref.kind)).size !== REQUIRED_INPUTS.size)
|
|
154
|
+
throw runnerError("remote work offer is missing required inputs");
|
|
155
|
+
const inputs = new Map();
|
|
156
|
+
for (const ref of refs)
|
|
157
|
+
inputs.set(ref.kind, await this.client.downloadInput(offer, ref));
|
|
158
|
+
return inputs;
|
|
159
|
+
}
|
|
160
|
+
async releaseJob(job) {
|
|
161
|
+
await job.cleanup?.();
|
|
162
|
+
job.offer = await this.client.release(job.offer);
|
|
163
|
+
this.handled += 1;
|
|
164
|
+
this.finishJob(job);
|
|
165
|
+
await this.heartbeat();
|
|
166
|
+
}
|
|
167
|
+
finishJob(job) {
|
|
168
|
+
job.allocation.release();
|
|
169
|
+
this.jobs.delete(job.offer.offer_id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function validateResult(value) {
|
|
173
|
+
if (!value || !new Set(["succeeded", "failed", "cancelled"]).has(value.status)
|
|
174
|
+
|| value.release !== undefined && typeof value.release !== "function"
|
|
175
|
+
|| value.artifacts !== undefined && (!Array.isArray(value.artifacts) || value.artifacts.some((entry) => !entry
|
|
176
|
+
|| !new Set(["result-bundle", "diagnostic"]).has(entry.kind) || !Buffer.isBuffer(entry.body)))) {
|
|
177
|
+
throw runnerError("remote worker executor returned an invalid result");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function diagnostic(error, credentialValues) {
|
|
181
|
+
return Buffer.from(`${JSON.stringify({ schema_version: "1", error: safeDiagnosticMessage(error, credentialValues, 2_048), at: new Date().toISOString() })}\n`);
|
|
182
|
+
}
|
|
183
|
+
function rejectionCode(error) {
|
|
184
|
+
const code = error?.code;
|
|
185
|
+
return typeof code === "string" && /^[a-z0-9][a-z0-9._-]{0,127}$/.test(code) ? code : "invalid-input";
|
|
186
|
+
}
|
|
187
|
+
async function retry(operation, retryMs, signal) {
|
|
188
|
+
while (true) {
|
|
189
|
+
try {
|
|
190
|
+
return await operation();
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (signal?.aborted)
|
|
194
|
+
throw error;
|
|
195
|
+
await delay(retryMs, signal);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function delay(milliseconds, signal) {
|
|
200
|
+
if (signal?.aborted)
|
|
201
|
+
return Promise.resolve();
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
const timer = setTimeout(done, milliseconds);
|
|
204
|
+
function done() { signal?.removeEventListener("abort", done); clearTimeout(timer); resolve(); }
|
|
205
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function interval(value, label) {
|
|
209
|
+
if (!Number.isSafeInteger(value) || value < 50 || value > 5 * 60_000)
|
|
210
|
+
throw new TypeError(`remote worker ${label} interval is invalid`);
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
function runnerError(message) { return new HitchError(message, { code: "remote_worker_invalid_input", exitCode: 12 }); }
|
|
214
|
+
//# sourceMappingURL=remote-worker-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-worker-runner.js","sourceRoot":"","sources":["../../../src/control-plane/remote-worker-runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { HitchError, atomicWriteJSON, ensureDir, readJSON, sha256Bytes, statePaths, withFileLock } from "../foundation/index.js";
|
|
5
|
+
import { validateResourceVector } from "./resources.js";
|
|
6
|
+
const WORKER_ID = /^worker_[a-z0-9][a-z0-9_-]{0,62}$/;
|
|
7
|
+
const LEASE_ID = /^lease_[a-f0-9]{32}$/;
|
|
8
|
+
const DEFAULT_HEARTBEAT_TTL_MS = 45_000;
|
|
9
|
+
export class RemoteWorkerRegistry {
|
|
10
|
+
root;
|
|
11
|
+
directory;
|
|
12
|
+
locks;
|
|
13
|
+
heartbeatTtlMs;
|
|
14
|
+
constructor(input) {
|
|
15
|
+
if (!input.root)
|
|
16
|
+
throw new TypeError("remote worker registry root is required");
|
|
17
|
+
const ttl = input.heartbeatTtlMs ?? DEFAULT_HEARTBEAT_TTL_MS;
|
|
18
|
+
if (!Number.isSafeInteger(ttl) || ttl < 1_000)
|
|
19
|
+
throw new TypeError("remote worker heartbeat TTL is invalid");
|
|
20
|
+
const paths = statePaths(input.root);
|
|
21
|
+
this.root = paths.root;
|
|
22
|
+
this.directory = paths.workers;
|
|
23
|
+
this.locks = paths.workerLocks;
|
|
24
|
+
this.heartbeatTtlMs = ttl;
|
|
25
|
+
}
|
|
26
|
+
async initialize() {
|
|
27
|
+
await Promise.all([ensureDir(this.directory), ensureDir(this.locks)]);
|
|
28
|
+
}
|
|
29
|
+
async register(value) {
|
|
30
|
+
const registration = parseRemoteWorkerRegistration(value);
|
|
31
|
+
const token = randomBytes(32).toString("hex");
|
|
32
|
+
const tokenHash = sha256Bytes(token);
|
|
33
|
+
const persisted = await withFileLock(this.locks, registration.worker_id, async () => {
|
|
34
|
+
const existing = await this.readPersisted(registration.worker_id);
|
|
35
|
+
if (existing && (existing.worker.provider !== registration.provider
|
|
36
|
+
|| existing.worker.collision_domain_id !== registration.collision_domain_id)) {
|
|
37
|
+
throw new HitchError("remote worker identity cannot be rebound", { code: "worker_identity_conflict", exitCode: 2 });
|
|
38
|
+
}
|
|
39
|
+
const now = new Date().toISOString();
|
|
40
|
+
const next = persistedWorker(registration, tokenHash, existing, now);
|
|
41
|
+
await atomicWriteJSON(this.workerPath(registration.worker_id), next);
|
|
42
|
+
return next;
|
|
43
|
+
}, { timeoutCode: "worker_record_locked", timeoutExitCode: 12 });
|
|
44
|
+
return { worker: publicRecord(persisted), token };
|
|
45
|
+
}
|
|
46
|
+
async authenticate(workerId, token) {
|
|
47
|
+
if (!WORKER_ID.test(workerId) || !/^[a-f0-9]{64}$/.test(token))
|
|
48
|
+
return false;
|
|
49
|
+
const record = await this.readPersisted(workerId);
|
|
50
|
+
if (!record || record.revoked_at)
|
|
51
|
+
return false;
|
|
52
|
+
const supplied = Buffer.from(sha256Bytes(token));
|
|
53
|
+
const expected = Buffer.from(record.token_hash);
|
|
54
|
+
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
|
|
55
|
+
}
|
|
56
|
+
async heartbeat(workerId, value) {
|
|
57
|
+
if (!WORKER_ID.test(workerId))
|
|
58
|
+
throw invalidWorker("remote worker id is invalid");
|
|
59
|
+
const heartbeat = parseRemoteWorkerHeartbeat(value);
|
|
60
|
+
const persisted = await withFileLock(this.locks, workerId, async () => {
|
|
61
|
+
const current = await this.requirePersisted(workerId);
|
|
62
|
+
if (current.revoked_at)
|
|
63
|
+
throw new HitchError("remote worker identity is revoked", { code: "worker_revoked", exitCode: 11 });
|
|
64
|
+
if (heartbeat.generation !== current.generation)
|
|
65
|
+
throw new HitchError("remote worker generation is stale", { code: "worker_generation_mismatch", exitCode: 12 });
|
|
66
|
+
assertWithin(heartbeat.allocated, current.worker.capacity.allocatable, "remote worker allocation exceeds allocatable capacity");
|
|
67
|
+
const next = {
|
|
68
|
+
...current,
|
|
69
|
+
worker: {
|
|
70
|
+
...current.worker,
|
|
71
|
+
status: heartbeat.health === "unavailable" ? "offline" : "ready",
|
|
72
|
+
capacity: { ...current.worker.capacity, allocated: heartbeat.allocated },
|
|
73
|
+
},
|
|
74
|
+
provider_status: {
|
|
75
|
+
...current.provider_status,
|
|
76
|
+
health: heartbeat.health,
|
|
77
|
+
capacity: { ...current.provider_status.capacity, allocated: heartbeat.allocated },
|
|
78
|
+
heartbeat_at: heartbeat.sent_at,
|
|
79
|
+
},
|
|
80
|
+
active_leases: heartbeat.active_leases,
|
|
81
|
+
heartbeat_at: heartbeat.sent_at,
|
|
82
|
+
};
|
|
83
|
+
await atomicWriteJSON(this.workerPath(workerId), next);
|
|
84
|
+
return next;
|
|
85
|
+
}, { timeoutCode: "worker_record_locked", timeoutExitCode: 12 });
|
|
86
|
+
return publicRecord(persisted);
|
|
87
|
+
}
|
|
88
|
+
async validateHeartbeatGeneration(workerId, generation) {
|
|
89
|
+
if (!WORKER_ID.test(workerId) || !Number.isSafeInteger(generation) || generation < 1)
|
|
90
|
+
throw invalidWorker("remote worker heartbeat identity is invalid");
|
|
91
|
+
const current = await this.requirePersisted(workerId);
|
|
92
|
+
if (current.revoked_at)
|
|
93
|
+
throw new HitchError("remote worker identity is revoked", { code: "worker_revoked", exitCode: 11 });
|
|
94
|
+
if (generation !== current.generation)
|
|
95
|
+
throw new HitchError("remote worker generation is stale", { code: "worker_generation_mismatch", exitCode: 12 });
|
|
96
|
+
}
|
|
97
|
+
async revoke(workerId) {
|
|
98
|
+
if (!WORKER_ID.test(workerId))
|
|
99
|
+
throw invalidWorker("remote worker id is invalid");
|
|
100
|
+
const persisted = await withFileLock(this.locks, workerId, async () => {
|
|
101
|
+
const current = await this.requirePersisted(workerId);
|
|
102
|
+
if (current.revoked_at)
|
|
103
|
+
return current;
|
|
104
|
+
const now = new Date().toISOString();
|
|
105
|
+
const next = {
|
|
106
|
+
...current,
|
|
107
|
+
generation: current.generation + 1,
|
|
108
|
+
worker: { ...current.worker, status: "offline" },
|
|
109
|
+
provider_status: { ...current.provider_status, health: "unavailable", heartbeat_at: now },
|
|
110
|
+
active_leases: [],
|
|
111
|
+
revoked_at: now,
|
|
112
|
+
heartbeat_at: now,
|
|
113
|
+
};
|
|
114
|
+
await atomicWriteJSON(this.workerPath(workerId), next);
|
|
115
|
+
return next;
|
|
116
|
+
}, { timeoutCode: "worker_record_locked", timeoutExitCode: 12 });
|
|
117
|
+
return publicRecord(persisted);
|
|
118
|
+
}
|
|
119
|
+
async get(workerId, now = Date.now()) {
|
|
120
|
+
if (!WORKER_ID.test(workerId))
|
|
121
|
+
throw invalidWorker("remote worker id is invalid");
|
|
122
|
+
const record = await this.readPersisted(workerId);
|
|
123
|
+
return record ? publicRecord(withLiveness(record, now, this.heartbeatTtlMs)) : null;
|
|
124
|
+
}
|
|
125
|
+
async list(now = Date.now()) {
|
|
126
|
+
await ensureDir(this.directory);
|
|
127
|
+
const entries = await readdir(this.directory, { withFileTypes: true });
|
|
128
|
+
const records = [];
|
|
129
|
+
for (const entry of entries.sort((left, right) => Buffer.from(left.name).compare(Buffer.from(right.name)))) {
|
|
130
|
+
if (!entry.isFile() || !/^worker_[a-z0-9][a-z0-9_-]{0,62}\.json$/.test(entry.name))
|
|
131
|
+
continue;
|
|
132
|
+
const parsed = parsePersistedRemoteWorker(await readJSON(path.join(this.directory, entry.name)));
|
|
133
|
+
records.push(publicRecord(withLiveness(parsed, now, this.heartbeatTtlMs)));
|
|
134
|
+
}
|
|
135
|
+
return records;
|
|
136
|
+
}
|
|
137
|
+
workerPath(workerId) {
|
|
138
|
+
return path.join(this.directory, `${workerId}.json`);
|
|
139
|
+
}
|
|
140
|
+
readPersisted(workerId) {
|
|
141
|
+
return readJSON(this.workerPath(workerId), null).then((value) => value === null ? null : parsePersistedRemoteWorker(value));
|
|
142
|
+
}
|
|
143
|
+
async requirePersisted(workerId) {
|
|
144
|
+
const record = await this.readPersisted(workerId);
|
|
145
|
+
if (!record)
|
|
146
|
+
throw new HitchError(`remote worker not found: ${workerId}`, { code: "worker_not_found", exitCode: 3 });
|
|
147
|
+
return record;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function parseRemoteWorkerRegistration(value) {
|
|
151
|
+
const record = exact(value, ["schema_version", "worker_id", "provider", "collision_domain_id", "platforms", "backends", "features", "task_membership", "capacity"], "remote worker registration");
|
|
152
|
+
if (record.schema_version !== "1" || typeof record.worker_id !== "string" || !WORKER_ID.test(record.worker_id)
|
|
153
|
+
|| !validText(record.provider) || !validText(record.collision_domain_id))
|
|
154
|
+
throw invalidWorker("remote worker registration identity is invalid");
|
|
155
|
+
const platforms = stringSet(record.platforms, "remote worker platforms");
|
|
156
|
+
const memberships = stringSet(record.task_membership, "remote worker task membership");
|
|
157
|
+
if (platforms.length === 0 || memberships.length === 0 || memberships.some((entry) => entry !== "known" && entry !== "opaque"))
|
|
158
|
+
throw invalidWorker("remote worker capabilities are invalid");
|
|
159
|
+
const backends = parseBackends(record.backends);
|
|
160
|
+
const features = parseFeatures(record.features);
|
|
161
|
+
const capacity = parseRegistrationCapacity(record.capacity);
|
|
162
|
+
return {
|
|
163
|
+
schema_version: "1", worker_id: record.worker_id, provider: record.provider,
|
|
164
|
+
collision_domain_id: record.collision_domain_id, platforms, backends, features,
|
|
165
|
+
task_membership: memberships, capacity,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export function parseRemoteWorkerHeartbeat(value) {
|
|
169
|
+
const record = exact(value, ["schema_version", "generation", "health", "allocated", "active_leases", "sent_at"], "remote worker heartbeat");
|
|
170
|
+
if (record.schema_version !== "1" || !Number.isSafeInteger(record.generation) || record.generation < 1
|
|
171
|
+
|| !new Set(["healthy", "degraded", "unavailable"]).has(String(record.health))
|
|
172
|
+
|| typeof record.sent_at !== "string" || !Number.isFinite(Date.parse(record.sent_at))
|
|
173
|
+
|| Math.abs(Date.now() - Date.parse(record.sent_at)) > 5 * 60_000 || !Array.isArray(record.active_leases)) {
|
|
174
|
+
throw invalidWorker("remote worker heartbeat is invalid");
|
|
175
|
+
}
|
|
176
|
+
const leases = parseHeartbeatLeases(record.active_leases);
|
|
177
|
+
return {
|
|
178
|
+
schema_version: "1", generation: record.generation,
|
|
179
|
+
health: record.health,
|
|
180
|
+
allocated: validateResourceVector(record.allocated, "remote worker allocated resources"),
|
|
181
|
+
active_leases: leases, sent_at: record.sent_at,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function parseHeartbeatLeases(value) {
|
|
185
|
+
if (!Array.isArray(value))
|
|
186
|
+
throw invalidWorker("remote worker heartbeat leases are invalid");
|
|
187
|
+
const leases = value.map((value) => {
|
|
188
|
+
const lease = exact(value, ["lease_id", "epoch"], "remote worker heartbeat lease");
|
|
189
|
+
if (typeof lease.lease_id !== "string" || !LEASE_ID.test(lease.lease_id)
|
|
190
|
+
|| !Number.isSafeInteger(lease.epoch) || lease.epoch < 1)
|
|
191
|
+
throw invalidWorker("remote worker heartbeat lease is invalid");
|
|
192
|
+
return { lease_id: lease.lease_id, epoch: lease.epoch };
|
|
193
|
+
}).sort((left, right) => left.lease_id.localeCompare(right.lease_id));
|
|
194
|
+
if (new Set(leases.map((lease) => lease.lease_id)).size !== leases.length)
|
|
195
|
+
throw invalidWorker("remote worker heartbeat leases are duplicated");
|
|
196
|
+
return leases;
|
|
197
|
+
}
|
|
198
|
+
function persistedWorker(registration, tokenHash, existing, now) {
|
|
199
|
+
const allocated = zero();
|
|
200
|
+
const worker = {
|
|
201
|
+
schema_version: "1", worker_id: registration.worker_id, provider: registration.provider, status: "ready",
|
|
202
|
+
collision_domain_id: registration.collision_domain_id,
|
|
203
|
+
capabilities: {
|
|
204
|
+
backends: registration.backends.map((entry) => entry.id), platforms: registration.platforms,
|
|
205
|
+
task_membership: registration.task_membership,
|
|
206
|
+
isolated_same_task_attempts: registration.features.isolated_same_task_attempts, remote: true,
|
|
207
|
+
},
|
|
208
|
+
capacity: { ...registration.capacity, allocated },
|
|
209
|
+
};
|
|
210
|
+
const providerStatus = {
|
|
211
|
+
schema_version: "1", provider: registration.provider, worker_id: registration.worker_id,
|
|
212
|
+
collision_domain_id: registration.collision_domain_id, health: "healthy", platforms: registration.platforms,
|
|
213
|
+
backends: registration.backends, features: registration.features,
|
|
214
|
+
capacity: { total: registration.capacity.total, allocatable: registration.capacity.allocatable, allocated }, heartbeat_at: now,
|
|
215
|
+
};
|
|
216
|
+
return {
|
|
217
|
+
schema_version: "1", generation: (existing?.generation ?? 0) + 1, worker, provider_status: providerStatus,
|
|
218
|
+
active_leases: [], token_hash: tokenHash, registered_at: existing?.registered_at ?? now, heartbeat_at: now,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function parsePersistedRemoteWorker(value) {
|
|
222
|
+
const record = exact(value, ["schema_version", "generation", "worker", "provider_status", "active_leases", "token_hash", "registered_at", "heartbeat_at", "revoked_at"], "persisted remote worker");
|
|
223
|
+
if (record.schema_version !== "1" || !Number.isSafeInteger(record.generation) || record.generation < 1
|
|
224
|
+
|| typeof record.token_hash !== "string" || !/^sha256:[a-f0-9]{64}$/.test(record.token_hash)
|
|
225
|
+
|| !validTimestamp(record.registered_at) || !validTimestamp(record.heartbeat_at)
|
|
226
|
+
|| record.revoked_at !== undefined && !validTimestamp(record.revoked_at))
|
|
227
|
+
throw invalidWorker("persisted remote worker identity is invalid");
|
|
228
|
+
const worker = parsePersistedWorker(record.worker);
|
|
229
|
+
const provider = parsePersistedProvider(record.provider_status);
|
|
230
|
+
const activeLeases = parseHeartbeatLeases(record.active_leases);
|
|
231
|
+
if (worker.worker_id !== provider.worker_id || worker.provider !== provider.provider || worker.collision_domain_id !== provider.collision_domain_id
|
|
232
|
+
|| JSON.stringify(worker.capacity.allocated) !== JSON.stringify(provider.capacity.allocated))
|
|
233
|
+
throw invalidWorker("persisted remote worker views are inconsistent");
|
|
234
|
+
return {
|
|
235
|
+
schema_version: "1", generation: record.generation, worker, provider_status: provider,
|
|
236
|
+
active_leases: activeLeases, token_hash: record.token_hash,
|
|
237
|
+
registered_at: record.registered_at, heartbeat_at: record.heartbeat_at,
|
|
238
|
+
...(record.revoked_at === undefined ? {} : { revoked_at: record.revoked_at }),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function parsePersistedWorker(value) {
|
|
242
|
+
const record = exact(value, ["schema_version", "worker_id", "provider", "status", "collision_domain_id", "capabilities", "capacity"], "persisted execution worker");
|
|
243
|
+
const capabilities = exact(record.capabilities, ["backends", "platforms", "task_membership", "isolated_same_task_attempts", "remote"], "persisted worker capabilities");
|
|
244
|
+
const capacity = exact(record.capacity, ["total", "reserved_for_system", "allocatable", "allocated"], "persisted worker capacity");
|
|
245
|
+
const registration = parseRemoteWorkerRegistration({
|
|
246
|
+
schema_version: record.schema_version, worker_id: record.worker_id, provider: record.provider,
|
|
247
|
+
collision_domain_id: record.collision_domain_id, platforms: capabilities.platforms,
|
|
248
|
+
backends: stringSet(capabilities.backends, "persisted worker backends").map((id) => ({ id, version: "unknown" })),
|
|
249
|
+
features: { docker: true, buildkit: false, model_proxy: false, isolated_same_task_attempts: capabilities.isolated_same_task_attempts },
|
|
250
|
+
task_membership: capabilities.task_membership,
|
|
251
|
+
capacity: { total: capacity.total, reserved_for_system: capacity.reserved_for_system, allocatable: capacity.allocatable },
|
|
252
|
+
});
|
|
253
|
+
if (!new Set(["ready", "draining", "offline"]).has(String(record.status)) || capabilities.remote !== true)
|
|
254
|
+
throw invalidWorker("persisted execution worker status is invalid");
|
|
255
|
+
const allocated = validateResourceVector(capacity.allocated, "persisted worker allocated resources");
|
|
256
|
+
assertWithin(allocated, registration.capacity.allocatable, "persisted worker allocation exceeds capacity");
|
|
257
|
+
return {
|
|
258
|
+
schema_version: "1", worker_id: registration.worker_id, provider: registration.provider,
|
|
259
|
+
status: record.status, collision_domain_id: registration.collision_domain_id,
|
|
260
|
+
capabilities: {
|
|
261
|
+
backends: stringSet(capabilities.backends, "persisted worker backends"), platforms: registration.platforms,
|
|
262
|
+
task_membership: registration.task_membership, isolated_same_task_attempts: registration.features.isolated_same_task_attempts, remote: true,
|
|
263
|
+
}, capacity: { ...registration.capacity, allocated },
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function parsePersistedProvider(value) {
|
|
267
|
+
const record = exact(value, ["schema_version", "provider", "worker_id", "collision_domain_id", "health", "platforms", "backends", "features", "capacity", "heartbeat_at"], "persisted provider status");
|
|
268
|
+
const capacity = exact(record.capacity, ["total", "allocatable", "allocated"], "persisted provider capacity");
|
|
269
|
+
const total = validateResourceVector(capacity.total, "persisted provider total resources");
|
|
270
|
+
const allocatable = validateResourceVector(capacity.allocatable, "persisted provider allocatable resources");
|
|
271
|
+
assertWithin(allocatable, total, "persisted provider allocatable resources exceed total capacity");
|
|
272
|
+
const registration = parseRemoteWorkerRegistration({
|
|
273
|
+
schema_version: record.schema_version, worker_id: record.worker_id, provider: record.provider,
|
|
274
|
+
collision_domain_id: record.collision_domain_id, platforms: record.platforms, backends: record.backends,
|
|
275
|
+
features: record.features, task_membership: ["known"],
|
|
276
|
+
capacity: { total, allocatable, reserved_for_system: subtract(total, allocatable) },
|
|
277
|
+
});
|
|
278
|
+
const allocated = validateResourceVector(capacity.allocated, "persisted provider allocated resources");
|
|
279
|
+
assertWithin(allocated, registration.capacity.allocatable, "persisted provider allocation exceeds capacity");
|
|
280
|
+
if (!new Set(["healthy", "degraded", "unavailable"]).has(String(record.health)) || !validTimestamp(record.heartbeat_at))
|
|
281
|
+
throw invalidWorker("persisted provider status is invalid");
|
|
282
|
+
return {
|
|
283
|
+
schema_version: "1", provider: registration.provider, worker_id: registration.worker_id,
|
|
284
|
+
collision_domain_id: registration.collision_domain_id, health: record.health,
|
|
285
|
+
platforms: registration.platforms, backends: registration.backends, features: registration.features,
|
|
286
|
+
capacity: { total: registration.capacity.total, allocatable: registration.capacity.allocatable, allocated }, heartbeat_at: record.heartbeat_at,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function parseRegistrationCapacity(value) {
|
|
290
|
+
const capacity = exact(value, ["total", "reserved_for_system", "allocatable"], "remote worker capacity");
|
|
291
|
+
const total = validateResourceVector(capacity.total, "remote worker total capacity");
|
|
292
|
+
const reserved = validateResourceVector(capacity.reserved_for_system, "remote worker reserved capacity");
|
|
293
|
+
const allocatable = validateResourceVector(capacity.allocatable, "remote worker allocatable capacity");
|
|
294
|
+
for (const field of resourceFields())
|
|
295
|
+
if (resourceValue(reserved, field) + resourceValue(allocatable, field) !== resourceValue(total, field))
|
|
296
|
+
throw invalidWorker("remote worker capacity accounting is invalid");
|
|
297
|
+
return { total, reserved_for_system: reserved, allocatable };
|
|
298
|
+
}
|
|
299
|
+
function parseBackends(value) {
|
|
300
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
301
|
+
throw invalidWorker("remote worker backends are invalid");
|
|
302
|
+
const result = value.map((entry) => {
|
|
303
|
+
const backend = exact(entry, ["id", "version"], "remote worker backend");
|
|
304
|
+
if (!validText(backend.id) || !validText(backend.version))
|
|
305
|
+
throw invalidWorker("remote worker backend is invalid");
|
|
306
|
+
return { id: backend.id, version: backend.version };
|
|
307
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
308
|
+
if (new Set(result.map((entry) => entry.id)).size !== result.length)
|
|
309
|
+
throw invalidWorker("remote worker backends are duplicated");
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
function parseFeatures(value) {
|
|
313
|
+
const features = exact(value, ["docker", "buildkit", "model_proxy", "isolated_same_task_attempts"], "remote worker features");
|
|
314
|
+
if (Object.values(features).some((entry) => typeof entry !== "boolean"))
|
|
315
|
+
throw invalidWorker("remote worker features are invalid");
|
|
316
|
+
return features;
|
|
317
|
+
}
|
|
318
|
+
function withLiveness(record, now, ttl) {
|
|
319
|
+
if (record.revoked_at || now - Date.parse(record.heartbeat_at) <= ttl)
|
|
320
|
+
return record;
|
|
321
|
+
return { ...record, worker: { ...record.worker, status: "offline" }, provider_status: { ...record.provider_status, health: "unavailable" } };
|
|
322
|
+
}
|
|
323
|
+
function publicRecord(record) {
|
|
324
|
+
const { token_hash: _tokenHash, ...visible } = record;
|
|
325
|
+
return structuredClone(visible);
|
|
326
|
+
}
|
|
327
|
+
function exact(value, keys, label) {
|
|
328
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
329
|
+
throw invalidWorker(`${label} must be an object`);
|
|
330
|
+
const record = value;
|
|
331
|
+
if (Object.keys(record).some((key) => !keys.includes(key)))
|
|
332
|
+
throw invalidWorker(`${label} has unknown fields`);
|
|
333
|
+
return record;
|
|
334
|
+
}
|
|
335
|
+
function stringSet(value, label) {
|
|
336
|
+
if (!Array.isArray(value) || value.some((entry) => !validText(entry)))
|
|
337
|
+
throw invalidWorker(`${label} are invalid`);
|
|
338
|
+
const result = [...value].sort();
|
|
339
|
+
if (new Set(result).size !== result.length)
|
|
340
|
+
throw invalidWorker(`${label} are duplicated`);
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
function assertWithin(value, limit, message) {
|
|
344
|
+
for (const field of resourceFields())
|
|
345
|
+
if (resourceValue(value, field) > resourceValue(limit, field))
|
|
346
|
+
throw invalidWorker(message);
|
|
347
|
+
}
|
|
348
|
+
function resourceFields() {
|
|
349
|
+
return ["cpu_millis", "memory_bytes", "container_slots", "build_slots", "gpu_count", "ephemeral_disk_bytes"];
|
|
350
|
+
}
|
|
351
|
+
function zero() {
|
|
352
|
+
return { cpu_millis: 0, memory_bytes: 0, container_slots: 0, build_slots: 0 };
|
|
353
|
+
}
|
|
354
|
+
function subtract(left, right) {
|
|
355
|
+
const result = {
|
|
356
|
+
cpu_millis: left.cpu_millis - right.cpu_millis,
|
|
357
|
+
memory_bytes: left.memory_bytes - right.memory_bytes,
|
|
358
|
+
container_slots: left.container_slots - right.container_slots,
|
|
359
|
+
build_slots: left.build_slots - right.build_slots,
|
|
360
|
+
};
|
|
361
|
+
if (left.gpu_count !== undefined || right.gpu_count !== undefined)
|
|
362
|
+
result.gpu_count = (left.gpu_count ?? 0) - (right.gpu_count ?? 0);
|
|
363
|
+
if (left.ephemeral_disk_bytes !== undefined || right.ephemeral_disk_bytes !== undefined)
|
|
364
|
+
result.ephemeral_disk_bytes = (left.ephemeral_disk_bytes ?? 0) - (right.ephemeral_disk_bytes ?? 0);
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
function resourceValue(resources, field) { return resources[field] ?? 0; }
|
|
368
|
+
function validText(value) {
|
|
369
|
+
return typeof value === "string" && value.length > 0 && value.length <= 1_024 && !/[\0\r\n]/.test(value);
|
|
370
|
+
}
|
|
371
|
+
function validTimestamp(value) {
|
|
372
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
373
|
+
}
|
|
374
|
+
function invalidWorker(message) {
|
|
375
|
+
return new HitchError(message, { code: "worker_protocol_invalid", exitCode: 2 });
|
|
376
|
+
}
|
|
377
|
+
//# sourceMappingURL=remote-workers.js.map
|