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.
@@ -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._local_manifest is not None:
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
- prepare = " ".join(
118
- [
119
- self._node_prefix(),
120
- "HITCH_ROOT=/tmp/hitch-state",
121
- *(["HITCH_HARBOR_INTERNAL=1"] if self._local_manifest is not None else []),
122
- f"node {entry} prepare",
123
- shlex.quote(self.harness_ref),
124
- *self._local_source_cli_args(),
125
- "--json",
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
- await self._exec(environment, prepare)
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"):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hitch",
3
- "version": "0.2.1",
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",