agent-hitch 0.2.0 → 0.2.2
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 +88 -250
- package/README.zh-CN.md +84 -230
- package/dist/bin/hitch.js +1 -1
- package/dist/bin/hitch.js.map +1 -1
- package/dist/scripts/check-architecture.js +159 -0
- package/dist/scripts/check-architecture.js.map +1 -0
- package/dist/scripts/check-release.js +1 -1
- package/dist/scripts/check-release.js.map +1 -1
- package/dist/src/adapters/catalog.js +55 -0
- package/dist/src/adapters/catalog.js.map +1 -0
- package/dist/src/adapters/contract.js +2 -0
- package/dist/src/adapters/contract.js.map +1 -0
- package/dist/src/adapters/discovery.js +22 -0
- package/dist/src/adapters/discovery.js.map +1 -0
- package/dist/src/adapters/index.js +3 -0
- package/dist/src/adapters/index.js.map +1 -0
- package/dist/src/adapters/providers/claude.js +69 -0
- package/dist/src/adapters/providers/claude.js.map +1 -0
- package/dist/src/adapters/providers/codex.js +74 -0
- package/dist/src/adapters/providers/codex.js.map +1 -0
- package/dist/src/adapters/providers/deepseek.js +51 -0
- package/dist/src/adapters/providers/deepseek.js.map +1 -0
- package/dist/src/adapters/providers/opencode.js +73 -0
- package/dist/src/adapters/providers/opencode.js.map +1 -0
- package/dist/src/adapters/providers/pi.js +82 -0
- package/dist/src/adapters/providers/pi.js.map +1 -0
- package/dist/src/adapters/providers/shared.js +82 -0
- package/dist/src/adapters/providers/shared.js.map +1 -0
- package/dist/src/artifacts/handoff.js +12 -0
- package/dist/src/artifacts/handoff.js.map +1 -0
- package/dist/src/artifacts/index.js +5 -0
- package/dist/src/artifacts/index.js.map +1 -0
- package/dist/src/artifacts/integrity.js +188 -0
- package/dist/src/artifacts/integrity.js.map +1 -0
- package/dist/src/artifacts/preparer.js +379 -0
- package/dist/src/artifacts/preparer.js.map +1 -0
- package/dist/src/artifacts/store.js +157 -0
- package/dist/src/artifacts/store.js.map +1 -0
- package/dist/src/artifacts/types.js +2 -0
- package/dist/src/artifacts/types.js.map +1 -0
- package/dist/src/backends/contract.js +2 -0
- package/dist/src/backends/contract.js.map +1 -0
- package/dist/src/{harbor-backend.js → backends/harbor/backend.js} +52 -14
- package/dist/src/backends/harbor/backend.js.map +1 -0
- package/dist/src/backends/harbor/index.js +4 -0
- package/dist/src/backends/harbor/index.js.map +1 -0
- package/dist/src/backends/harbor/local-git-transport.js +447 -0
- package/dist/src/backends/harbor/local-git-transport.js.map +1 -0
- package/dist/src/{eval-tools.js → backends/harbor/tools.js} +2 -6
- package/dist/src/backends/harbor/tools.js.map +1 -0
- package/dist/src/backends/index.js +4 -0
- package/dist/src/backends/index.js.map +1 -0
- package/dist/src/cli/arguments.js +123 -0
- package/dist/src/cli/arguments.js.map +1 -0
- package/dist/src/cli/commands/compare.js +55 -0
- package/dist/src/cli/commands/compare.js.map +1 -0
- package/dist/src/cli/commands/daemon.js +106 -0
- package/dist/src/cli/commands/daemon.js.map +1 -0
- package/dist/src/cli/commands/eval.js +126 -0
- package/dist/src/cli/commands/eval.js.map +1 -0
- package/dist/src/cli/commands/feedback.js +84 -0
- package/dist/src/cli/commands/feedback.js.map +1 -0
- package/dist/src/cli/commands/inspect.js +20 -0
- package/dist/src/cli/commands/inspect.js.map +1 -0
- package/dist/src/cli/commands/list.js +17 -0
- package/dist/src/cli/commands/list.js.map +1 -0
- package/dist/src/cli/commands/prepare.js +21 -0
- package/dist/src/cli/commands/prepare.js.map +1 -0
- package/dist/src/cli/commands/resolve.js +17 -0
- package/dist/src/cli/commands/resolve.js.map +1 -0
- package/dist/src/cli/commands/run.js +145 -0
- package/dist/src/cli/commands/run.js.map +1 -0
- package/dist/src/cli/commands/runs.js +45 -0
- package/dist/src/cli/commands/runs.js.map +1 -0
- package/dist/src/cli/commands/trajectory.js +31 -0
- package/dist/src/cli/commands/trajectory.js.map +1 -0
- package/dist/src/cli/commands/workspace.js +44 -0
- package/dist/src/cli/commands/workspace.js.map +1 -0
- package/dist/src/cli/index.js +2 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/cli/main.js +48 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/cli/output.js +96 -0
- package/dist/src/cli/output.js.map +1 -0
- package/dist/src/controller-runtime/index.js +3 -0
- package/dist/src/controller-runtime/index.js.map +1 -0
- package/dist/src/controller-runtime/store.js +2 -7
- package/dist/src/controller-runtime/store.js.map +1 -1
- package/dist/src/daemon/auth.js +108 -0
- package/dist/src/daemon/auth.js.map +1 -0
- package/dist/src/daemon/client.js +76 -0
- package/dist/src/daemon/client.js.map +1 -0
- package/dist/src/daemon/index.js +5 -0
- package/dist/src/daemon/index.js.map +1 -0
- package/dist/src/daemon/launcher.js +24 -0
- package/dist/src/daemon/launcher.js.map +1 -0
- package/dist/src/{scheduler.js → daemon/scheduler.js} +6 -7
- package/dist/src/daemon/scheduler.js.map +1 -0
- package/dist/src/{daemon.js → daemon/server.js} +10 -155
- package/dist/src/daemon/server.js.map +1 -0
- package/dist/src/domain/artifacts.js +2 -0
- package/dist/src/domain/artifacts.js.map +1 -0
- package/dist/src/domain/controller-runtime.js +2 -0
- package/dist/src/domain/controller-runtime.js.map +1 -0
- package/dist/src/domain/eval-records.js +2 -0
- package/dist/src/domain/eval-records.js.map +1 -0
- package/dist/src/domain/evals.js +2 -0
- package/dist/src/domain/evals.js.map +1 -0
- package/dist/src/domain/feedback.js +2 -0
- package/dist/src/domain/feedback.js.map +1 -0
- package/dist/src/domain/{types.js → ids.js} +1 -1
- package/dist/src/domain/ids.js.map +1 -0
- package/dist/src/domain/index.js +11 -0
- package/dist/src/domain/index.js.map +1 -0
- package/dist/src/domain/revisions.js +2 -0
- package/dist/src/domain/revisions.js.map +1 -0
- package/dist/src/domain/runs.js +3 -0
- package/dist/src/domain/runs.js.map +1 -0
- package/dist/src/domain/trajectories.js +2 -0
- package/dist/src/domain/trajectories.js.map +1 -0
- package/dist/src/domain/{validate.js → validation.js} +194 -5
- package/dist/src/domain/validation.js.map +1 -0
- package/dist/src/domain/workspaces.js +2 -0
- package/dist/src/domain/workspaces.js.map +1 -0
- package/dist/src/evals/events.js +67 -0
- package/dist/src/evals/events.js.map +1 -0
- package/dist/src/evals/index.js +5 -0
- package/dist/src/evals/index.js.map +1 -0
- package/dist/src/evals/records.js +55 -0
- package/dist/src/evals/records.js.map +1 -0
- package/dist/src/evals/request.js +109 -0
- package/dist/src/evals/request.js.map +1 -0
- package/dist/src/evals/service.js +280 -0
- package/dist/src/evals/service.js.map +1 -0
- package/dist/src/evals/trial-import.js +394 -0
- package/dist/src/evals/trial-import.js.map +1 -0
- package/dist/src/feedback/index.js +2 -0
- package/dist/src/feedback/index.js.map +1 -0
- package/dist/src/feedback/service.js +3 -4
- package/dist/src/feedback/service.js.map +1 -1
- package/dist/src/{config.js → foundation/config.js} +1 -0
- package/dist/src/foundation/config.js.map +1 -0
- package/dist/src/foundation/errors.js.map +1 -0
- package/dist/src/foundation/executable.js +135 -0
- package/dist/src/foundation/executable.js.map +1 -0
- package/dist/src/foundation/fs.js.map +1 -0
- package/dist/src/foundation/hash.js +24 -0
- package/dist/src/foundation/hash.js.map +1 -0
- package/dist/src/foundation/index.js +11 -0
- package/dist/src/foundation/index.js.map +1 -0
- package/dist/src/foundation/line-stream.js.map +1 -0
- package/dist/src/foundation/locks.js +114 -0
- package/dist/src/foundation/locks.js.map +1 -0
- package/dist/src/{package-root.js → foundation/package-root.js} +3 -2
- package/dist/src/foundation/package-root.js.map +1 -0
- package/dist/src/foundation/process.js.map +1 -0
- package/dist/src/revisions/index.js +4 -0
- package/dist/src/revisions/index.js.map +1 -0
- package/dist/src/{harness-reference.js → revisions/reference.js} +16 -2
- package/dist/src/revisions/reference.js.map +1 -0
- package/dist/src/revisions/resolver.js +25 -0
- package/dist/src/revisions/resolver.js.map +1 -0
- package/dist/src/revisions/sources/git.js +180 -0
- package/dist/src/revisions/sources/git.js.map +1 -0
- package/dist/src/revisions/sources/installed.js +45 -0
- package/dist/src/revisions/sources/installed.js.map +1 -0
- package/dist/src/revisions/sources/npm.js +97 -0
- package/dist/src/revisions/sources/npm.js.map +1 -0
- package/dist/src/runs/compare.js +132 -0
- package/dist/src/runs/compare.js.map +1 -0
- package/dist/src/{events.js → runs/events.js} +1 -2
- package/dist/src/runs/events.js.map +1 -0
- package/dist/src/{engine.js → runs/executor.js} +172 -151
- package/dist/src/runs/executor.js.map +1 -0
- package/dist/src/runs/finalizer.js +22 -0
- package/dist/src/runs/finalizer.js.map +1 -0
- package/dist/src/runs/identity.js +35 -0
- package/dist/src/runs/identity.js.map +1 -0
- package/dist/src/runs/index.js +11 -0
- package/dist/src/runs/index.js.map +1 -0
- package/dist/src/runs/manifest.js +70 -0
- package/dist/src/runs/manifest.js.map +1 -0
- package/dist/src/runs/outcome.js +49 -0
- package/dist/src/runs/outcome.js.map +1 -0
- package/dist/src/runs/query.js +104 -0
- package/dist/src/runs/query.js.map +1 -0
- package/dist/src/runs/queued.js +54 -0
- package/dist/src/runs/queued.js.map +1 -0
- package/dist/src/runs/records.js +288 -0
- package/dist/src/runs/records.js.map +1 -0
- package/dist/src/runs/request.js +169 -0
- package/dist/src/runs/request.js.map +1 -0
- package/dist/src/trajectories/format.js +1 -1
- package/dist/src/trajectories/format.js.map +1 -1
- package/dist/src/trajectories/index.js +7 -0
- package/dist/src/trajectories/index.js.map +1 -0
- package/dist/src/trajectories/projector.js +1 -1
- package/dist/src/trajectories/projector.js.map +1 -1
- package/dist/src/trajectories/provider-capture.js +133 -0
- package/dist/src/trajectories/provider-capture.js.map +1 -0
- package/dist/src/trajectories/providers/deepseek.js +179 -0
- package/dist/src/trajectories/providers/deepseek.js.map +1 -0
- package/dist/src/trajectories/store.js +148 -8
- package/dist/src/trajectories/store.js.map +1 -1
- package/dist/src/workspaces/copy.js +142 -0
- package/dist/src/workspaces/copy.js.map +1 -0
- package/dist/src/workspaces/digest.js +47 -0
- package/dist/src/workspaces/digest.js.map +1 -0
- package/dist/src/workspaces/git.js +164 -0
- package/dist/src/workspaces/git.js.map +1 -0
- package/dist/src/workspaces/index.js +6 -0
- package/dist/src/workspaces/index.js.map +1 -0
- package/dist/src/workspaces/lifecycle.js +313 -0
- package/dist/src/workspaces/lifecycle.js.map +1 -0
- package/dist/src/workspaces/planner.js +80 -0
- package/dist/src/workspaces/planner.js.map +1 -0
- package/dist/src/workspaces/store.js +25 -0
- package/dist/src/workspaces/store.js.map +1 -0
- package/dist/src/workspaces/types.js +4 -0
- package/dist/src/workspaces/types.js.map +1 -0
- package/dist/src/workspaces/utils.js +56 -0
- package/dist/src/workspaces/utils.js.map +1 -0
- package/docs/schemas/eval-request.schema.json +2 -0
- package/docs/schemas/eval-result.schema.json +50 -1
- package/docs/schemas/local-git-transport-manifest.schema.json +34 -0
- package/docs/schemas/result.schema.json +9 -0
- package/docs/schemas/run-context.schema.json +39 -0
- package/docs/schemas/run-manifest.schema.json +81 -0
- package/docs/schemas/run-request.schema.json +16 -1
- package/docs/schemas/trajectory-ref.schema.json +66 -23
- package/integrations/harbor/hitch_harbor_agent.py +482 -10
- package/package.json +21 -3
- package/dist/src/adapters.js +0 -461
- package/dist/src/adapters.js.map +0 -1
- package/dist/src/artifacts.js +0 -1081
- package/dist/src/artifacts.js.map +0 -1
- package/dist/src/cli.js +0 -699
- package/dist/src/cli.js.map +0 -1
- package/dist/src/config.js.map +0 -1
- package/dist/src/daemon.js.map +0 -1
- package/dist/src/domain/types.js.map +0 -1
- package/dist/src/domain/validate.js.map +0 -1
- package/dist/src/engine.js.map +0 -1
- package/dist/src/errors.js.map +0 -1
- package/dist/src/eval-tools.js.map +0 -1
- package/dist/src/evals.js +0 -313
- package/dist/src/evals.js.map +0 -1
- package/dist/src/events.js.map +0 -1
- package/dist/src/fs.js.map +0 -1
- package/dist/src/harbor-backend.js.map +0 -1
- package/dist/src/harness-reference.js.map +0 -1
- package/dist/src/line-stream.js.map +0 -1
- package/dist/src/locks.js +0 -38
- package/dist/src/locks.js.map +0 -1
- package/dist/src/package-root.js.map +0 -1
- package/dist/src/process.js.map +0 -1
- package/dist/src/registry.js +0 -81
- package/dist/src/registry.js.map +0 -1
- package/dist/src/scheduler.js.map +0 -1
- package/dist/src/workspaces.js +0 -858
- package/dist/src/workspaces.js.map +0 -1
- /package/dist/src/{errors.js → foundation/errors.js} +0 -0
- /package/dist/src/{fs.js → foundation/fs.js} +0 -0
- /package/dist/src/{line-stream.js → foundation/line-stream.js} +0 -0
- /package/dist/src/{process.js → foundation/process.js} +0 -0
|
@@ -6,7 +6,10 @@ import hashlib
|
|
|
6
6
|
import json
|
|
7
7
|
import re
|
|
8
8
|
import shlex
|
|
9
|
+
import stat as stat_module
|
|
9
10
|
import tempfile
|
|
11
|
+
import uuid
|
|
12
|
+
from datetime import datetime
|
|
10
13
|
from pathlib import Path
|
|
11
14
|
from typing import Any
|
|
12
15
|
|
|
@@ -15,6 +18,10 @@ from harbor.environments.base import BaseEnvironment, ExecResult
|
|
|
15
18
|
from harbor.models.agent.context import AgentContext
|
|
16
19
|
|
|
17
20
|
CONTROLLER_RUNTIME_MANIFEST_VERSION = "2"
|
|
21
|
+
LOCAL_GIT_TRANSPORT_MANIFEST_VERSION = "1"
|
|
22
|
+
LOCAL_GIT_TRANSPORT_MAX_BYTES = 512 * 1024 * 1024
|
|
23
|
+
LOCAL_GIT_REMOTE_ROOT = "/opt/hitch-local-source"
|
|
24
|
+
HARNESS_ARTIFACT_REMOTE_ROOT = "/opt/hitch-harness-artifact"
|
|
18
25
|
|
|
19
26
|
|
|
20
27
|
class HitchHarborAgent(BaseAgent):
|
|
@@ -28,9 +35,15 @@ class HitchHarborAgent(BaseAgent):
|
|
|
28
35
|
hitch_runtime_dir: str,
|
|
29
36
|
candidate_id: str = "candidate-1",
|
|
30
37
|
controller_runtime_id: str | None = None,
|
|
38
|
+
harness_artifact: dict[str, Any] | None = None,
|
|
39
|
+
local_source_transport: dict[str, Any] | None = None,
|
|
31
40
|
hitch_timeout_ms: int = 900_000,
|
|
32
41
|
agent_args: list[str] | None = None,
|
|
33
42
|
workdir: str = "/app",
|
|
43
|
+
eval_id: str | None = None,
|
|
44
|
+
benchmark_id: str | None = None,
|
|
45
|
+
benchmark_revision: str | None = None,
|
|
46
|
+
verifier_identity: str | None = None,
|
|
34
47
|
**kwargs: Any,
|
|
35
48
|
) -> None:
|
|
36
49
|
super().__init__(logs_dir=logs_dir, **kwargs)
|
|
@@ -38,12 +51,21 @@ class HitchHarborAgent(BaseAgent):
|
|
|
38
51
|
self.revision_identity = revision_identity
|
|
39
52
|
self.hitch_runtime_dir = Path(hitch_runtime_dir)
|
|
40
53
|
self.controller_runtime_id = controller_runtime_id
|
|
54
|
+
self.harness_artifact = dict(harness_artifact) if harness_artifact else None
|
|
55
|
+
self.local_source_transport = dict(local_source_transport) if local_source_transport else None
|
|
41
56
|
self.candidate_id = candidate_id
|
|
42
57
|
self.hitch_timeout_ms = int(hitch_timeout_ms)
|
|
43
58
|
self.agent_args = list(agent_args or [])
|
|
44
59
|
self.workdir = workdir
|
|
60
|
+
self.eval_id = eval_id
|
|
61
|
+
self.benchmark_id = benchmark_id
|
|
62
|
+
self.benchmark_revision = benchmark_revision
|
|
63
|
+
self.verifier_identity = verifier_identity
|
|
45
64
|
self._hitch_version: str | None = None
|
|
46
65
|
self._entrypoint: str | None = None
|
|
66
|
+
self._artifact_manifest: dict[str, Any] | None = None
|
|
67
|
+
self._artifact_uploaded = False
|
|
68
|
+
self._local_manifest: dict[str, Any] | None = None
|
|
47
69
|
|
|
48
70
|
@staticmethod
|
|
49
71
|
def name() -> str:
|
|
@@ -76,6 +98,16 @@ class HitchHarborAgent(BaseAgent):
|
|
|
76
98
|
# and the actual container upload, spec §4.6).
|
|
77
99
|
self._verify_manifest_identity(manifest)
|
|
78
100
|
self._verify_payload(manifest)
|
|
101
|
+
if self.harness_artifact is not None:
|
|
102
|
+
try:
|
|
103
|
+
self._artifact_manifest = self._verify_harness_artifact_host()
|
|
104
|
+
except Exception as error:
|
|
105
|
+
raise RuntimeError(f"hitch-artifact-materialize: {error}") from error
|
|
106
|
+
if self.local_source_transport is not None:
|
|
107
|
+
try:
|
|
108
|
+
self._local_manifest = self._verify_local_source_host()
|
|
109
|
+
except Exception as error:
|
|
110
|
+
raise RuntimeError(f"hitch-local-source-materialize: {error}") from error
|
|
79
111
|
payload_dir = self.hitch_runtime_dir / "payload"
|
|
80
112
|
if not payload_dir.is_dir():
|
|
81
113
|
raise RuntimeError(f"Hitch runtime bundle has no payload directory: {self.hitch_runtime_dir}")
|
|
@@ -84,19 +116,293 @@ class HitchHarborAgent(BaseAgent):
|
|
|
84
116
|
# bookkeeping and is not identity (spec §4.2).
|
|
85
117
|
await environment.upload_dir(payload_dir, "/opt/hitch")
|
|
86
118
|
await self._ensure_node(environment)
|
|
119
|
+
if self._artifact_manifest is not None:
|
|
120
|
+
platform = await self._container_platform(environment)
|
|
121
|
+
if platform == self._artifact_manifest["platform"]:
|
|
122
|
+
await environment.upload_dir(
|
|
123
|
+
Path(str(self.harness_artifact["directory"])),
|
|
124
|
+
HARNESS_ARTIFACT_REMOTE_ROOT,
|
|
125
|
+
)
|
|
126
|
+
self._artifact_uploaded = True
|
|
127
|
+
if self._local_manifest is not None and not self._artifact_uploaded:
|
|
128
|
+
try:
|
|
129
|
+
await self._upload_and_materialize_local_source(environment, self._local_manifest)
|
|
130
|
+
except Exception as error:
|
|
131
|
+
raise RuntimeError(f"hitch-local-source-materialize: {error}") from error
|
|
87
132
|
entry = self._remote_entry(entrypoint)
|
|
88
133
|
version = await self._exec(environment, f"{self._node_prefix()} node {entry} --version")
|
|
89
134
|
self._hitch_version = (version.stdout or "").strip() or None
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
135
|
+
if not self._artifact_uploaded:
|
|
136
|
+
prepare = " ".join(
|
|
137
|
+
[
|
|
138
|
+
self._node_prefix(),
|
|
139
|
+
"HITCH_ROOT=/tmp/hitch-state",
|
|
140
|
+
*(["HITCH_HARBOR_INTERNAL=1"] if self._local_manifest is not None else []),
|
|
141
|
+
f"node {entry} prepare",
|
|
142
|
+
shlex.quote(self.harness_ref),
|
|
143
|
+
*self._local_source_cli_args(),
|
|
144
|
+
"--json",
|
|
145
|
+
]
|
|
146
|
+
)
|
|
147
|
+
await self._exec(environment, prepare)
|
|
148
|
+
|
|
149
|
+
def _verify_harness_artifact_host(self) -> dict[str, Any]:
|
|
150
|
+
"""Pin host artifact metadata before Harbor copies the directory."""
|
|
151
|
+
transport = self.harness_artifact or {}
|
|
152
|
+
required = {
|
|
153
|
+
"directory", "artifact_id", "artifact_integrity", "entrypoint_integrity",
|
|
154
|
+
"harness_id", "revision_identity", "platform", "source_type",
|
|
155
|
+
}
|
|
156
|
+
if set(transport) != required:
|
|
157
|
+
raise RuntimeError("prepared artifact handoff metadata fields are invalid")
|
|
158
|
+
directory = Path(str(transport["directory"]))
|
|
159
|
+
if not directory.is_absolute():
|
|
160
|
+
raise RuntimeError("prepared artifact host path must be absolute")
|
|
161
|
+
try:
|
|
162
|
+
directory_info = directory.lstat()
|
|
163
|
+
except OSError as error:
|
|
164
|
+
raise RuntimeError("prepared artifact directory is missing or unreadable") from error
|
|
165
|
+
if not stat_module.S_ISDIR(directory_info.st_mode) or directory.is_symlink():
|
|
166
|
+
raise RuntimeError("prepared artifact handoff must be a regular directory")
|
|
167
|
+
artifact_file = directory / "artifact.json"
|
|
168
|
+
self._assert_regular_host_file(artifact_file, "prepared artifact manifest", 1024 * 1024)
|
|
169
|
+
try:
|
|
170
|
+
manifest = json.loads(artifact_file.read_text(encoding="utf-8"))
|
|
171
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as error:
|
|
172
|
+
raise RuntimeError("prepared artifact manifest is unreadable") from error
|
|
173
|
+
pinned = {
|
|
174
|
+
"artifact_id": manifest.get("artifact_id"),
|
|
175
|
+
"artifact_integrity": manifest.get("artifact_integrity"),
|
|
176
|
+
"entrypoint_integrity": manifest.get("entrypoint_integrity"),
|
|
177
|
+
"harness_id": manifest.get("harness_id"),
|
|
178
|
+
"revision_identity": manifest.get("revision_identity"),
|
|
179
|
+
"platform": manifest.get("platform"),
|
|
180
|
+
"source_type": manifest.get("source_type"),
|
|
181
|
+
}
|
|
182
|
+
if any(transport.get(key) != value for key, value in pinned.items()):
|
|
183
|
+
raise RuntimeError("prepared artifact metadata does not match its manifest")
|
|
184
|
+
if manifest.get("revision_identity") != self.revision_identity:
|
|
185
|
+
raise RuntimeError("prepared artifact revision does not match the job-pinned identity")
|
|
186
|
+
if manifest.get("source_type") == "installed":
|
|
187
|
+
raise RuntimeError("installed harness artifacts cannot cross the container boundary")
|
|
188
|
+
if not re.fullmatch(r"sha256:[0-9a-f]{64}", str(manifest.get("artifact_id", ""))):
|
|
189
|
+
raise RuntimeError("prepared artifact ID is invalid")
|
|
190
|
+
for field in ("artifact_integrity", "entrypoint_integrity", "revision_identity"):
|
|
191
|
+
if not re.fullmatch(r"sha256:[0-9a-f]{64}", str(manifest.get(field, ""))):
|
|
192
|
+
raise RuntimeError(f"prepared artifact {field} is invalid")
|
|
193
|
+
if not re.fullmatch(r"(?:linux|darwin|win32)-[a-z0-9_]+", str(manifest.get("platform", ""))):
|
|
194
|
+
raise RuntimeError("prepared artifact platform is invalid")
|
|
195
|
+
return manifest
|
|
196
|
+
|
|
197
|
+
async def _container_platform(self, environment: BaseEnvironment) -> str:
|
|
198
|
+
result = await self._exec(
|
|
199
|
+
environment,
|
|
200
|
+
f'{self._node_prefix()} node -p "process.platform + \'-\' + process.arch"',
|
|
98
201
|
)
|
|
99
|
-
|
|
202
|
+
platform = (result.stdout or "").strip()
|
|
203
|
+
if not re.fullmatch(r"(?:linux|darwin|win32)-[a-z0-9_]+", platform):
|
|
204
|
+
raise RuntimeError("container returned an invalid Node.js platform identity")
|
|
205
|
+
return platform
|
|
206
|
+
|
|
207
|
+
def _verify_local_source_host(self) -> dict[str, Any]:
|
|
208
|
+
"""Validate the independent local-source handoff immediately before upload."""
|
|
209
|
+
transport = self.local_source_transport or {}
|
|
210
|
+
required = {
|
|
211
|
+
"kind", "manifest_path", "payload_path", "locked_resolution_path", "harness_id",
|
|
212
|
+
"resolution_identity", "commit", "tree", "payload_sha256", "payload_bytes",
|
|
213
|
+
"object_count", "file_count",
|
|
214
|
+
}
|
|
215
|
+
if set(transport) != required:
|
|
216
|
+
raise RuntimeError("local source transport metadata fields are invalid")
|
|
217
|
+
manifest_path = Path(str(transport["manifest_path"]))
|
|
218
|
+
payload_path = Path(str(transport["payload_path"]))
|
|
219
|
+
resolution_path = Path(str(transport["locked_resolution_path"]))
|
|
220
|
+
if not all(candidate.is_absolute() for candidate in (manifest_path, payload_path, resolution_path)):
|
|
221
|
+
raise RuntimeError("local source transport host paths must be absolute")
|
|
222
|
+
self._assert_regular_host_file(manifest_path, "local source manifest", 64 * 1024)
|
|
223
|
+
payload_stat = self._assert_regular_host_file(payload_path, "local source payload", LOCAL_GIT_TRANSPORT_MAX_BYTES)
|
|
224
|
+
self._assert_regular_host_file(resolution_path, "local source locked resolution", 1024 * 1024)
|
|
225
|
+
try:
|
|
226
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
227
|
+
resolution = json.loads(resolution_path.read_text(encoding="utf-8"))
|
|
228
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as error:
|
|
229
|
+
raise RuntimeError(f"local source metadata is unreadable: {error}") from error
|
|
230
|
+
self._validate_local_manifest(manifest)
|
|
231
|
+
pinned = {
|
|
232
|
+
"kind": manifest["kind"],
|
|
233
|
+
"harness_id": manifest["harness_id"],
|
|
234
|
+
"resolution_identity": manifest["resolution_identity"],
|
|
235
|
+
"commit": manifest["commit"],
|
|
236
|
+
"tree": manifest["tree"],
|
|
237
|
+
"payload_sha256": manifest["payload_sha256"],
|
|
238
|
+
"payload_bytes": manifest["payload_bytes"],
|
|
239
|
+
"object_count": manifest["object_count"],
|
|
240
|
+
"file_count": manifest["file_count"],
|
|
241
|
+
}
|
|
242
|
+
if any(transport.get(key) != value for key, value in pinned.items()):
|
|
243
|
+
raise RuntimeError("local source transport metadata does not match its manifest")
|
|
244
|
+
if manifest["resolution_identity"] != self.revision_identity:
|
|
245
|
+
raise RuntimeError("local source resolution identity does not match the job-pinned identity")
|
|
246
|
+
if self.harness_ref != f'{manifest["harness_id"]}@commit:{manifest["commit"]}':
|
|
247
|
+
raise RuntimeError("local source commit does not match the job-pinned harness ref")
|
|
248
|
+
if payload_stat.st_size != manifest["payload_bytes"]:
|
|
249
|
+
raise RuntimeError("local source payload size does not match its manifest")
|
|
250
|
+
actual_digest = self._sha256_file(payload_path)
|
|
251
|
+
if actual_digest != manifest["payload_sha256"]:
|
|
252
|
+
raise RuntimeError("local source payload digest does not match its manifest")
|
|
253
|
+
if not isinstance(resolution, dict) or (
|
|
254
|
+
resolution.get("harness_id") != manifest["harness_id"]
|
|
255
|
+
or resolution.get("identity") != manifest["resolution_identity"]
|
|
256
|
+
or resolution.get("revision", {}).get("commit") != manifest["commit"]
|
|
257
|
+
or resolution.get("source", {}).get("type") != "git"
|
|
258
|
+
or resolution.get("source", {}).get("registered") is not False
|
|
259
|
+
):
|
|
260
|
+
raise RuntimeError("local source locked resolution does not match its manifest")
|
|
261
|
+
return manifest
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def _assert_regular_host_file(path: Path, label: str, maximum: int) -> Any:
|
|
265
|
+
try:
|
|
266
|
+
info = path.lstat()
|
|
267
|
+
except OSError as error:
|
|
268
|
+
raise RuntimeError(f"{label} is missing or unreadable") from error
|
|
269
|
+
if not stat_module.S_ISREG(info.st_mode):
|
|
270
|
+
raise RuntimeError(f"{label} must be a regular file")
|
|
271
|
+
if info.st_size > maximum:
|
|
272
|
+
raise RuntimeError(f"{label} exceeds the size limit ({maximum} bytes)")
|
|
273
|
+
return info
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def _sha256_file(path: Path) -> str:
|
|
277
|
+
digest = hashlib.sha256()
|
|
278
|
+
with path.open("rb") as handle:
|
|
279
|
+
while chunk := handle.read(1024 * 1024):
|
|
280
|
+
digest.update(chunk)
|
|
281
|
+
return "sha256:" + digest.hexdigest()
|
|
282
|
+
|
|
283
|
+
@staticmethod
|
|
284
|
+
def _validate_local_manifest(manifest: Any) -> None:
|
|
285
|
+
allowed = {
|
|
286
|
+
"schema_version", "kind", "harness_id", "resolution_identity", "commit", "tree",
|
|
287
|
+
"payload_sha256", "payload_bytes", "object_count", "file_count", "created_at",
|
|
288
|
+
}
|
|
289
|
+
if not isinstance(manifest, dict) or set(manifest) != allowed:
|
|
290
|
+
raise RuntimeError("local source manifest fields are invalid")
|
|
291
|
+
if manifest.get("schema_version") != LOCAL_GIT_TRANSPORT_MANIFEST_VERSION:
|
|
292
|
+
raise RuntimeError("unsupported local source transport manifest schema")
|
|
293
|
+
if manifest.get("kind") != "local-git-commit":
|
|
294
|
+
raise RuntimeError("unsupported local source transport kind")
|
|
295
|
+
if not isinstance(manifest.get("harness_id"), str) or not re.fullmatch(r"[a-z][a-z0-9-]*", manifest["harness_id"]):
|
|
296
|
+
raise RuntimeError("local source manifest harness id is invalid")
|
|
297
|
+
if not isinstance(manifest.get("resolution_identity"), str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", manifest["resolution_identity"]):
|
|
298
|
+
raise RuntimeError("local source manifest resolution identity is invalid")
|
|
299
|
+
commit = manifest.get("commit")
|
|
300
|
+
if not isinstance(commit, str) or not re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", commit):
|
|
301
|
+
raise RuntimeError("local source manifest commit is not a full lowercase OID")
|
|
302
|
+
tree = manifest.get("tree")
|
|
303
|
+
if not isinstance(tree, str) or not re.fullmatch(rf"[0-9a-f]{{{len(commit)}}}", tree):
|
|
304
|
+
raise RuntimeError("local source manifest tree is invalid")
|
|
305
|
+
if not isinstance(manifest.get("payload_sha256"), str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", manifest["payload_sha256"]):
|
|
306
|
+
raise RuntimeError("local source manifest payload digest is invalid")
|
|
307
|
+
for key, maximum in (
|
|
308
|
+
("payload_bytes", LOCAL_GIT_TRANSPORT_MAX_BYTES),
|
|
309
|
+
("object_count", 100_000),
|
|
310
|
+
("file_count", 50_000),
|
|
311
|
+
):
|
|
312
|
+
value = manifest.get(key)
|
|
313
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 0 or value > maximum:
|
|
314
|
+
raise RuntimeError(f"local source manifest {key} is invalid")
|
|
315
|
+
if not isinstance(manifest.get("created_at"), str) or not manifest["created_at"]:
|
|
316
|
+
raise RuntimeError("local source manifest created_at is invalid")
|
|
317
|
+
try:
|
|
318
|
+
datetime.fromisoformat(manifest["created_at"].replace("Z", "+00:00"))
|
|
319
|
+
except ValueError as error:
|
|
320
|
+
raise RuntimeError("local source manifest created_at is invalid") from error
|
|
321
|
+
|
|
322
|
+
async def _upload_and_materialize_local_source(
|
|
323
|
+
self,
|
|
324
|
+
environment: BaseEnvironment,
|
|
325
|
+
manifest: dict[str, Any],
|
|
326
|
+
) -> None:
|
|
327
|
+
transport = self.local_source_transport or {}
|
|
328
|
+
await self._exec(environment, f"mkdir -p {LOCAL_GIT_REMOTE_ROOT} && chmod 700 {LOCAL_GIT_REMOTE_ROOT}")
|
|
329
|
+
await environment.upload_file(Path(str(transport["manifest_path"])), f"{LOCAL_GIT_REMOTE_ROOT}/manifest.json")
|
|
330
|
+
await environment.upload_file(Path(str(transport["payload_path"])), f"{LOCAL_GIT_REMOTE_ROOT}/payload.pack")
|
|
331
|
+
await environment.upload_file(Path(str(transport["locked_resolution_path"])), f"{LOCAL_GIT_REMOTE_ROOT}/resolution.json")
|
|
332
|
+
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")
|
|
333
|
+
# Verify both the uploaded manifest fields and payload bytes inside the
|
|
334
|
+
# trial before importing any Git object. All interpolated values passed
|
|
335
|
+
# to the shell have already been restricted to digest/OID grammars.
|
|
336
|
+
verifier = """
|
|
337
|
+
const fs = require('node:fs');
|
|
338
|
+
const crypto = require('node:crypto');
|
|
339
|
+
const expected = JSON.parse(process.argv[1]);
|
|
340
|
+
const manifest = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
341
|
+
if (JSON.stringify(Object.keys(manifest).sort()) !== JSON.stringify(Object.keys(expected).sort())) throw new Error('manifest fields mismatch');
|
|
342
|
+
for (const key of Object.keys(expected)) if (manifest[key] !== expected[key]) throw new Error(`manifest mismatch: ${key}`);
|
|
343
|
+
if (fs.statSync(process.argv[3]).size !== expected.payload_bytes) throw new Error('payload size mismatch');
|
|
344
|
+
(async () => {
|
|
345
|
+
const hash = crypto.createHash('sha256');
|
|
346
|
+
for await (const chunk of fs.createReadStream(process.argv[3])) hash.update(chunk);
|
|
347
|
+
const digest = 'sha256:' + hash.digest('hex');
|
|
348
|
+
if (digest !== expected.payload_sha256) throw new Error('payload digest mismatch');
|
|
349
|
+
})().catch((error) => { console.error(error.message); process.exit(1); });
|
|
350
|
+
""".strip()
|
|
351
|
+
expected = {
|
|
352
|
+
"schema_version": manifest["schema_version"],
|
|
353
|
+
"kind": manifest["kind"],
|
|
354
|
+
"harness_id": manifest["harness_id"],
|
|
355
|
+
"resolution_identity": manifest["resolution_identity"],
|
|
356
|
+
"commit": manifest["commit"],
|
|
357
|
+
"tree": manifest["tree"],
|
|
358
|
+
"payload_sha256": manifest["payload_sha256"],
|
|
359
|
+
"payload_bytes": manifest["payload_bytes"],
|
|
360
|
+
"object_count": manifest["object_count"],
|
|
361
|
+
"file_count": manifest["file_count"],
|
|
362
|
+
"created_at": manifest["created_at"],
|
|
363
|
+
}
|
|
364
|
+
verify_command = " ".join([
|
|
365
|
+
self._node_prefix(),
|
|
366
|
+
"node", "-e", shlex.quote(verifier),
|
|
367
|
+
shlex.quote(json.dumps(expected, separators=(",", ":"))),
|
|
368
|
+
f"{LOCAL_GIT_REMOTE_ROOT}/manifest.json",
|
|
369
|
+
f"{LOCAL_GIT_REMOTE_ROOT}/payload.pack",
|
|
370
|
+
])
|
|
371
|
+
await self._exec(environment, verify_command)
|
|
372
|
+
commit = manifest["commit"]
|
|
373
|
+
tree = manifest["tree"]
|
|
374
|
+
materialize = f"""
|
|
375
|
+
set -eu
|
|
376
|
+
git init --bare {LOCAL_GIT_REMOTE_ROOT}/repo.git >/dev/null
|
|
377
|
+
git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git index-pack --stdin < {LOCAL_GIT_REMOTE_ROOT}/payload.pack >/dev/null
|
|
378
|
+
test "$(git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git rev-parse {commit}^{{commit}})" = "{commit}"
|
|
379
|
+
test "$(git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git rev-parse {commit}^{{tree}})" = "{tree}"
|
|
380
|
+
printf '%s\n' {commit} > {LOCAL_GIT_REMOTE_ROOT}/repo.git/shallow
|
|
381
|
+
git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commit}
|
|
382
|
+
"""
|
|
383
|
+
await self._exec(environment, materialize)
|
|
384
|
+
|
|
385
|
+
def _local_source_cli_args(self) -> list[str]:
|
|
386
|
+
if self._local_manifest is None or self._artifact_uploaded:
|
|
387
|
+
return []
|
|
388
|
+
return [
|
|
389
|
+
"--internal-locked-resolution", f"{LOCAL_GIT_REMOTE_ROOT}/resolution.json",
|
|
390
|
+
"--internal-local-git-manifest", f"{LOCAL_GIT_REMOTE_ROOT}/manifest.json",
|
|
391
|
+
"--internal-local-git-source", f"{LOCAL_GIT_REMOTE_ROOT}/repo.git",
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
def _artifact_cli_args(self) -> list[str]:
|
|
395
|
+
if not self._artifact_uploaded or self._artifact_manifest is None:
|
|
396
|
+
return []
|
|
397
|
+
manifest = self._artifact_manifest
|
|
398
|
+
return [
|
|
399
|
+
"--internal-prepared-artifact", HARNESS_ARTIFACT_REMOTE_ROOT,
|
|
400
|
+
"--internal-artifact-id", str(manifest["artifact_id"]),
|
|
401
|
+
"--internal-artifact-integrity", str(manifest["artifact_integrity"]),
|
|
402
|
+
"--internal-artifact-entrypoint-integrity", str(manifest["entrypoint_integrity"]),
|
|
403
|
+
"--internal-artifact-revision-identity", str(manifest["revision_identity"]),
|
|
404
|
+
"--internal-artifact-platform", str(manifest["platform"]),
|
|
405
|
+
]
|
|
100
406
|
|
|
101
407
|
@staticmethod
|
|
102
408
|
def _remote_entry(entrypoint: str) -> str:
|
|
@@ -196,7 +502,41 @@ class HitchHarborAgent(BaseAgent):
|
|
|
196
502
|
context: AgentContext,
|
|
197
503
|
) -> None:
|
|
198
504
|
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
|
505
|
+
run_id = "run_" + uuid.uuid4().hex
|
|
506
|
+
trial_id, task_id, attempt = self._trial_identity()
|
|
507
|
+
context_payload: dict[str, Any] = {"kind": "ad_hoc"}
|
|
508
|
+
parent_payload: dict[str, Any] | None = None
|
|
509
|
+
if all((self.eval_id, self.benchmark_id, self.benchmark_revision, self.verifier_identity)):
|
|
510
|
+
workspace_digest = await self._workspace_digest(environment)
|
|
511
|
+
task_digest_input = json.dumps(
|
|
512
|
+
{
|
|
513
|
+
"benchmark_id": self.benchmark_id,
|
|
514
|
+
"benchmark_revision": self.benchmark_revision,
|
|
515
|
+
"task_id": task_id,
|
|
516
|
+
"instruction": instruction,
|
|
517
|
+
"initial_workspace_digest": workspace_digest,
|
|
518
|
+
},
|
|
519
|
+
ensure_ascii=False,
|
|
520
|
+
separators=(",", ":"),
|
|
521
|
+
sort_keys=True,
|
|
522
|
+
).encode("utf-8")
|
|
523
|
+
context_payload = {
|
|
524
|
+
"kind": "benchmark_task",
|
|
525
|
+
"benchmark_id": self.benchmark_id,
|
|
526
|
+
"benchmark_revision": self.benchmark_revision,
|
|
527
|
+
"task_id": task_id,
|
|
528
|
+
"task_digest": "sha256:" + hashlib.sha256(task_digest_input).hexdigest(),
|
|
529
|
+
"verifier_identity": self.verifier_identity,
|
|
530
|
+
}
|
|
531
|
+
parent_payload = {
|
|
532
|
+
"kind": "eval",
|
|
533
|
+
"eval_id": self.eval_id,
|
|
534
|
+
"trial_id": trial_id,
|
|
535
|
+
"attempt": attempt,
|
|
536
|
+
}
|
|
199
537
|
temporary: Path | None = None
|
|
538
|
+
context_temporary: Path | None = None
|
|
539
|
+
parent_temporary: Path | None = None
|
|
200
540
|
try:
|
|
201
541
|
with tempfile.NamedTemporaryFile(
|
|
202
542
|
mode="w",
|
|
@@ -210,9 +550,18 @@ class HitchHarborAgent(BaseAgent):
|
|
|
210
550
|
temporary = Path(handle.name)
|
|
211
551
|
remote_instruction = "/tmp/hitch-instruction.txt"
|
|
212
552
|
await environment.upload_file(temporary, remote_instruction)
|
|
553
|
+
context_temporary = self._temporary_json("hitch-context-", context_payload)
|
|
554
|
+
await environment.upload_file(context_temporary, "/tmp/hitch-context.json")
|
|
555
|
+
if parent_payload is not None:
|
|
556
|
+
parent_temporary = self._temporary_json("hitch-parent-", parent_payload)
|
|
557
|
+
await environment.upload_file(parent_temporary, "/tmp/hitch-parent.json")
|
|
213
558
|
finally:
|
|
214
559
|
if temporary is not None:
|
|
215
560
|
temporary.unlink(missing_ok=True)
|
|
561
|
+
if context_temporary is not None:
|
|
562
|
+
context_temporary.unlink(missing_ok=True)
|
|
563
|
+
if parent_temporary is not None:
|
|
564
|
+
parent_temporary.unlink(missing_ok=True)
|
|
216
565
|
|
|
217
566
|
if self._entrypoint is None:
|
|
218
567
|
raise RuntimeError("Hitch agent setup() must run before run() to resolve the runtime entrypoint")
|
|
@@ -220,20 +569,31 @@ class HitchHarborAgent(BaseAgent):
|
|
|
220
569
|
arguments = [
|
|
221
570
|
self._node_prefix(),
|
|
222
571
|
"HITCH_ROOT=/tmp/hitch-state",
|
|
572
|
+
*(["HITCH_HARBOR_INTERNAL=1"] if self._local_manifest is not None or self._artifact_uploaded or parent_payload is not None else []),
|
|
223
573
|
f"node {entry} run",
|
|
224
574
|
"--harness",
|
|
225
575
|
shlex.quote(self.harness_ref),
|
|
576
|
+
*self._local_source_cli_args(),
|
|
577
|
+
*self._artifact_cli_args(),
|
|
226
578
|
"--cwd",
|
|
227
579
|
shlex.quote(self.workdir),
|
|
228
580
|
"--workspace-mode",
|
|
229
581
|
"shared",
|
|
230
582
|
"--prompt-file",
|
|
231
583
|
shlex.quote(remote_instruction),
|
|
584
|
+
"--context-file",
|
|
585
|
+
"/tmp/hitch-context.json",
|
|
232
586
|
"--timeout",
|
|
233
587
|
str(self.hitch_timeout_ms),
|
|
234
588
|
"--output",
|
|
235
589
|
"jsonl",
|
|
236
590
|
]
|
|
591
|
+
if parent_payload is not None:
|
|
592
|
+
arguments.extend([
|
|
593
|
+
"--parent-file", "/tmp/hitch-parent.json",
|
|
594
|
+
"--internal-run-id", run_id,
|
|
595
|
+
"--internal-defer-benchmark-observation",
|
|
596
|
+
])
|
|
237
597
|
if self.model_name:
|
|
238
598
|
arguments.extend(["--model", shlex.quote(self.model_name)])
|
|
239
599
|
for value in self.agent_args:
|
|
@@ -246,7 +606,9 @@ class HitchHarborAgent(BaseAgent):
|
|
|
246
606
|
)
|
|
247
607
|
execution = await environment.exec(command, cwd=self.workdir)
|
|
248
608
|
events = self._events(execution.stdout or "")
|
|
249
|
-
|
|
609
|
+
observed_run_id = next((event.get("run_id") for event in events if event.get("run_id")), None)
|
|
610
|
+
if observed_run_id:
|
|
611
|
+
run_id = str(observed_run_id)
|
|
250
612
|
hitch_result = None
|
|
251
613
|
if run_id:
|
|
252
614
|
result_path = f"/tmp/hitch-state/runs/{run_id}/result.json"
|
|
@@ -255,15 +617,51 @@ class HitchHarborAgent(BaseAgent):
|
|
|
255
617
|
)
|
|
256
618
|
if result.return_code == 0 and result.stdout:
|
|
257
619
|
hitch_result = json.loads(result.stdout)
|
|
620
|
+
export = await environment.exec(
|
|
621
|
+
f"""
|
|
622
|
+
set -eu
|
|
623
|
+
source_dir={shlex.quote(f'/tmp/hitch-state/runs/{run_id}')}
|
|
624
|
+
target_dir=/logs/agent/hitch-run-bundle
|
|
625
|
+
rm -rf "$target_dir"
|
|
626
|
+
mkdir -p "$target_dir"
|
|
627
|
+
for name in request.json resolution.json manifest.json result.json events.jsonl stdout.log stderr.log trajectory.ref.json trajectory; do
|
|
628
|
+
if [ -e "$source_dir/$name" ]; then cp -a "$source_dir/$name" "$target_dir/$name"; fi
|
|
629
|
+
done
|
|
630
|
+
""".strip()
|
|
631
|
+
)
|
|
632
|
+
if export.return_code != 0:
|
|
633
|
+
raise RuntimeError("Hitch run bundle export failed before trial teardown")
|
|
258
634
|
context.metadata = {
|
|
259
635
|
"candidate_id": self.candidate_id,
|
|
260
636
|
"harness_ref": self.harness_ref,
|
|
261
637
|
"revision_identity": self.revision_identity,
|
|
262
638
|
"controller_runtime_id": self.controller_runtime_id,
|
|
263
639
|
"hitch_run_id": run_id,
|
|
640
|
+
"hitch_run_bundle": "hitch-run-bundle",
|
|
641
|
+
"eval_id": self.eval_id,
|
|
642
|
+
"trial_id": trial_id,
|
|
643
|
+
"task_id": task_id,
|
|
644
|
+
"attempt": attempt,
|
|
264
645
|
"hitch_status": hitch_result.get("status") if hitch_result else None,
|
|
265
646
|
"hitch_artifact_id": hitch_result.get("artifact_id") if hitch_result else None,
|
|
266
647
|
}
|
|
648
|
+
if self._local_manifest is not None:
|
|
649
|
+
context.metadata["local_source_transport"] = {
|
|
650
|
+
"kind": self._local_manifest["kind"],
|
|
651
|
+
"commit": self._local_manifest["commit"],
|
|
652
|
+
"tree": self._local_manifest["tree"],
|
|
653
|
+
"resolution_identity": self._local_manifest["resolution_identity"],
|
|
654
|
+
"payload_sha256": self._local_manifest["payload_sha256"],
|
|
655
|
+
"payload_bytes": self._local_manifest["payload_bytes"],
|
|
656
|
+
"status": "verified",
|
|
657
|
+
}
|
|
658
|
+
if self._artifact_manifest is not None:
|
|
659
|
+
context.metadata["harness_artifact_transport"] = {
|
|
660
|
+
"artifact_id": self._artifact_manifest["artifact_id"],
|
|
661
|
+
"artifact_integrity": self._artifact_manifest["artifact_integrity"],
|
|
662
|
+
"platform": self._artifact_manifest["platform"],
|
|
663
|
+
"status": "uploaded" if self._artifact_uploaded else "incompatible_platform_fallback",
|
|
664
|
+
}
|
|
267
665
|
if execution.return_code != 0:
|
|
268
666
|
message = (execution.stderr or "").strip()
|
|
269
667
|
if hitch_result and hitch_result.get("error", {}).get("message"):
|
|
@@ -279,6 +677,80 @@ class HitchHarborAgent(BaseAgent):
|
|
|
279
677
|
f"expected {self.revision_identity}, got {hitch_result.get('revision_identity')}"
|
|
280
678
|
)
|
|
281
679
|
|
|
680
|
+
def _trial_identity(self) -> tuple[str, str, int]:
|
|
681
|
+
"""Read Harbor's stable trial/task identity from the persisted trial state."""
|
|
682
|
+
trial_dir = self.logs_dir.parent if self.logs_dir.name == "agent" else self.logs_dir
|
|
683
|
+
trial_id = trial_dir.name or "trial__1"
|
|
684
|
+
task_id = self._locked_task_id(trial_dir)
|
|
685
|
+
match = re.fullmatch(r"(.+)__(\d+)", trial_id)
|
|
686
|
+
if match:
|
|
687
|
+
return trial_id, task_id or match.group(1), max(1, int(match.group(2)))
|
|
688
|
+
# Harbor 0.21 uses a random seven-character suffix and truncates the
|
|
689
|
+
# task portion of trial_name. It therefore cannot be used to recover
|
|
690
|
+
# task identity; the trial lock above is authoritative. Keep the split
|
|
691
|
+
# only as a compatibility fallback for older/fake Harbor runtimes.
|
|
692
|
+
fallback_task_id = trial_id.rsplit("__", 1)[0] if "__" in trial_id else trial_id
|
|
693
|
+
return trial_id, task_id or fallback_task_id, 1
|
|
694
|
+
|
|
695
|
+
@staticmethod
|
|
696
|
+
def _locked_task_id(trial_dir: Path) -> str | None:
|
|
697
|
+
lock_path = trial_dir / "lock.json"
|
|
698
|
+
if not lock_path.is_file():
|
|
699
|
+
return None
|
|
700
|
+
try:
|
|
701
|
+
lock = json.loads(lock_path.read_text(encoding="utf-8"))
|
|
702
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as error:
|
|
703
|
+
raise RuntimeError(f"Harbor trial lock is unreadable: {lock_path}") from error
|
|
704
|
+
task = lock.get("task") if isinstance(lock, dict) else None
|
|
705
|
+
task_id = task.get("name") if isinstance(task, dict) else None
|
|
706
|
+
if not isinstance(task_id, str) or not task_id.strip():
|
|
707
|
+
raise RuntimeError(f"Harbor trial lock has no task.name: {lock_path}")
|
|
708
|
+
return task_id.strip()
|
|
709
|
+
|
|
710
|
+
def _temporary_json(self, prefix: str, value: dict[str, Any]) -> Path:
|
|
711
|
+
with tempfile.NamedTemporaryFile(
|
|
712
|
+
mode="w",
|
|
713
|
+
encoding="utf-8",
|
|
714
|
+
prefix=prefix,
|
|
715
|
+
suffix=".json",
|
|
716
|
+
dir=self.logs_dir,
|
|
717
|
+
delete=False,
|
|
718
|
+
) as handle:
|
|
719
|
+
json.dump(value, handle, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
|
|
720
|
+
handle.write("\n")
|
|
721
|
+
return Path(handle.name)
|
|
722
|
+
|
|
723
|
+
async def _workspace_digest(self, environment: BaseEnvironment) -> str:
|
|
724
|
+
script = r"""
|
|
725
|
+
const fs = require('node:fs');
|
|
726
|
+
const path = require('node:path');
|
|
727
|
+
const crypto = require('node:crypto');
|
|
728
|
+
const hash = crypto.createHash('sha256');
|
|
729
|
+
function walk(root, relative = '') {
|
|
730
|
+
const directory = path.join(root, relative);
|
|
731
|
+
for (const name of fs.readdirSync(directory).sort()) {
|
|
732
|
+
const child = relative ? path.join(relative, name) : name;
|
|
733
|
+
const absolute = path.join(root, child);
|
|
734
|
+
const info = fs.lstatSync(absolute);
|
|
735
|
+
if (info.isDirectory()) { hash.update(`d\0${child}\0${info.mode & 0o7777}\0`); walk(root, child); }
|
|
736
|
+
else if (info.isFile()) { hash.update(`f\0${child}\0${info.mode & 0o7777}\0${info.size}\0`); hash.update(fs.readFileSync(absolute)); hash.update('\0'); }
|
|
737
|
+
else if (info.isSymbolicLink()) { hash.update(`l\0${child}\0${fs.readlinkSync(absolute)}\0`); }
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
walk(process.argv[1]);
|
|
741
|
+
process.stdout.write('sha256:' + hash.digest('hex'));
|
|
742
|
+
""".strip()
|
|
743
|
+
result = await environment.exec(
|
|
744
|
+
" ".join([self._node_prefix(), "node", "-e", shlex.quote(script), shlex.quote(self.workdir)]),
|
|
745
|
+
cwd=self.workdir,
|
|
746
|
+
)
|
|
747
|
+
digest = (result.stdout or "").strip()
|
|
748
|
+
if result.return_code == 0 and re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
|
|
749
|
+
return digest
|
|
750
|
+
# Older/fake Harbor environments may not expose a readable workspace
|
|
751
|
+
# during bridge tests. Keep a deterministic, visibly synthetic input.
|
|
752
|
+
return "sha256:" + hashlib.sha256(b"workspace-unavailable").hexdigest()
|
|
753
|
+
|
|
282
754
|
async def _ensure_node(self, environment: BaseEnvironment) -> None:
|
|
283
755
|
probe = await environment.exec(
|
|
284
756
|
"node -e 'process.exit(Number(process.versions.node.split(\".\")[0]) >= 22 ? 0 : 1)'"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-hitch",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Content-addressed version control and evidence storage for agent harnesses",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -18,6 +18,23 @@
|
|
|
18
18
|
"url": "git+https://github.com/rsi-gear/agent-hitch.git"
|
|
19
19
|
},
|
|
20
20
|
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
"./adapters": "./dist/src/adapters/index.js",
|
|
23
|
+
"./artifacts": "./dist/src/artifacts/index.js",
|
|
24
|
+
"./backends": "./dist/src/backends/index.js",
|
|
25
|
+
"./controller-runtime": "./dist/src/controller-runtime/index.js",
|
|
26
|
+
"./daemon": "./dist/src/daemon/index.js",
|
|
27
|
+
"./domain": "./dist/src/domain/index.js",
|
|
28
|
+
"./evals": "./dist/src/evals/index.js",
|
|
29
|
+
"./feedback": "./dist/src/feedback/index.js",
|
|
30
|
+
"./foundation": "./dist/src/foundation/index.js",
|
|
31
|
+
"./revisions": "./dist/src/revisions/index.js",
|
|
32
|
+
"./runs": "./dist/src/runs/index.js",
|
|
33
|
+
"./trajectories": "./dist/src/trajectories/index.js",
|
|
34
|
+
"./workspaces": "./dist/src/workspaces/index.js",
|
|
35
|
+
"./cli": "./dist/src/cli/index.js",
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
21
38
|
"bin": {
|
|
22
39
|
"hitch": "dist/bin/hitch.js"
|
|
23
40
|
},
|
|
@@ -30,10 +47,11 @@
|
|
|
30
47
|
],
|
|
31
48
|
"scripts": {
|
|
32
49
|
"typecheck": "tsc --noEmit",
|
|
33
|
-
"build": "tsc -p tsconfig.json",
|
|
50
|
+
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
34
51
|
"test": "node --test \"dist/test/*.test.js\"",
|
|
35
52
|
"coverage": "node --experimental-test-coverage --test-coverage-include=dist/src/**/*.js --test-coverage-lines=75 --test-coverage-branches=65 --test-coverage-functions=70 --test \"dist/test/*.test.js\"",
|
|
36
|
-
"check": "
|
|
53
|
+
"check:architecture": "node dist/scripts/check-architecture.js",
|
|
54
|
+
"check": "npm run typecheck && npm run build && npm run check:architecture && node dist/scripts/check-syntax.js && node --test \"dist/test/*.test.js\"",
|
|
37
55
|
"prepublishOnly": "npm run check"
|
|
38
56
|
},
|
|
39
57
|
"engines": {
|