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.
@@ -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":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,aAAa,GAAG,GAAG,GAAG,IAAI,CAAC;AACjC,MAAM,kBAAkB,GAAG,CAAC,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,CAAU,CAAC;AAuBvG,MAAM,uBAAuB,GAGxB;IACH;QACE,MAAM,EAAE,uBAAuB;QAC/B,QAAQ,EAAE;YACR,4CAA4C;YAC5C,uCAAuC;YACvC,4BAA4B;YAC5B,wCAAwC;YACxC,6BAA6B;SAC9B;KACF;IACD;QACE,MAAM,EAAE,qBAAqB;QAC7B,QAAQ,EAAE;YACR,yBAAyB;YACzB,8EAA8E;YAC9E,uCAAuC;SACxC;KACF;IACD;QACE,MAAM,EAAE,wBAAwB;QAChC,QAAQ,EAAE;YACR,0DAA0D;YAC1D,qCAAqC;YACrC,6DAA6D;YAC7D,gDAAgD;SACjD;KACF;IACD;QACE,MAAM,EAAE,qBAAqB;QAC7B,QAAQ,EAAE;YACR,uEAAuE;YACvE,wDAAwD;SACzD;KACF;IACD;QACE,MAAM,EAAE,8BAA8B;QACtC,QAAQ,EAAE;YACR,uEAAuE;YACvE,2FAA2F;SAC5F;KACF;CACF,CAAC;AAEF,MAAM,uBAAuB,GAAG;IAC9B,sBAAsB;IACtB,2BAA2B;IAC3B,6BAA6B;IAC7B,4BAA4B;IAC5B,0DAA0D;CAClD,CAAC;AAEX;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,mCAAmC,CACvD,cAAsB,EACtB,MAA0B;IAE1B,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;IAChE,MAAM,QAAQ,GAAG,MAAM,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,2BAA2B,CAAC,CAAC,CAAC;IACzG,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,MAAM,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAE/E,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,kBAAkB,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC;QAChD,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;QACzD,IAAI,KAAK,KAAK,IAAI;YAAE,SAAS;QAC7B,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjB,WAAW,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;IACvC,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,uBAAuB,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAEnF,MAAM,OAAO,GAAG,uBAAuB;SACpC,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;SAC5E,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;IAC/B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,OAAO;QACL,cAAc,EAAE,GAAG;QACnB,IAAI,EAAE,iCAAiC;QACvC,OAAO;QACP,YAAY,EAAE,WAAW;KAC1B,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,sBAAsB,CAAC,IAAY;IAChD,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IACvD,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,KAAc,CAAC;IACnB,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7E,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,IAAI,MAAM,CAAC,cAAc,KAAK,GAAG,IAAI,MAAM,CAAC,IAAI,KAAK,iCAAiC;QAAE,OAAO,IAAI,CAAC;IACpG,MAAM,OAAO,GAAG,qBAAqB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtD,MAAM,WAAW,GAAG,WAAW,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IACrD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAClE,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC;QAC7C,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAClC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;gBAAE,OAAO,EAAE,CAAC;YACjF,MAAM,IAAI,GAAG,OAAkC,CAAC;YAChD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;YAC5B,MAAM,WAAW,GAAG,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACxD,MAAM,eAAe,GAAG,WAAW,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACvD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,IAAK,MAAiB,GAAG,CAAC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YACnI,OAAO,CAAC,EAAE,OAAO,EAAE,MAAgB,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,CAAC,CAAC;QAC9F,CAAC,CAAC;QACJ,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,CAAC;IACtC,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC;IACpC,OAAO;QACL,cAAc,EAAE,GAAG;QACnB,IAAI,EAAE,iCAAiC;QACvC,OAAO;QACP,YAAY,EAAE,WAAW;QACzB,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5C,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,IAAK,UAAqB,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,UAAoB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjH,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC,IAAK,SAAoB,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,SAAmB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9G,CAAC;AACJ,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAc;IAC3C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,MAAM,OAAO,GAAG,IAAI,GAAG,CAA+B,uBAAuB,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3G,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAyC,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,KAAqC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvK,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACzB,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;QACvG,CAAC,CAAC,EAAE,CAAC;AACT,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAA8B;IAC3D,OAAO,KAAK,CAAC,eAAe,IAAI,OAAO,KAAK,CAAC,eAAe,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC;QAChH,CAAC,CAAC,KAAK,CAAC,eAA0C;QAClD,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,KAA8B;IAClE,MAAM,OAAO,GAAG,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,OAAO,IAAI,EAAE,CAA4B,CAAC;IAClF,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;IACjC,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IAClF,OAAO,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;AACtH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAOnC;IACC,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,mBAAmB,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAChF,IAAI,KAAK,CAAC,SAAS,KAAK,WAAW;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,GAAG,EAAE,CAAC;IACvG,IAAI,KAAK,CAAC,SAAS,KAAK,WAAW,IAAI,KAAK,CAAC,YAAY,KAAK,OAAO;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,wBAAwB,EAAE,GAAG,GAAG,EAAE,CAAC;IACtJ,IAAI,KAAK,CAAC,gBAAgB,KAAK,OAAO;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,+BAA+B,EAAE,GAAG,GAAG,EAAE,CAAC;IAC9H,IAAI,KAAK,CAAC,cAAc;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,iCAAiC,EAAE,GAAG,GAAG,EAAE,CAAC;IAClH,yEAAyE;IACzE,2EAA2E;IAC3E,gCAAgC;IAChC,IAAI,KAAK,CAAC,KAAK,CAAC,cAAc;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,iCAAiC,EAAE,GAAG,GAAG,EAAE,CAAC;IACxH,MAAM,MAAM,GAAG,qBAAqB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAClD,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,WAAW;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,yBAAyB,EAAE,CAAC;IACxH,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC;AAC7E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qCAAqC,CACzD,YAAoB,EACpB,UAA4C;IAE5C,MAAM,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,EAAE,2BAA2B,CAAC,EAAE;QACtF,GAAG,UAAU;QACb,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACtC,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,IAAY;IACtC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,OAAO,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QACrE,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,IAAY,EAAE,QAAgB;IAC3D,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACpE,MAAM,KAAK,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;QACxC,IAAI,IAAI,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAC1B,IAAI,IAAI,IAAI,QAAQ,EAAE,CAAC;YACrB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAClC,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;YACtC,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACjC,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;QACtC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;QAC3C,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAC3C,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5D,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,uCAAuC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;IAChG,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;AACH,CAAC"}
@@ -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 = "/app",
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
- self.workdir = workdir
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(self.workdir),
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=self.workdir)
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.stderr or "").strip()
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 or 'no diagnostic output')}"
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(self.workdir)]),
1300
- cwd=self.workdir,
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,