agent-hitch 0.2.5 → 0.2.6
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/dist/src/backends/harbor/backend.js +2 -1
- package/dist/src/backends/harbor/backend.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/cli/arguments.js +14 -0
- package/dist/src/cli/arguments.js.map +1 -1
- package/dist/src/cli/output.js +2 -1
- package/dist/src/cli/output.js.map +1 -1
- package/dist/src/evals/harbor-bridge-error.js +22 -1
- package/dist/src/evals/harbor-bridge-error.js.map +1 -1
- package/dist/src/evals/index.js +1 -1
- package/dist/src/evals/index.js.map +1 -1
- package/dist/src/evals/infrastructure-retry.js +181 -0
- package/dist/src/evals/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-slots.js +10 -2
- package/dist/src/evals/rerun-slots.js.map +1 -1
- package/dist/src/evals/service.js +57 -2
- package/dist/src/evals/service.js.map +1 -1
- package/dist/src/evals/trial-import.js +32 -32
- package/dist/src/evals/trial-import.js.map +1 -1
- package/dist/src/evals/verifier-diagnostics.js +235 -0
- package/dist/src/evals/verifier-diagnostics.js.map +1 -0
- package/docs/schemas/eval-request.schema.json +2 -0
- package/integrations/harbor/hitch_harbor_agent.py +130 -9
- package/integrations/harbor/hitch_harbor_verifier.py +314 -0
- package/package.json +3 -2
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { open, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { atomicWriteJSON } from "../foundation/index.js";
|
|
4
|
+
const MAX_LOG_BYTES = 256 * 1024;
|
|
5
|
+
const VERIFIER_LOG_FILES = ["test-stdout.txt", "test-stderr.txt", "stdout.txt", "stderr.txt"];
|
|
6
|
+
const INFRASTRUCTURE_PATTERNS = [
|
|
7
|
+
{
|
|
8
|
+
signal: "dns_resolution_failed",
|
|
9
|
+
patterns: [
|
|
10
|
+
/curl:\s*\(\d+\)\s*Could not resolve host:/i,
|
|
11
|
+
/Temporary failure in name resolution/i,
|
|
12
|
+
/Name or service not known/i,
|
|
13
|
+
/getaddrinfo\s+(?:EAI_AGAIN|ENOTFOUND)/i,
|
|
14
|
+
/Could not resolve hostname/i,
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
signal: "network_unreachable",
|
|
19
|
+
patterns: [
|
|
20
|
+
/Network is unreachable/i,
|
|
21
|
+
/Failed to establish a new connection:[^\n]*(?:timed out|connection refused)/i,
|
|
22
|
+
/Could not connect to (?:host|server)/i,
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
signal: "package_install_failed",
|
|
27
|
+
patterns: [
|
|
28
|
+
/Could not find a version that satisfies the requirement/i,
|
|
29
|
+
/No matching distribution found for/i,
|
|
30
|
+
/Failed to (?:download|fetch) [^\n]*(?:package|wheel|index)/i,
|
|
31
|
+
/error:\s*failed to (?:download|fetch|install)/i,
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
signal: "test_runner_missing",
|
|
36
|
+
patterns: [
|
|
37
|
+
/(?:^|\n)[^\n]*(?:uvx|pytest|pipx|tox|nox): command not found(?:\n|$)/i,
|
|
38
|
+
/No module named ['"]?(?:pytest|unittest|tox|nox)['"]?/i,
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
signal: "verifier_environment_missing",
|
|
43
|
+
patterns: [
|
|
44
|
+
/\/(?:root|home\/[^/]+)\/\.local\/bin\/env: No such file or directory/i,
|
|
45
|
+
/(?:^|\n)[^\n]*\/bin\/(?:python|python3|pytest|uv|uvx): No such file or directory(?:\n|$)/i,
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
const TEST_EXECUTION_EVIDENCE = [
|
|
50
|
+
/test session starts/i,
|
|
51
|
+
/collected\s+\d+\s+items?/i,
|
|
52
|
+
/(?:^|\n)Ran\s+\d+\s+tests?/i,
|
|
53
|
+
/(?:^|\n)TAP version\s+\d+/i,
|
|
54
|
+
/={3,}[^\n]*(?:passed|failed|errors?|skipped)[^\n]*={3,}/i,
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* Detect a verifier that wrote a zero reward after its own bootstrap failed.
|
|
58
|
+
*
|
|
59
|
+
* Harbor treats reward.txt as authoritative even when a shell verifier masks
|
|
60
|
+
* an earlier command failure. We fail closed only when reward is exactly zero,
|
|
61
|
+
* no structured CTRF evidence exists, no test-runner evidence appears in the
|
|
62
|
+
* bounded logs, and a stable infrastructure signature is present.
|
|
63
|
+
*/
|
|
64
|
+
export async function detectVerifierInfrastructureFailure(trialDirectory, reward) {
|
|
65
|
+
const verifierDirectory = path.join(trialDirectory, "verifier");
|
|
66
|
+
const explicit = await readExplicitDiagnostic(path.join(verifierDirectory, "infrastructure-error.json"));
|
|
67
|
+
if (explicit)
|
|
68
|
+
return explicit;
|
|
69
|
+
if (reward !== 0)
|
|
70
|
+
return null;
|
|
71
|
+
if (await nonEmptyFile(path.join(verifierDirectory, "ctrf.json")))
|
|
72
|
+
return null;
|
|
73
|
+
const logs = [];
|
|
74
|
+
const sourceFiles = [];
|
|
75
|
+
for (const name of VERIFIER_LOG_FILES) {
|
|
76
|
+
const file = path.join(verifierDirectory, name);
|
|
77
|
+
const value = await readBoundedFile(file, MAX_LOG_BYTES);
|
|
78
|
+
if (value === null)
|
|
79
|
+
continue;
|
|
80
|
+
logs.push(value);
|
|
81
|
+
sourceFiles.push(`verifier/${name}`);
|
|
82
|
+
}
|
|
83
|
+
if (logs.length === 0)
|
|
84
|
+
return null;
|
|
85
|
+
const combined = logs.join("\n");
|
|
86
|
+
if (TEST_EXECUTION_EVIDENCE.some((pattern) => pattern.test(combined)))
|
|
87
|
+
return null;
|
|
88
|
+
const signals = INFRASTRUCTURE_PATTERNS
|
|
89
|
+
.filter(({ patterns }) => patterns.some((pattern) => pattern.test(combined)))
|
|
90
|
+
.map(({ signal }) => signal);
|
|
91
|
+
if (signals.length === 0)
|
|
92
|
+
return null;
|
|
93
|
+
return {
|
|
94
|
+
schema_version: "1",
|
|
95
|
+
code: "verifier_infrastructure_failure",
|
|
96
|
+
signals,
|
|
97
|
+
source_files: sourceFiles,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
async function readExplicitDiagnostic(file) {
|
|
101
|
+
const raw = await readBoundedFile(file, MAX_LOG_BYTES);
|
|
102
|
+
if (raw === null)
|
|
103
|
+
return null;
|
|
104
|
+
let value;
|
|
105
|
+
try {
|
|
106
|
+
value = JSON.parse(raw);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
112
|
+
return null;
|
|
113
|
+
const record = value;
|
|
114
|
+
if (record.schema_version !== "1" || record.code !== "verifier_infrastructure_failure")
|
|
115
|
+
return null;
|
|
116
|
+
const signals = infrastructureSignals(record.signals);
|
|
117
|
+
const sourceFiles = stringArray(record.source_files);
|
|
118
|
+
if (signals.length === 0 || sourceFiles.length === 0)
|
|
119
|
+
return null;
|
|
120
|
+
const attempts = Array.isArray(record.attempts)
|
|
121
|
+
? record.attempts.flatMap((attempt) => {
|
|
122
|
+
if (!attempt || typeof attempt !== "object" || Array.isArray(attempt))
|
|
123
|
+
return [];
|
|
124
|
+
const item = attempt;
|
|
125
|
+
const number = item.attempt;
|
|
126
|
+
const itemSignals = infrastructureSignals(item.signals);
|
|
127
|
+
const itemSourceFiles = stringArray(item.source_files);
|
|
128
|
+
if (!Number.isSafeInteger(number) || number < 1 || itemSignals.length === 0 || itemSourceFiles.length === 0)
|
|
129
|
+
return [];
|
|
130
|
+
return [{ attempt: number, signals: itemSignals, source_files: itemSourceFiles }];
|
|
131
|
+
})
|
|
132
|
+
: [];
|
|
133
|
+
const maxRetries = record.max_retries;
|
|
134
|
+
const backoffMs = record.backoff_ms;
|
|
135
|
+
return {
|
|
136
|
+
schema_version: "1",
|
|
137
|
+
code: "verifier_infrastructure_failure",
|
|
138
|
+
signals,
|
|
139
|
+
source_files: sourceFiles,
|
|
140
|
+
...(attempts.length > 0 ? { attempts } : {}),
|
|
141
|
+
...(Number.isSafeInteger(maxRetries) && maxRetries >= 0 ? { max_retries: maxRetries } : {}),
|
|
142
|
+
...(Number.isSafeInteger(backoffMs) && backoffMs >= 0 ? { backoff_ms: backoffMs } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function infrastructureSignals(value) {
|
|
146
|
+
if (!Array.isArray(value))
|
|
147
|
+
return [];
|
|
148
|
+
const allowed = new Set(INFRASTRUCTURE_PATTERNS.map(({ signal }) => signal));
|
|
149
|
+
return [...new Set(value.filter((entry) => typeof entry === "string" && allowed.has(entry)))];
|
|
150
|
+
}
|
|
151
|
+
function stringArray(value) {
|
|
152
|
+
return Array.isArray(value)
|
|
153
|
+
? [...new Set(value.filter((entry) => typeof entry === "string" && entry.length > 0))]
|
|
154
|
+
: [];
|
|
155
|
+
}
|
|
156
|
+
export function verifierResult(trial) {
|
|
157
|
+
return trial.verifier_result && typeof trial.verifier_result === "object" && !Array.isArray(trial.verifier_result)
|
|
158
|
+
? trial.verifier_result
|
|
159
|
+
: null;
|
|
160
|
+
}
|
|
161
|
+
export function primaryVerifierReward(trial) {
|
|
162
|
+
const rewards = (verifierResult(trial)?.rewards || {});
|
|
163
|
+
const preferred = rewards.reward;
|
|
164
|
+
if (typeof preferred === "number" && Number.isFinite(preferred))
|
|
165
|
+
return preferred;
|
|
166
|
+
return Object.values(rewards).find((value) => typeof value === "number" && Number.isFinite(value));
|
|
167
|
+
}
|
|
168
|
+
export function verifierObservation(input) {
|
|
169
|
+
const ref = input.verifierRef ? { verifier_result_ref: input.verifierRef } : {};
|
|
170
|
+
if (input.runStatus === "cancelled")
|
|
171
|
+
return { status: "invalid", invalid_reason: "cancelled", ...ref };
|
|
172
|
+
if (input.runStatus !== "succeeded" || input.recordStatus !== "valid")
|
|
173
|
+
return { status: "invalid", invalid_reason: "infrastructure_failure", ...ref };
|
|
174
|
+
if (input.trajectoryStatus !== "valid")
|
|
175
|
+
return { status: "invalid", invalid_reason: "trajectory_missing_or_corrupt", ...ref };
|
|
176
|
+
if (input.infrastructure)
|
|
177
|
+
return { status: "invalid", invalid_reason: "verifier_infrastructure_failure", ...ref };
|
|
178
|
+
// The candidate run and trajectory are already sealed and valid. A later
|
|
179
|
+
// Harbor exception belongs to verification/teardown and must never trigger
|
|
180
|
+
// a second candidate execution.
|
|
181
|
+
if (input.trial.exception_info)
|
|
182
|
+
return { status: "invalid", invalid_reason: "verifier_infrastructure_failure", ...ref };
|
|
183
|
+
const reward = primaryVerifierReward(input.trial);
|
|
184
|
+
if (reward === undefined || !input.verifierRef)
|
|
185
|
+
return { status: "invalid", invalid_reason: "verifier_result_missing" };
|
|
186
|
+
return { status: "valid", reward, verifier_result_ref: input.verifierRef };
|
|
187
|
+
}
|
|
188
|
+
export async function writeVerifierInfrastructureDiagnostic(runDirectory, diagnostic) {
|
|
189
|
+
await atomicWriteJSON(path.join(runDirectory, "verifier", "infrastructure-error.json"), {
|
|
190
|
+
...diagnostic,
|
|
191
|
+
detected_at: new Date().toISOString(),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
async function nonEmptyFile(file) {
|
|
195
|
+
try {
|
|
196
|
+
const info = await stat(file);
|
|
197
|
+
return info.isFile() && info.size > 0;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (error.code === "ENOENT")
|
|
201
|
+
return false;
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function readBoundedFile(file, maxBytes) {
|
|
206
|
+
let handle;
|
|
207
|
+
try {
|
|
208
|
+
handle = await open(file, "r");
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (error.code === "ENOENT")
|
|
212
|
+
return null;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const size = (await handle.stat()).size;
|
|
217
|
+
if (size === 0)
|
|
218
|
+
return "";
|
|
219
|
+
if (size <= maxBytes) {
|
|
220
|
+
const buffer = Buffer.alloc(size);
|
|
221
|
+
await handle.read(buffer, 0, size, 0);
|
|
222
|
+
return buffer.toString("utf8");
|
|
223
|
+
}
|
|
224
|
+
const half = Math.floor(maxBytes / 2);
|
|
225
|
+
const head = Buffer.alloc(half);
|
|
226
|
+
const tail = Buffer.alloc(maxBytes - half);
|
|
227
|
+
await handle.read(head, 0, head.length, 0);
|
|
228
|
+
await handle.read(tail, 0, tail.length, size - tail.length);
|
|
229
|
+
return `${head.toString("utf8")}\n[... verifier log truncated ...]\n${tail.toString("utf8")}`;
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
await handle.close();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=verifier-diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifier-diagnostics.js","sourceRoot":"","sources":["../../../src/evals/verifier-diagnostics.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"model": { "type": "string" },
|
|
16
16
|
"attempts": { "type": "integer", "minimum": 1 },
|
|
17
17
|
"max_concurrent": { "type": "integer", "minimum": 1 },
|
|
18
|
+
"infrastructure_retries": { "type": "integer", "minimum": 0 },
|
|
19
|
+
"infrastructure_retry_backoff_ms": { "type": "number", "minimum": 0 },
|
|
18
20
|
"timeout_ms": { "type": "number", "minimum": 0 },
|
|
19
21
|
"setup_timeout_ms": { "type": "number", "minimum": 0 },
|
|
20
22
|
"agent_args": { "type": "array", "items": { "type": "string" } },
|
|
@@ -60,7 +60,7 @@ class HitchHarborAgent(BaseAgent):
|
|
|
60
60
|
local_source_transport: dict[str, Any] | None = None,
|
|
61
61
|
hitch_timeout_ms: int = 900_000,
|
|
62
62
|
agent_args: list[str] | None = None,
|
|
63
|
-
workdir: str =
|
|
63
|
+
workdir: str | None = None,
|
|
64
64
|
eval_id: str | None = None,
|
|
65
65
|
benchmark_id: str | None = None,
|
|
66
66
|
benchmark_revision: str | None = None,
|
|
@@ -79,7 +79,10 @@ class HitchHarborAgent(BaseAgent):
|
|
|
79
79
|
self.candidate_id = candidate_id
|
|
80
80
|
self.hitch_timeout_ms = int(hitch_timeout_ms)
|
|
81
81
|
self.agent_args = list(agent_args or [])
|
|
82
|
-
|
|
82
|
+
if workdir is not None and (not isinstance(workdir, str) or not workdir.strip()):
|
|
83
|
+
raise ValueError("workdir must be a non-empty string when provided")
|
|
84
|
+
self.workdir = workdir.strip() if isinstance(workdir, str) else None
|
|
85
|
+
self._workdir_source: str | None = "agent_config" if self.workdir is not None else None
|
|
83
86
|
self.eval_id = eval_id
|
|
84
87
|
self.benchmark_id = benchmark_id
|
|
85
88
|
self.benchmark_revision = benchmark_revision
|
|
@@ -145,6 +148,7 @@ class HitchHarborAgent(BaseAgent):
|
|
|
145
148
|
payload_dir = self.hitch_runtime_dir / "payload"
|
|
146
149
|
if not payload_dir.is_dir():
|
|
147
150
|
raise RuntimeError(f"Hitch runtime bundle has no payload directory: {self.hitch_runtime_dir}")
|
|
151
|
+
await self._resolve_workdir(environment)
|
|
148
152
|
# Upload the cached bundle's payload (package.json + dist/) as the
|
|
149
153
|
# package root under /opt/hitch; the local cache path is host-side
|
|
150
154
|
# bookkeeping and is not identity (spec §4.2).
|
|
@@ -193,6 +197,111 @@ class HitchHarborAgent(BaseAgent):
|
|
|
193
197
|
if cache_lock is not None:
|
|
194
198
|
await self._release_artifact_cache_lock(cache_lock)
|
|
195
199
|
|
|
200
|
+
async def _resolve_workdir(self, environment: BaseEnvironment) -> str:
|
|
201
|
+
"""Resolve Harbor's effective task directory and prove it is usable.
|
|
202
|
+
|
|
203
|
+
Harbor environments already combine task-level ``[environment].workdir``
|
|
204
|
+
with the container image's ``WORKDIR``. Honor an explicit bridge
|
|
205
|
+
override, then the task configuration, then ask the running container
|
|
206
|
+
for its default cwd instead of assuming a global path such as /app.
|
|
207
|
+
"""
|
|
208
|
+
candidate = self.workdir
|
|
209
|
+
source = self._workdir_source
|
|
210
|
+
task_config = getattr(environment, "task_env_config", None)
|
|
211
|
+
task_workdir = getattr(task_config, "workdir", None)
|
|
212
|
+
discovery: ExecResult | None = None
|
|
213
|
+
if candidate is None and isinstance(task_workdir, str) and task_workdir.strip():
|
|
214
|
+
candidate = task_workdir.strip()
|
|
215
|
+
source = "task_environment"
|
|
216
|
+
if candidate is None:
|
|
217
|
+
discovery = await environment.exec("pwd -P")
|
|
218
|
+
candidate = (discovery.stdout or "").strip()
|
|
219
|
+
source = "container_workdir"
|
|
220
|
+
if discovery.return_code != 0 or not candidate:
|
|
221
|
+
detail = self._exec_diagnostic(discovery)
|
|
222
|
+
await self._raise_workdir_error(
|
|
223
|
+
environment,
|
|
224
|
+
"Could not determine the Harbor task working directory from the container "
|
|
225
|
+
f"(exit={discovery.return_code}): {detail}",
|
|
226
|
+
source=source,
|
|
227
|
+
candidate=candidate or None,
|
|
228
|
+
probe=discovery,
|
|
229
|
+
)
|
|
230
|
+
if (
|
|
231
|
+
candidate is None
|
|
232
|
+
or not PurePosixPath(candidate).is_absolute()
|
|
233
|
+
or "\x00" in candidate
|
|
234
|
+
or "\n" in candidate
|
|
235
|
+
or "\r" in candidate
|
|
236
|
+
):
|
|
237
|
+
await self._raise_workdir_error(
|
|
238
|
+
environment,
|
|
239
|
+
f"Harbor task working directory must be an absolute POSIX path; got {candidate!r} "
|
|
240
|
+
f"from {source or 'unknown'}",
|
|
241
|
+
source=source,
|
|
242
|
+
candidate=candidate,
|
|
243
|
+
probe=discovery,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
exists = await environment.exec(f"test -d {shlex.quote(candidate)}", cwd="/")
|
|
247
|
+
if exists.return_code != 0:
|
|
248
|
+
detail = self._exec_diagnostic(exists)
|
|
249
|
+
await self._raise_workdir_error(
|
|
250
|
+
environment,
|
|
251
|
+
f"Harbor task working directory does not exist or is not a directory: {candidate} "
|
|
252
|
+
f"(source={source}, exit={exists.return_code}): {detail}",
|
|
253
|
+
source=source,
|
|
254
|
+
candidate=candidate,
|
|
255
|
+
probe=exists,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
usable = await environment.exec("pwd -P", cwd=candidate)
|
|
259
|
+
resolved = (usable.stdout or "").strip()
|
|
260
|
+
if usable.return_code != 0 or not resolved or not PurePosixPath(resolved).is_absolute():
|
|
261
|
+
detail = self._exec_diagnostic(usable)
|
|
262
|
+
await self._raise_workdir_error(
|
|
263
|
+
environment,
|
|
264
|
+
f"Harbor task working directory exists but cannot be used to start the Hitch agent: {candidate} "
|
|
265
|
+
f"(source={source}, exit={usable.return_code}): {detail}",
|
|
266
|
+
source=source,
|
|
267
|
+
candidate=candidate,
|
|
268
|
+
probe=usable,
|
|
269
|
+
)
|
|
270
|
+
self.workdir = resolved
|
|
271
|
+
self._workdir_source = source
|
|
272
|
+
return resolved
|
|
273
|
+
|
|
274
|
+
async def _raise_workdir_error(
|
|
275
|
+
self,
|
|
276
|
+
environment: BaseEnvironment,
|
|
277
|
+
message: str,
|
|
278
|
+
*,
|
|
279
|
+
source: str | None,
|
|
280
|
+
candidate: str | None,
|
|
281
|
+
probe: ExecResult | None,
|
|
282
|
+
) -> None:
|
|
283
|
+
evidence: dict[str, Any] = {
|
|
284
|
+
"schema_version": "1",
|
|
285
|
+
"code": "hitch_workdir_invalid",
|
|
286
|
+
"message": self._bounded_tail(message, 2048),
|
|
287
|
+
"recorded_at": datetime.now(timezone.utc).isoformat(),
|
|
288
|
+
"eval_id": self.eval_id,
|
|
289
|
+
"workdir": {
|
|
290
|
+
"source": source,
|
|
291
|
+
"candidate": candidate,
|
|
292
|
+
"return_code": probe.return_code if probe is not None else None,
|
|
293
|
+
"stdout_tail": self._bounded_tail(probe.stdout or "") if probe is not None else "",
|
|
294
|
+
"stderr_tail": self._bounded_tail(probe.stderr or "") if probe is not None else "",
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
await self._write_bridge_error(environment, evidence)
|
|
298
|
+
raise HitchBridgeError("hitch_workdir_invalid", message, evidence)
|
|
299
|
+
|
|
300
|
+
def _require_workdir(self) -> str:
|
|
301
|
+
if self.workdir is None:
|
|
302
|
+
raise RuntimeError("Hitch agent setup() must resolve the Harbor task working directory before run()")
|
|
303
|
+
return self.workdir
|
|
304
|
+
|
|
196
305
|
def _verify_harness_artifact_host(self) -> dict[str, Any]:
|
|
197
306
|
"""Pin host artifact metadata before Harbor copies the directory."""
|
|
198
307
|
transport = self.harness_artifact or {}
|
|
@@ -834,6 +943,7 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
|
|
|
834
943
|
context: AgentContext,
|
|
835
944
|
) -> None:
|
|
836
945
|
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
|
946
|
+
workdir = self._require_workdir()
|
|
837
947
|
assigned_run_id = "run_" + uuid.uuid4().hex
|
|
838
948
|
run_id = assigned_run_id
|
|
839
949
|
trial_id, task_id, attempt = self._trial_identity()
|
|
@@ -909,7 +1019,7 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
|
|
|
909
1019
|
*self._local_source_cli_args(),
|
|
910
1020
|
*self._artifact_cli_args(),
|
|
911
1021
|
"--cwd",
|
|
912
|
-
shlex.quote(
|
|
1022
|
+
shlex.quote(workdir),
|
|
913
1023
|
"--workspace-mode",
|
|
914
1024
|
"shared",
|
|
915
1025
|
"--prompt-file",
|
|
@@ -937,7 +1047,7 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
|
|
|
937
1047
|
+ " 2> >(tee /logs/agent/hitch-stderr.log >&2)"
|
|
938
1048
|
+ " | tee /logs/agent/hitch-events.jsonl"
|
|
939
1049
|
)
|
|
940
|
-
execution = await environment.exec(command, cwd=
|
|
1050
|
+
execution = await environment.exec(command, cwd=workdir)
|
|
941
1051
|
events = self._events(execution.stdout or "")
|
|
942
1052
|
observed_run_id = next((
|
|
943
1053
|
value
|
|
@@ -989,14 +1099,14 @@ mv "$stage_dir" "$target_dir"
|
|
|
989
1099
|
primary_message: str | None = None
|
|
990
1100
|
if execution.return_code != 0:
|
|
991
1101
|
primary_code = "hitch_process_failed"
|
|
992
|
-
diagnostic = (execution
|
|
1102
|
+
diagnostic = self._exec_diagnostic(execution)
|
|
993
1103
|
if hitch_result and isinstance(hitch_result.get("error"), dict):
|
|
994
1104
|
result_message = hitch_result["error"].get("message")
|
|
995
1105
|
if isinstance(result_message, str) and result_message.strip():
|
|
996
1106
|
diagnostic = result_message.strip()
|
|
997
1107
|
primary_message = (
|
|
998
1108
|
f"Hitch agent run failed with code {execution.return_code} "
|
|
999
|
-
f"(run_id={run_id}, trial_id={trial_id}): {self._bounded_tail(diagnostic
|
|
1109
|
+
f"(run_id={run_id}, trial_id={trial_id}): {self._bounded_tail(diagnostic)}"
|
|
1000
1110
|
)
|
|
1001
1111
|
elif result_error_code is not None:
|
|
1002
1112
|
primary_code = result_error_code
|
|
@@ -1022,6 +1132,8 @@ mv "$stage_dir" "$target_dir"
|
|
|
1022
1132
|
"controller_runtime_id": self.controller_runtime_id,
|
|
1023
1133
|
"hitch_run_id": run_id,
|
|
1024
1134
|
"hitch_run_bundle": "hitch-run-bundle",
|
|
1135
|
+
"hitch_workdir": workdir,
|
|
1136
|
+
"hitch_workdir_source": self._workdir_source,
|
|
1025
1137
|
"eval_id": self.eval_id,
|
|
1026
1138
|
"trial_id": trial_id,
|
|
1027
1139
|
"task_id": task_id,
|
|
@@ -1226,7 +1338,8 @@ mv "$stage_dir" "$target_dir"
|
|
|
1226
1338
|
async def _write_bridge_error(environment: BaseEnvironment, evidence: dict[str, Any]) -> None:
|
|
1227
1339
|
payload = json.dumps(evidence, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
|
|
1228
1340
|
await environment.exec(
|
|
1229
|
-
f"umask 077; printf '%s\\n' {shlex.quote(payload)} > {HITCH_BRIDGE_ERROR_LOG}"
|
|
1341
|
+
f"umask 077; printf '%s\\n' {shlex.quote(payload)} > {HITCH_BRIDGE_ERROR_LOG}",
|
|
1342
|
+
cwd="/",
|
|
1230
1343
|
)
|
|
1231
1344
|
|
|
1232
1345
|
def _trial_identity(self) -> tuple[str, str, int]:
|
|
@@ -1276,6 +1389,7 @@ mv "$stage_dir" "$target_dir"
|
|
|
1276
1389
|
return Path(handle.name)
|
|
1277
1390
|
|
|
1278
1391
|
async def _workspace_digest(self, environment: BaseEnvironment) -> str:
|
|
1392
|
+
workdir = self._require_workdir()
|
|
1279
1393
|
script = r"""
|
|
1280
1394
|
const fs = require('node:fs');
|
|
1281
1395
|
const path = require('node:path');
|
|
@@ -1296,8 +1410,8 @@ walk(process.argv[1]);
|
|
|
1296
1410
|
process.stdout.write('sha256:' + hash.digest('hex'));
|
|
1297
1411
|
""".strip()
|
|
1298
1412
|
result = await environment.exec(
|
|
1299
|
-
" ".join([self._node_prefix(), "node", "-e", shlex.quote(script), shlex.quote(
|
|
1300
|
-
cwd=
|
|
1413
|
+
" ".join([self._node_prefix(), "node", "-e", shlex.quote(script), shlex.quote(workdir)]),
|
|
1414
|
+
cwd=workdir,
|
|
1301
1415
|
)
|
|
1302
1416
|
digest = (result.stdout or "").strip()
|
|
1303
1417
|
if result.return_code == 0 and re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
|
|
@@ -1373,6 +1487,13 @@ node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)
|
|
|
1373
1487
|
raise RuntimeError(f"container setup command failed ({result.return_code}): {diagnostic}")
|
|
1374
1488
|
return result
|
|
1375
1489
|
|
|
1490
|
+
@staticmethod
|
|
1491
|
+
def _exec_diagnostic(result: ExecResult) -> str:
|
|
1492
|
+
for value in (result.stderr, result.stdout):
|
|
1493
|
+
if value and value.strip():
|
|
1494
|
+
return value.strip()
|
|
1495
|
+
return "no diagnostic output"
|
|
1496
|
+
|
|
1376
1497
|
|
|
1377
1498
|
def canonical_manifest_json(manifest: dict[str, Any]) -> str:
|
|
1378
1499
|
"""Canonically encode the runtime identity `{ schema_version, node_range,
|