agent-hitch 0.2.1 → 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 -256
- package/README.zh-CN.md +84 -235
- package/dist/src/artifacts/handoff.js +12 -0
- package/dist/src/artifacts/handoff.js.map +1 -0
- package/dist/src/artifacts/index.js +2 -0
- package/dist/src/artifacts/index.js.map +1 -1
- package/dist/src/artifacts/store.js +72 -2
- package/dist/src/artifacts/store.js.map +1 -1
- package/dist/src/backends/harbor/backend.js +15 -2
- package/dist/src/backends/harbor/backend.js.map +1 -1
- package/dist/src/cli/commands/run.js +65 -3
- package/dist/src/cli/commands/run.js.map +1 -1
- package/dist/src/evals/service.js +54 -1
- package/dist/src/evals/service.js.map +1 -1
- package/dist/src/runs/executor.js +10 -4
- package/dist/src/runs/executor.js.map +1 -1
- package/docs/schemas/eval-result.schema.json +14 -0
- package/integrations/harbor/hitch_harbor_agent.py +112 -14
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ CONTROLLER_RUNTIME_MANIFEST_VERSION = "2"
|
|
|
21
21
|
LOCAL_GIT_TRANSPORT_MANIFEST_VERSION = "1"
|
|
22
22
|
LOCAL_GIT_TRANSPORT_MAX_BYTES = 512 * 1024 * 1024
|
|
23
23
|
LOCAL_GIT_REMOTE_ROOT = "/opt/hitch-local-source"
|
|
24
|
+
HARNESS_ARTIFACT_REMOTE_ROOT = "/opt/hitch-harness-artifact"
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class HitchHarborAgent(BaseAgent):
|
|
@@ -34,6 +35,7 @@ class HitchHarborAgent(BaseAgent):
|
|
|
34
35
|
hitch_runtime_dir: str,
|
|
35
36
|
candidate_id: str = "candidate-1",
|
|
36
37
|
controller_runtime_id: str | None = None,
|
|
38
|
+
harness_artifact: dict[str, Any] | None = None,
|
|
37
39
|
local_source_transport: dict[str, Any] | None = None,
|
|
38
40
|
hitch_timeout_ms: int = 900_000,
|
|
39
41
|
agent_args: list[str] | None = None,
|
|
@@ -49,6 +51,7 @@ class HitchHarborAgent(BaseAgent):
|
|
|
49
51
|
self.revision_identity = revision_identity
|
|
50
52
|
self.hitch_runtime_dir = Path(hitch_runtime_dir)
|
|
51
53
|
self.controller_runtime_id = controller_runtime_id
|
|
54
|
+
self.harness_artifact = dict(harness_artifact) if harness_artifact else None
|
|
52
55
|
self.local_source_transport = dict(local_source_transport) if local_source_transport else None
|
|
53
56
|
self.candidate_id = candidate_id
|
|
54
57
|
self.hitch_timeout_ms = int(hitch_timeout_ms)
|
|
@@ -60,6 +63,8 @@ class HitchHarborAgent(BaseAgent):
|
|
|
60
63
|
self.verifier_identity = verifier_identity
|
|
61
64
|
self._hitch_version: str | None = None
|
|
62
65
|
self._entrypoint: str | None = None
|
|
66
|
+
self._artifact_manifest: dict[str, Any] | None = None
|
|
67
|
+
self._artifact_uploaded = False
|
|
63
68
|
self._local_manifest: dict[str, Any] | None = None
|
|
64
69
|
|
|
65
70
|
@staticmethod
|
|
@@ -93,6 +98,11 @@ class HitchHarborAgent(BaseAgent):
|
|
|
93
98
|
# and the actual container upload, spec §4.6).
|
|
94
99
|
self._verify_manifest_identity(manifest)
|
|
95
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
|
|
96
106
|
if self.local_source_transport is not None:
|
|
97
107
|
try:
|
|
98
108
|
self._local_manifest = self._verify_local_source_host()
|
|
@@ -106,7 +116,15 @@ class HitchHarborAgent(BaseAgent):
|
|
|
106
116
|
# bookkeeping and is not identity (spec §4.2).
|
|
107
117
|
await environment.upload_dir(payload_dir, "/opt/hitch")
|
|
108
118
|
await self._ensure_node(environment)
|
|
109
|
-
if self.
|
|
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:
|
|
110
128
|
try:
|
|
111
129
|
await self._upload_and_materialize_local_source(environment, self._local_manifest)
|
|
112
130
|
except Exception as error:
|
|
@@ -114,18 +132,77 @@ class HitchHarborAgent(BaseAgent):
|
|
|
114
132
|
entry = self._remote_entry(entrypoint)
|
|
115
133
|
version = await self._exec(environment, f"{self._node_prefix()} node {entry} --version")
|
|
116
134
|
self._hitch_version = (version.stdout or "").strip() or None
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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"',
|
|
127
201
|
)
|
|
128
|
-
|
|
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
|
|
129
206
|
|
|
130
207
|
def _verify_local_source_host(self) -> dict[str, Any]:
|
|
131
208
|
"""Validate the independent local-source handoff immediately before upload."""
|
|
@@ -306,7 +383,7 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
|
|
|
306
383
|
await self._exec(environment, materialize)
|
|
307
384
|
|
|
308
385
|
def _local_source_cli_args(self) -> list[str]:
|
|
309
|
-
if self._local_manifest is None:
|
|
386
|
+
if self._local_manifest is None or self._artifact_uploaded:
|
|
310
387
|
return []
|
|
311
388
|
return [
|
|
312
389
|
"--internal-locked-resolution", f"{LOCAL_GIT_REMOTE_ROOT}/resolution.json",
|
|
@@ -314,6 +391,19 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
|
|
|
314
391
|
"--internal-local-git-source", f"{LOCAL_GIT_REMOTE_ROOT}/repo.git",
|
|
315
392
|
]
|
|
316
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
|
+
]
|
|
406
|
+
|
|
317
407
|
@staticmethod
|
|
318
408
|
def _remote_entry(entrypoint: str) -> str:
|
|
319
409
|
"""Shell-quote the full remote path so the entrypoint is always a
|
|
@@ -479,11 +569,12 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
|
|
|
479
569
|
arguments = [
|
|
480
570
|
self._node_prefix(),
|
|
481
571
|
"HITCH_ROOT=/tmp/hitch-state",
|
|
482
|
-
*(["HITCH_HARBOR_INTERNAL=1"] if self._local_manifest is not None or parent_payload is not None else []),
|
|
572
|
+
*(["HITCH_HARBOR_INTERNAL=1"] if self._local_manifest is not None or self._artifact_uploaded or parent_payload is not None else []),
|
|
483
573
|
f"node {entry} run",
|
|
484
574
|
"--harness",
|
|
485
575
|
shlex.quote(self.harness_ref),
|
|
486
576
|
*self._local_source_cli_args(),
|
|
577
|
+
*self._artifact_cli_args(),
|
|
487
578
|
"--cwd",
|
|
488
579
|
shlex.quote(self.workdir),
|
|
489
580
|
"--workspace-mode",
|
|
@@ -564,6 +655,13 @@ done
|
|
|
564
655
|
"payload_bytes": self._local_manifest["payload_bytes"],
|
|
565
656
|
"status": "verified",
|
|
566
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
|
+
}
|
|
567
665
|
if execution.return_code != 0:
|
|
568
666
|
message = (execution.stderr or "").strip()
|
|
569
667
|
if hitch_result and hitch_result.get("error", {}).get("message"):
|