@lucid-agents/taskmarket 1.17.2 → 1.18.0

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,868 @@
1
+ import { createRequire as __tmCreateRequire } from 'node:module'; import { fileURLToPath as __tmFileURLToPath } from 'node:url'; import { dirname as __tmDirname } from 'node:path'; const require = __tmCreateRequire(import.meta.url); const __tmFilename = __tmFileURLToPath(import.meta.url); const __tmDirnameValue = __tmDirname(__tmFilename);
2
+ import {
3
+ SandboxUnavailableError,
4
+ holderAlive,
5
+ isHostConfig,
6
+ isSecretLike,
7
+ pathMatcher,
8
+ unsafeHostPath,
9
+ withFileLock,
10
+ writeAtomic
11
+ } from "./chunk-64HQ7ZF2.js";
12
+ import {
13
+ buyComputeCredit,
14
+ fetchComputePricing,
15
+ listPendingComputePurchases,
16
+ loadComputeCreditConfig,
17
+ onComputeBalanceObserved,
18
+ readComputeCreditStatus
19
+ } from "./chunk-W7CXEYST.js";
20
+ import {
21
+ getTaskmarketHome
22
+ } from "./chunk-75LS6XS6.js";
23
+ import {
24
+ burnRate,
25
+ computeLow
26
+ } from "./chunk-EB6OJBF5.js";
27
+ import {
28
+ AgentLaunchError
29
+ } from "./chunk-YM4S6IE4.js";
30
+
31
+ // src/lib/agent/active-task.ts
32
+ import { createHash } from "crypto";
33
+ import { promises as fs } from "fs";
34
+ import path from "path";
35
+ var ACTIVE_TASK_VERSION = 1;
36
+ function taskStage(record) {
37
+ if (record.state !== "claimed") return record.state;
38
+ if (record.deliverable) return "frozen";
39
+ return record.workspace?.state === "ready" ? "working" : "claimed";
40
+ }
41
+ function occupiesSlot(record) {
42
+ return !!record && record.state !== "submitted" && record.state !== "stopped";
43
+ }
44
+ function fileFor(wallet, chainId, home) {
45
+ const key = createHash("sha256").update(`${wallet.toLowerCase()}:${chainId}`).digest("hex").slice(0, 32);
46
+ return path.join(home, "agent", "active-task", `${key}.json`);
47
+ }
48
+ function parse(raw, wallet, chainId) {
49
+ const record = JSON.parse(raw);
50
+ if (record?.version !== ACTIVE_TASK_VERSION || typeof record.taskId !== "string" || typeof record.state !== "string" || record.chainId !== chainId || typeof record.wallet !== "string" || record.wallet.toLowerCase() !== wallet.toLowerCase()) {
51
+ throw new Error("The active task record is not one this version can read");
52
+ }
53
+ return record;
54
+ }
55
+ function fileActiveTaskStore(wallet, chainId, home = getTaskmarketHome()) {
56
+ const file = fileFor(wallet, chainId, home);
57
+ const load = async () => {
58
+ let raw;
59
+ try {
60
+ raw = await fs.readFile(file, "utf8");
61
+ } catch (error) {
62
+ if (error.code === "ENOENT") return void 0;
63
+ throw error;
64
+ }
65
+ return parse(raw, wallet, chainId);
66
+ };
67
+ return {
68
+ load,
69
+ update: (change) => withFileLock(`${file}.lock`, "The active task record", async () => {
70
+ const { next, value } = await change(await load());
71
+ if (next) await writeAtomic(file, `${JSON.stringify(next, null, 2)}
72
+ `);
73
+ return value;
74
+ })
75
+ };
76
+ }
77
+
78
+ // src/lib/agent/deliverable-store.ts
79
+ import { promises as fs2 } from "fs";
80
+ import path3 from "path";
81
+ import { randomBytes } from "crypto";
82
+ import { keccak256 } from "viem";
83
+
84
+ // src/lib/agent/deliverable-check.ts
85
+ import { createHash as createHash2 } from "crypto";
86
+ import path2 from "path";
87
+ var ARTIFACT_ROLES = ["preview", "source", "final", "attachment"];
88
+ var TEST_OUTPUT_NAME = "taskmarket-test-output.txt";
89
+ var DELIVERABLE_HARD_LIMITS = Object.freeze({
90
+ maxArtifacts: 19,
91
+ maxFileBytes: 16 * 1024 * 1024,
92
+ maxTotalBytes: 64 * 1024 * 1024,
93
+ maxReportBytes: 1024 * 1024,
94
+ maxTestOutputBytes: 1024 * 1024,
95
+ maxAttempts: 10,
96
+ maxValidators: 8,
97
+ maxValidatorSeconds: 1800,
98
+ maxBaselineFiles: 2e3
99
+ });
100
+ var DEFAULT_DELIVERABLE_LIMITS = Object.freeze({
101
+ maxArtifacts: 10,
102
+ maxFileBytes: 8 * 1024 * 1024,
103
+ maxTotalBytes: 32 * 1024 * 1024,
104
+ maxReportBytes: 256 * 1024,
105
+ maxTestOutputBytes: 64 * 1024
106
+ });
107
+ var DeliverableSpecError = class extends Error {
108
+ constructor(message) {
109
+ super(message);
110
+ this.name = "DeliverableSpecError";
111
+ }
112
+ };
113
+ var HEX_COMMIT = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
114
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
115
+ var TASK_ID = /^0x[0-9a-fA-F]{1,64}$/;
116
+ function canonicalJson(value) {
117
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
118
+ if (value && typeof value === "object") {
119
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
120
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
121
+ }
122
+ return JSON.stringify(value);
123
+ }
124
+ function sha256Hex(data) {
125
+ return createHash2("sha256").update(data).digest("hex");
126
+ }
127
+ function specHash(spec) {
128
+ return sha256Hex(canonicalJson(spec));
129
+ }
130
+ function relativePathProblem(rel) {
131
+ if (typeof rel !== "string" || rel.length === 0) return "empty path";
132
+ if (rel.length > 1024) return "path longer than 1024 characters";
133
+ if (rel.includes("\\")) return "backslash in path";
134
+ if (/[\n\r\t]/.test(rel)) return "control character in path";
135
+ const unsafe = unsafeHostPath(rel, "linux") ?? unsafeHostPath(rel);
136
+ if (unsafe) return unsafe;
137
+ return void 0;
138
+ }
139
+ function effectiveLimits(spec) {
140
+ const limits = { ...DEFAULT_DELIVERABLE_LIMITS, ...spec.limits };
141
+ return {
142
+ maxArtifacts: Math.min(limits.maxArtifacts, DELIVERABLE_HARD_LIMITS.maxArtifacts),
143
+ maxFileBytes: Math.min(limits.maxFileBytes, DELIVERABLE_HARD_LIMITS.maxFileBytes),
144
+ maxTotalBytes: Math.min(limits.maxTotalBytes, DELIVERABLE_HARD_LIMITS.maxTotalBytes),
145
+ maxReportBytes: Math.min(limits.maxReportBytes, DELIVERABLE_HARD_LIMITS.maxReportBytes),
146
+ maxTestOutputBytes: Math.min(
147
+ limits.maxTestOutputBytes,
148
+ DELIVERABLE_HARD_LIMITS.maxTestOutputBytes
149
+ )
150
+ };
151
+ }
152
+ var positiveInt = (n, max) => typeof n === "number" && Number.isInteger(n) && n >= 1 && n <= max;
153
+ function assertDeliverableSpec(spec, sandboxRoot) {
154
+ const fail = (message) => {
155
+ throw new DeliverableSpecError(`Invalid deliverable spec: ${message}`);
156
+ };
157
+ if (spec?.version !== 1 || spec.vertical !== "coding") fail("unsupported version or vertical");
158
+ if (!TASK_ID.test(spec.taskId ?? "")) fail("taskId must be 0x-prefixed hex");
159
+ const ws = spec.workspace;
160
+ if (typeof ws !== "string" || !path2.posix.isAbsolute(ws) || path2.posix.normalize(ws) !== ws || !ws.startsWith(`${sandboxRoot}/`) || /[\n\r\0]/.test(ws)) {
161
+ fail(`workspace must be a normalized absolute path inside ${sandboxRoot}`);
162
+ }
163
+ if (!HEX_COMMIT.test(spec.baseline?.commit ?? ""))
164
+ fail("baseline.commit must be a git commit id");
165
+ const files = spec.baseline.files;
166
+ if (!Array.isArray(files) || files.length === 0)
167
+ fail("baseline.files must list the original files");
168
+ if (files.length > DELIVERABLE_HARD_LIMITS.maxBaselineFiles) fail("too many baseline files");
169
+ const seen = /* @__PURE__ */ new Set();
170
+ for (const file of files) {
171
+ const problem = relativePathProblem(file?.path);
172
+ if (problem) fail(`baseline file ${file?.path}: ${problem}`);
173
+ if (!SHA256_HEX.test(file.sha256 ?? "")) fail(`baseline file ${file.path}: sha256 must be hex`);
174
+ if (seen.has(file.path)) fail(`baseline file ${file.path} listed twice`);
175
+ seen.add(file.path);
176
+ }
177
+ for (const [label, entry] of [
178
+ ["patch", spec.patch],
179
+ ["report", spec.report]
180
+ ]) {
181
+ const problem = relativePathProblem(entry?.path);
182
+ if (problem) fail(`${label}.path: ${problem}`);
183
+ if (!ARTIFACT_ROLES.includes(entry.role)) fail(`${label}.role is not an artifact role`);
184
+ }
185
+ if (spec.patch.path === spec.report.path) fail("patch and report must be different files");
186
+ for (const name of [spec.patch.path, spec.report.path]) {
187
+ if (path2.posix.basename(name) === TEST_OUTPUT_NAME) fail(`${TEST_OUTPUT_NAME} is reserved`);
188
+ }
189
+ if (spec.patch.mayChange !== void 0) {
190
+ if (!Array.isArray(spec.patch.mayChange) || spec.patch.mayChange.length === 0) {
191
+ fail("patch.mayChange, when given, must name at least one pattern");
192
+ }
193
+ }
194
+ const validators = spec.validators;
195
+ if (!Array.isArray(validators) || validators.length === 0) fail("at least one validator");
196
+ if (validators.length > DELIVERABLE_HARD_LIMITS.maxValidators) fail("too many validators");
197
+ for (const v of validators) {
198
+ if (typeof v?.name !== "string" || !/^[\w .:/-]{1,64}$/.test(v.name)) {
199
+ fail("validator names are 1-64 plain characters");
200
+ }
201
+ if (typeof v.command !== "string" || v.command.trim() === "" || v.command.length > 4096) {
202
+ fail(`validator ${v.name}: command must be 1-4096 characters`);
203
+ }
204
+ if (v.command.includes("\0")) fail(`validator ${v.name}: NUL in command`);
205
+ if (!positiveInt(v.timeoutSeconds, DELIVERABLE_HARD_LIMITS.maxValidatorSeconds)) {
206
+ fail(`validator ${v.name}: timeoutSeconds must be 1-1800`);
207
+ }
208
+ }
209
+ if (!positiveInt(spec.maxAttempts, DELIVERABLE_HARD_LIMITS.maxAttempts)) {
210
+ fail("maxAttempts must be 1-10");
211
+ }
212
+ if (!Number.isFinite(Date.parse(spec.deadline))) fail("deadline must be an ISO date");
213
+ for (const [key, value] of Object.entries(spec.limits ?? {})) {
214
+ if (!(key in DEFAULT_DELIVERABLE_LIMITS)) fail(`unknown limit ${key}`);
215
+ if (!positiveInt(value, Number.MAX_SAFE_INTEGER))
216
+ fail(`limit ${key} must be a positive integer`);
217
+ }
218
+ }
219
+ function checkManifest(proposal, spec, limits = effectiveLimits(spec)) {
220
+ const problems = [];
221
+ const list = proposal && typeof proposal === "object" && "artifacts" in proposal ? proposal.artifacts : void 0;
222
+ if (!Array.isArray(list) || list.length === 0) {
223
+ return {
224
+ artifacts: [],
225
+ problems: [
226
+ {
227
+ code: "manifest_invalid",
228
+ detail: `the manifest must be { "artifacts": [{ "path": "...", "role": "..." }] } and list at least ${spec.patch.path} and ${spec.report.path}`
229
+ }
230
+ ]
231
+ };
232
+ }
233
+ if (list.length > limits.maxArtifacts) {
234
+ problems.push({
235
+ code: "too_many_artifacts",
236
+ detail: `${list.length} files proposed; at most ${limits.maxArtifacts}`
237
+ });
238
+ return { artifacts: [], problems };
239
+ }
240
+ const artifacts = [];
241
+ const names = /* @__PURE__ */ new Map();
242
+ for (const raw of list) {
243
+ const entry = raw;
244
+ const rel = typeof entry?.path === "string" ? entry.path.replace(/^\.\//, "") : void 0;
245
+ if (rel === void 0) {
246
+ problems.push({ code: "manifest_invalid", detail: "every artifact needs a string path" });
247
+ continue;
248
+ }
249
+ const pathProblem = relativePathProblem(rel);
250
+ if (pathProblem) {
251
+ problems.push({ code: "path_invalid", path: rel, detail: pathProblem });
252
+ continue;
253
+ }
254
+ if (isHostConfig(rel)) {
255
+ problems.push({
256
+ code: "path_host_config",
257
+ path: rel,
258
+ detail: "host configuration is never delivered"
259
+ });
260
+ continue;
261
+ }
262
+ if (isSecretLike(rel)) {
263
+ problems.push({
264
+ code: "path_secret",
265
+ path: rel,
266
+ detail: "secret-like files are never delivered"
267
+ });
268
+ continue;
269
+ }
270
+ const expected = rel === spec.patch.path ? spec.patch.role : rel === spec.report.path ? spec.report.role : void 0;
271
+ const role = entry.role === void 0 ? expected ?? "attachment" : entry.role;
272
+ if (!ARTIFACT_ROLES.includes(role)) {
273
+ problems.push({
274
+ code: "manifest_invalid",
275
+ path: rel,
276
+ detail: `role must be one of ${ARTIFACT_ROLES.join(", ")}`
277
+ });
278
+ continue;
279
+ }
280
+ if (expected && role !== expected) {
281
+ problems.push({
282
+ code: "role_mismatch",
283
+ path: rel,
284
+ detail: `this file is delivered with role ${expected}`
285
+ });
286
+ continue;
287
+ }
288
+ const name = path2.posix.basename(rel);
289
+ if (name.toLowerCase() === TEST_OUTPUT_NAME) {
290
+ problems.push({
291
+ code: "reserved_name",
292
+ path: rel,
293
+ detail: `${TEST_OUTPUT_NAME} is written by the controller from the validators it runs; it cannot be supplied`
294
+ });
295
+ continue;
296
+ }
297
+ const other = names.get(name.toLowerCase());
298
+ if (other !== void 0) {
299
+ problems.push({
300
+ code: "duplicate_name",
301
+ path: rel,
302
+ detail: other === rel ? "listed twice" : `same file name as ${other}`
303
+ });
304
+ continue;
305
+ }
306
+ names.set(name.toLowerCase(), rel);
307
+ artifacts.push({ path: rel, role });
308
+ }
309
+ for (const required of [spec.patch.path, spec.report.path]) {
310
+ if (!artifacts.some((a) => a.path === required) && !problems.some((p) => p.path === required)) {
311
+ problems.push({
312
+ code: "missing_artifact",
313
+ path: required,
314
+ detail: "required by the task and not in the manifest"
315
+ });
316
+ }
317
+ }
318
+ return { artifacts, problems };
319
+ }
320
+ var HEADER_LINE = /^(old mode|new mode|deleted file mode|new file mode|similarity index|dissimilarity index|rename from|rename to|copy from|copy to|index) (.*)$/;
321
+ var HUNK = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
322
+ function stripPrefix(name, prefix) {
323
+ if (name === "/dev/null") return void 0;
324
+ if (name.startsWith('"')) throw new Error(`quoted path ${name} (use plain ASCII file names)`);
325
+ if (name.includes(" ")) throw new Error(`tab in path ${name}`);
326
+ if (!name.startsWith(prefix)) throw new Error(`path ${name} does not start with ${prefix}`);
327
+ return name.slice(prefix.length);
328
+ }
329
+ function parseGitPatch(text) {
330
+ const lines = text.split("\n");
331
+ if (lines[lines.length - 1] === "") lines.pop();
332
+ const files = [];
333
+ let i = 0;
334
+ try {
335
+ if (lines.length === 0) throw new Error("the patch is empty");
336
+ while (i < lines.length) {
337
+ const line = lines[i];
338
+ if (!line.startsWith("diff --git ")) {
339
+ throw new Error(
340
+ `line ${i + 1} is outside any file section (a git patch starts each file with "diff --git"): ${line.slice(0, 80)}`
341
+ );
342
+ }
343
+ const names = line.slice("diff --git ".length);
344
+ if (names.includes('"'))
345
+ throw new Error(`quoted path on line ${i + 1} (use plain ASCII file names)`);
346
+ const file = { modes: [], binary: false };
347
+ const fromHeader = {};
348
+ i++;
349
+ while (i < lines.length) {
350
+ const header = HEADER_LINE.exec(lines[i]);
351
+ if (!header) break;
352
+ const [, key, value] = header;
353
+ if (key === "new file mode" || key === "new mode") file.modes.push(value);
354
+ if (key === "rename from" || key === "copy from") fromHeader.renameFrom = value;
355
+ if (key === "rename to" || key === "copy to") fromHeader.renameTo = value;
356
+ i++;
357
+ }
358
+ let sawPaths = false;
359
+ if (lines[i]?.startsWith("Binary files ")) {
360
+ file.binary = true;
361
+ i++;
362
+ } else if (lines[i] === "GIT binary patch") {
363
+ file.binary = true;
364
+ i++;
365
+ while (i < lines.length && !lines[i].startsWith("diff --git ")) i++;
366
+ } else if (lines[i]?.startsWith("--- ")) {
367
+ file.oldPath = stripPrefix(lines[i].slice(4), "a/");
368
+ if (!lines[i + 1]?.startsWith("+++ "))
369
+ throw new Error(`line ${i + 2} must be the "+++" line`);
370
+ file.newPath = stripPrefix(lines[i + 1].slice(4), "b/");
371
+ sawPaths = true;
372
+ i += 2;
373
+ if (!HUNK.test(lines[i] ?? "")) throw new Error(`line ${i + 1} must start a hunk ("@@")`);
374
+ while (i < lines.length && HUNK.test(lines[i])) {
375
+ const [, , oldCount, , newCount] = HUNK.exec(lines[i]);
376
+ let left = Number(oldCount ?? "1");
377
+ let right = Number(newCount ?? "1");
378
+ i++;
379
+ while (left > 0 || right > 0) {
380
+ const body = lines[i];
381
+ if (body === void 0) throw new Error("the patch ends inside a hunk");
382
+ const mark = body[0];
383
+ if (mark === " " || body === "") {
384
+ left--;
385
+ right--;
386
+ } else if (mark === "-") left--;
387
+ else if (mark === "+") right--;
388
+ else if (mark !== "\\") {
389
+ throw new Error(
390
+ `line ${i + 1} is not a hunk line; the hunk header's counts are wrong`
391
+ );
392
+ }
393
+ if (left < 0 || right < 0)
394
+ throw new Error(`the hunk before line ${i + 2} is longer than its header says`);
395
+ i++;
396
+ }
397
+ if (lines[i]?.startsWith("\\")) i++;
398
+ }
399
+ }
400
+ if (fromHeader.renameFrom !== void 0 || fromHeader.renameTo !== void 0) {
401
+ if (fromHeader.renameFrom === void 0 || fromHeader.renameTo === void 0) {
402
+ throw new Error("a rename or copy needs both its from and to lines");
403
+ }
404
+ if (fromHeader.renameFrom.startsWith('"') || fromHeader.renameTo.startsWith('"')) {
405
+ throw new Error("quoted path in a rename (use plain ASCII file names)");
406
+ }
407
+ file.oldPath = fromHeader.renameFrom;
408
+ file.newPath = fromHeader.renameTo;
409
+ } else if (!sawPaths) {
410
+ const half = (names.length - 5) / 2;
411
+ const name = Number.isInteger(half) ? names.slice(2, 2 + half) : "";
412
+ if (!name || names !== `a/${name} b/${name}`) {
413
+ throw new Error(`cannot read the file names on "${line.slice(0, 120)}"`);
414
+ }
415
+ file.oldPath = name;
416
+ file.newPath = name;
417
+ }
418
+ files.push(file);
419
+ }
420
+ } catch (error) {
421
+ return { valid: false, detail: error.message };
422
+ }
423
+ return { valid: true, files };
424
+ }
425
+ var PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/;
426
+ function holdsPrivateKey(data) {
427
+ return PRIVATE_KEY.test(data.toString("latin1"));
428
+ }
429
+ function checkPatchContent(data, spec) {
430
+ const rel = spec.patch.path;
431
+ const parsed = parseGitPatch(data.toString("utf8"));
432
+ if (!parsed.valid) {
433
+ return [
434
+ {
435
+ code: "patch_format",
436
+ path: rel,
437
+ detail: `${parsed.detail}. Produce it with git diff against the baseline commit ${spec.baseline.commit.slice(0, 12)}, e.g. git diff ${spec.baseline.commit.slice(0, 12)} -- <files> > ${rel}`
438
+ }
439
+ ];
440
+ }
441
+ if (parsed.files.length === 0) {
442
+ return [{ code: "patch_empty", path: rel, detail: "the patch changes nothing" }];
443
+ }
444
+ const problems = [];
445
+ const allowed = spec.patch.mayChange ? pathMatcher(spec.patch.mayChange) : void 0;
446
+ const touched = /* @__PURE__ */ new Set();
447
+ for (const file of parsed.files) {
448
+ for (const mode of file.modes) {
449
+ if (mode === "120000" || mode === "160000") {
450
+ problems.push({
451
+ code: "patch_mode",
452
+ path: rel,
453
+ detail: `the patch creates a ${mode === "120000" ? "symlink" : "submodule"} at ${file.newPath}`
454
+ });
455
+ }
456
+ }
457
+ for (const name of [file.oldPath, file.newPath]) if (name !== void 0) touched.add(name);
458
+ }
459
+ for (const name of [...touched].sort()) {
460
+ const problem = relativePathProblem(name);
461
+ const why = problem ? problem : isHostConfig(name) ? "host configuration" : isSecretLike(name) ? "a secret-like file" : allowed && !allowed(name) ? `outside what the task lets it change (${spec.patch.mayChange.join(", ")})` : void 0;
462
+ if (why)
463
+ problems.push({ code: "patch_path", path: rel, detail: `the patch changes ${name}: ${why}` });
464
+ }
465
+ return problems;
466
+ }
467
+ function checkReportContent(data, spec, limits) {
468
+ const rel = spec.report.path;
469
+ if (data.length > limits.maxReportBytes) {
470
+ return [
471
+ {
472
+ code: "report_too_large",
473
+ path: rel,
474
+ detail: `${data.length} bytes; at most ${limits.maxReportBytes}`
475
+ }
476
+ ];
477
+ }
478
+ let text;
479
+ try {
480
+ text = new TextDecoder("utf-8", { fatal: true }).decode(data);
481
+ } catch {
482
+ return [{ code: "report_not_text", path: rel, detail: "the report must be UTF-8 text" }];
483
+ }
484
+ if (text.trim() === "") return [{ code: "empty", path: rel, detail: "the report is blank" }];
485
+ return [];
486
+ }
487
+
488
+ // src/lib/agent/deliverable-store.ts
489
+ var DELIVERABLE_RECORD_VERSION = 1;
490
+ var MAX_INTERRUPTED_ATTEMPTS = 3;
491
+ var MAX_RECORDED_PROBLEMS = 50;
492
+ var DeliverableError = class extends Error {
493
+ constructor(code, message) {
494
+ super(message);
495
+ this.code = code;
496
+ this.name = "DeliverableError";
497
+ }
498
+ };
499
+ var DeliverableTamperedError = class extends DeliverableError {
500
+ constructor(detail) {
501
+ super(
502
+ "deliverable_tampered",
503
+ `The frozen deliverable was changed after it was frozen: ${detail}`
504
+ );
505
+ this.name = "DeliverableTamperedError";
506
+ }
507
+ };
508
+ var normalTaskId = (taskId) => taskId.toLowerCase();
509
+ function deliverableDir(taskId, home = getTaskmarketHome()) {
510
+ const key = sha256Hex(normalTaskId(taskId)).slice(0, 32);
511
+ return path3.join(home, "agent", "tasks", key);
512
+ }
513
+ var recordFile = (taskId) => path3.join(deliverableDir(taskId), "deliverable.json");
514
+ async function loadDeliverableRecord(taskId) {
515
+ let raw;
516
+ try {
517
+ raw = await fs2.readFile(recordFile(taskId), "utf8");
518
+ } catch (error) {
519
+ if (error.code === "ENOENT") return void 0;
520
+ throw error;
521
+ }
522
+ let parsed;
523
+ try {
524
+ parsed = JSON.parse(raw);
525
+ } catch {
526
+ throw new DeliverableTamperedError(`${recordFile(taskId)} is not readable JSON`);
527
+ }
528
+ if (parsed?.version !== DELIVERABLE_RECORD_VERSION || parsed.taskId !== normalTaskId(taskId) || !Array.isArray(parsed.attempts)) {
529
+ throw new DeliverableTamperedError(`${recordFile(taskId)} is not this task's record`);
530
+ }
531
+ return parsed;
532
+ }
533
+ async function withRecordLock(taskId, work) {
534
+ return withFileLock(
535
+ path3.join(deliverableDir(taskId), "deliverable.lock"),
536
+ "the task deliverable record",
537
+ work
538
+ );
539
+ }
540
+ async function updateDeliverableRecord(taskId, change) {
541
+ return withRecordLock(taskId, async () => {
542
+ const current = await loadDeliverableRecord(taskId);
543
+ const next = change(current);
544
+ if (next) await writeAtomic(recordFile(taskId), `${JSON.stringify(next, null, 2)}
545
+ `);
546
+ return next ?? current;
547
+ });
548
+ }
549
+ var SHA256 = /^[0-9a-f]{64}$/;
550
+ function blobPath(taskId, sha256) {
551
+ if (!SHA256.test(sha256)) throw new Error("Not a SHA-256");
552
+ return path3.join(deliverableDir(taskId), "blobs", sha256);
553
+ }
554
+ async function saveDeliverableBlob(taskId, data) {
555
+ const sha256 = sha256Hex(data);
556
+ const file = blobPath(taskId, sha256);
557
+ const existing = await fs2.readFile(file).catch(() => void 0);
558
+ if (existing && sha256Hex(existing) === sha256) return sha256;
559
+ await writeAtomic(file, data);
560
+ return sha256;
561
+ }
562
+ async function loadDeliverableBlob(taskId, sha256) {
563
+ const data = await fs2.readFile(blobPath(taskId, sha256)).catch(() => void 0);
564
+ return data && sha256Hex(data) === sha256 ? data : void 0;
565
+ }
566
+ function describeArtifact(data, file) {
567
+ return {
568
+ ...file,
569
+ sizeBytes: data.length,
570
+ sha256Hash: sha256Hex(data),
571
+ keccak256Hash: keccak256(new Uint8Array(data))
572
+ };
573
+ }
574
+ function deliverableId(taskId, specHash2, artifacts) {
575
+ const files = [...artifacts].map(({ fileName, role, mimeType, sizeBytes, sha256Hash }) => ({
576
+ fileName,
577
+ role,
578
+ mimeType,
579
+ sizeBytes,
580
+ sha256Hash
581
+ })).sort((a, b) => a.fileName < b.fileName ? -1 : a.fileName > b.fileName ? 1 : 0);
582
+ return sha256Hex(
583
+ canonicalJson({ version: 1, taskId: normalTaskId(taskId), specHash: specHash2, artifacts: files })
584
+ );
585
+ }
586
+ function sealOf(frozen) {
587
+ return sha256Hex(canonicalJson({ ...frozen, seal: void 0 }));
588
+ }
589
+ var counted = (attempt) => attempt.outcome === "passed" || attempt.outcome === "failed";
590
+ function attemptsUsed(record) {
591
+ return record ? record.attempts.filter(counted).length : 0;
592
+ }
593
+ async function beginAttempt(spec, hash, options) {
594
+ const now = options.now.toISOString();
595
+ const pid = options.pid ?? process.pid;
596
+ let start;
597
+ await updateDeliverableRecord(spec.taskId, (current) => {
598
+ const record = current ?? {
599
+ version: DELIVERABLE_RECORD_VERSION,
600
+ taskId: normalTaskId(spec.taskId),
601
+ specHash: hash,
602
+ maxAttempts: spec.maxAttempts,
603
+ deadline: spec.deadline,
604
+ attempts: []
605
+ };
606
+ if (record.specHash !== hash) {
607
+ throw new DeliverableError(
608
+ "spec_changed",
609
+ `Task ${spec.taskId} is already being validated against another deliverable spec (${record.specHash.slice(0, 12)}); a deliverable is checked against the spec approved for it.`
610
+ );
611
+ }
612
+ for (const attempt2 of record.attempts) {
613
+ if (attempt2.outcome === "running" && !holderAlive(attempt2.pid, attempt2.startedAt)) {
614
+ attempt2.outcome = "interrupted";
615
+ attempt2.endedAt = now;
616
+ }
617
+ }
618
+ const running = record.attempts.find((attempt2) => attempt2.outcome === "running");
619
+ if (running) {
620
+ throw new DeliverableError(
621
+ "validation_running",
622
+ `Attempt ${running.n} for task ${spec.taskId} is still running (process ${running.pid}); wait for it to finish.`
623
+ );
624
+ }
625
+ if (record.frozen) {
626
+ start = { kind: "frozen", record };
627
+ return record;
628
+ }
629
+ const exhausted = (reason) => {
630
+ start = { kind: "exhausted", reason, record };
631
+ return record;
632
+ };
633
+ if (attemptsUsed(record) >= record.maxAttempts) return exhausted("attempts");
634
+ const interrupted = record.attempts.filter((a) => a.outcome === "interrupted").length;
635
+ if (interrupted >= MAX_INTERRUPTED_ATTEMPTS) return exhausted("interrupted");
636
+ if (options.now.getTime() > Date.parse(record.deadline)) return exhausted("deadline");
637
+ if (options.budgetRefusal !== void 0) return exhausted("budget");
638
+ const attempt = {
639
+ n: record.attempts.length + 1,
640
+ pid,
641
+ nonce: randomBytes(8).toString("hex"),
642
+ startedAt: now,
643
+ outcome: "running"
644
+ };
645
+ record.attempts.push(attempt);
646
+ start = { kind: "started", attempt, record };
647
+ return record;
648
+ });
649
+ return start;
650
+ }
651
+ function ownAttempt(record, nonce) {
652
+ const attempt = record?.attempts.find((a) => a.nonce === nonce);
653
+ if (!attempt || attempt.outcome !== "running") {
654
+ throw new DeliverableError(
655
+ "attempt_not_running",
656
+ "This validation attempt is no longer running (another process marked it interrupted)."
657
+ );
658
+ }
659
+ return attempt;
660
+ }
661
+ async function finishAttempt(taskId, nonce, result, now) {
662
+ const record = await updateDeliverableRecord(taskId, (current) => {
663
+ const attempt = ownAttempt(current, nonce);
664
+ Object.assign(attempt, {
665
+ ...result,
666
+ problems: result.problems?.slice(0, MAX_RECORDED_PROBLEMS),
667
+ endedAt: now.toISOString()
668
+ });
669
+ return current;
670
+ });
671
+ return record;
672
+ }
673
+ async function freezeDeliverable(taskId, nonce, frozen, attempt, now) {
674
+ let result;
675
+ await updateDeliverableRecord(taskId, (current) => {
676
+ const own = ownAttempt(current, nonce);
677
+ const record = current;
678
+ Object.assign(own, { ...attempt, outcome: "passed", endedAt: now.toISOString() });
679
+ if (record.frozen && record.frozen.deliverableId !== frozen.deliverableId) {
680
+ throw new DeliverableError(
681
+ "already_frozen",
682
+ `Task ${taskId} already has a frozen deliverable (${record.frozen.deliverableId}); it is not replaced.`
683
+ );
684
+ }
685
+ record.frozen ??= frozen;
686
+ result = record.frozen;
687
+ return record;
688
+ });
689
+ return result;
690
+ }
691
+ async function loadFrozenDeliverable(taskId) {
692
+ const record = await loadDeliverableRecord(taskId);
693
+ const frozen = record?.frozen;
694
+ if (!frozen) return void 0;
695
+ if (deliverableId(frozen.taskId, frozen.specHash, frozen.artifacts) !== frozen.deliverableId) {
696
+ throw new DeliverableTamperedError("the record no longer matches its deliverable id");
697
+ }
698
+ if (frozen.specHash !== record.specHash || frozen.taskId !== record.taskId) {
699
+ throw new DeliverableTamperedError("the record names another task or spec");
700
+ }
701
+ if (sealOf(frozen) !== frozen.seal) {
702
+ throw new DeliverableTamperedError("the frozen record was edited");
703
+ }
704
+ const files = [];
705
+ for (const artifact of [...frozen.artifacts, frozen.testOutput]) {
706
+ const data = await fs2.readFile(blobPath(taskId, artifact.sha256Hash)).catch(() => void 0);
707
+ if (!data) throw new DeliverableTamperedError(`${artifact.fileName} is missing`);
708
+ if (data.length !== artifact.sizeBytes || sha256Hex(data) !== artifact.sha256Hash || keccak256(new Uint8Array(data)) !== artifact.keccak256Hash) {
709
+ throw new DeliverableTamperedError(`${artifact.fileName} no longer matches its hashes`);
710
+ }
711
+ files.push({ ...artifact, data });
712
+ }
713
+ return { deliverable: frozen, files };
714
+ }
715
+
716
+ // src/lib/agent/compute.ts
717
+ async function readComputeView(options = {}) {
718
+ const warning = options.warning ?? null;
719
+ const empty = {
720
+ availableCredits: null,
721
+ reservedCredits: null,
722
+ burnCreditsPerHour: null,
723
+ low: warning === "low_credit" || warning === "credit_exhausted",
724
+ warning,
725
+ pendingPurchases: 0,
726
+ autoTopUp: null
727
+ };
728
+ let config;
729
+ try {
730
+ config = await loadComputeCreditConfig();
731
+ } catch (error) {
732
+ return { configured: false, ...empty, error: error.message };
733
+ }
734
+ if (!config) return { configured: false, ...empty };
735
+ try {
736
+ let status = await readComputeCreditStatus();
737
+ if (options.observe && await options.observe({ availableCredits: status.balance.availableCredits })) {
738
+ status = await readComputeCreditStatus();
739
+ }
740
+ const burn = burnRate(status.debits);
741
+ const autoTopUp = config.autoTopUp ? {
742
+ thresholdCredits: config.autoTopUp.thresholdCredits,
743
+ amountUsd: config.autoTopUp.amountUsd,
744
+ maxChunksPerDay: config.autoTopUp.maxChunksPerDay
745
+ } : null;
746
+ return {
747
+ configured: true,
748
+ availableCredits: status.balance.availableCredits,
749
+ reservedCredits: status.balance.reservedCredits,
750
+ burnCreditsPerHour: burn,
751
+ low: computeLow({
752
+ availableCredits: status.balance.availableCredits,
753
+ burnCreditsPerHour: burn,
754
+ warning,
755
+ ...autoTopUp ? { thresholdCredits: autoTopUp.thresholdCredits } : {}
756
+ }),
757
+ warning,
758
+ pendingPurchases: (await listPendingComputePurchases()).length,
759
+ autoTopUp
760
+ };
761
+ } catch (error) {
762
+ return {
763
+ configured: true,
764
+ ...empty,
765
+ error: error.code ?? error.message
766
+ };
767
+ }
768
+ }
769
+ async function computeAllocationId() {
770
+ return (await loadComputeCreditConfig().catch(() => void 0))?.allocationId;
771
+ }
772
+ async function computeCreditOffer() {
773
+ const pricing = await fetchComputePricing();
774
+ return {
775
+ usdPerCredit: pricing.usdPerCredit,
776
+ denominations: pricing.denominations.map((entry) => ({
777
+ amountUsd: entry.amountUsd,
778
+ credits: entry.credits,
779
+ default: entry.default
780
+ }))
781
+ };
782
+ }
783
+ async function buyComputeChunk(amountUsd) {
784
+ const pricing = await fetchComputePricing();
785
+ const size = amountUsd ?? (pricing.denominations.find((entry) => entry.default) ?? pricing.denominations[0])?.amountUsd;
786
+ if (size === void 0) {
787
+ throw new AgentLaunchError("invalid_amount", "The API sells no compute credit sizes");
788
+ }
789
+ const bought = await buyComputeCredit({ amountUsd: size });
790
+ return { purchase: bought.purchase, balance: bought.balance, amountUsd: size };
791
+ }
792
+ function computeCreditObserver(log) {
793
+ return async (observation) => {
794
+ try {
795
+ const { topUp } = await onComputeBalanceObserved(observation);
796
+ if (topUp.action === "bought") log("Compute credit auto-top-up bought one chunk");
797
+ else if (topUp.action === "skipped")
798
+ log(`Compute credit auto-top-up skipped: ${topUp.reason}`);
799
+ return topUp.action === "bought";
800
+ } catch (error) {
801
+ log(
802
+ `Compute credit auto-top-up failed: ${error.code ?? error.message}`
803
+ );
804
+ return false;
805
+ }
806
+ };
807
+ }
808
+ function insufficientCredit(error) {
809
+ return error instanceof SandboxUnavailableError && /\binsufficient_credit\b/.test(error.reason);
810
+ }
811
+ async function openSandbox(createBroker, observe) {
812
+ const first = createBroker();
813
+ try {
814
+ await first.connect("start");
815
+ return first;
816
+ } catch (error) {
817
+ if (!insufficientCredit(error)) throw error;
818
+ if (!await observe({ creditWarning: "credit_exhausted" })) {
819
+ throw new AgentLaunchError("insufficient_credit", error.reason);
820
+ }
821
+ }
822
+ const retry = createBroker();
823
+ try {
824
+ await retry.connect("start");
825
+ return retry;
826
+ } catch (error) {
827
+ if (insufficientCredit(error)) throw new AgentLaunchError("insufficient_credit", error.reason);
828
+ throw error;
829
+ }
830
+ }
831
+
832
+ export {
833
+ taskStage,
834
+ occupiesSlot,
835
+ fileActiveTaskStore,
836
+ TEST_OUTPUT_NAME,
837
+ DELIVERABLE_HARD_LIMITS,
838
+ DeliverableSpecError,
839
+ sha256Hex,
840
+ specHash,
841
+ relativePathProblem,
842
+ effectiveLimits,
843
+ assertDeliverableSpec,
844
+ checkManifest,
845
+ holdsPrivateKey,
846
+ checkPatchContent,
847
+ checkReportContent,
848
+ DeliverableError,
849
+ DeliverableTamperedError,
850
+ deliverableDir,
851
+ loadDeliverableRecord,
852
+ saveDeliverableBlob,
853
+ loadDeliverableBlob,
854
+ describeArtifact,
855
+ deliverableId,
856
+ sealOf,
857
+ attemptsUsed,
858
+ beginAttempt,
859
+ finishAttempt,
860
+ freezeDeliverable,
861
+ loadFrozenDeliverable,
862
+ readComputeView,
863
+ computeAllocationId,
864
+ computeCreditOffer,
865
+ buyComputeChunk,
866
+ computeCreditObserver,
867
+ openSandbox
868
+ };