@zhivex-ai/harness 0.11.0 → 0.11.1

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/index.js CHANGED
@@ -1218,7 +1218,6 @@ var normalizeScope = (scope) => {
1218
1218
  var stableKey = (kind, value) => createHash3("sha256").update(kind).update("\x00").update(value).digest("hex");
1219
1219
  var sessionId = () => `ses_${randomUUID()}`;
1220
1220
  var turnId = () => `turn_${randomUUID()}`;
1221
- var isMissing = (error) => error.code === "ENOENT";
1222
1221
  var ensurePrivateDatabase = async (workspace, stateDirectory) => {
1223
1222
  const requestedStateDirectory = path3.resolve(stateDirectory);
1224
1223
  await validateStateDirectory(workspace, requestedStateDirectory);
@@ -1232,19 +1231,12 @@ var ensurePrivateDatabase = async (workspace, stateDirectory) => {
1232
1231
  const databasePath = path3.join(canonicalStateDirectory2, HARNESS_SESSION_INDEX_FILE);
1233
1232
  let databaseEntry;
1234
1233
  try {
1234
+ const handle = await open(databasePath, "wx", 384);
1235
+ await handle.close();
1235
1236
  databaseEntry = await lstat2(databasePath);
1236
1237
  } catch (error) {
1237
- if (!isMissing(error)) {
1238
+ if (error.code !== "EEXIST")
1238
1239
  throw error;
1239
- }
1240
- try {
1241
- const handle = await open(databasePath, "wx", 384);
1242
- await handle.close();
1243
- } catch (creationError) {
1244
- if (creationError.code !== "EEXIST") {
1245
- throw creationError;
1246
- }
1247
- }
1248
1240
  databaseEntry = await lstat2(databasePath);
1249
1241
  }
1250
1242
  if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
@@ -1764,13 +1756,12 @@ var openSessionStore = openCliSessionStore;
1764
1756
  // src/execution-environment.ts
1765
1757
  import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypto";
1766
1758
  import { spawn as spawn2 } from "node:child_process";
1767
- import { constants as fsConstants } from "node:fs";
1759
+ import { constants as fsConstants2 } from "node:fs";
1768
1760
  import {
1769
1761
  chmod as chmod2,
1770
1762
  copyFile,
1771
1763
  lstat as lstat5,
1772
1764
  mkdir as mkdir3,
1773
- readFile as readFile2,
1774
1765
  readdir as readdir2,
1775
1766
  realpath as realpath3,
1776
1767
  rename as rename2,
@@ -1791,13 +1782,14 @@ var MAX_EDIT_FILE_BYTES = 1024 * 1024;
1791
1782
  var MAX_EDIT_PROPOSAL_BYTES = 4 * 1024 * 1024;
1792
1783
  var SHA256_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
1793
1784
  var fileDigestSchema = z.string().regex(SHA256_DIGEST_PATTERN, "Digest must use the format sha256:<64 lowercase hexadecimal characters>.");
1785
+ var expectedEditDigestSchema = fileDigestSchema.nullable().describe("Required. Use the inspected sha256 digest for an existing file, or explicit JSON null for a create-only target. Never omit this field.");
1794
1786
  var workspaceFilePathSchema = z.string().min(1).max(1024).refine((value) => !value.includes("\x00"), "Path cannot contain a NUL byte.").refine((value) => !path4.isAbsolute(value) && !/^[A-Za-z]:[\\/]/.test(value), "Path must be relative.").refine((value) => {
1795
1787
  const segments = value.split(/[\\/]+/);
1796
1788
  return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
1797
1789
  }, "Path must be normalized and cannot contain empty, dot, or parent segments.");
1798
1790
  var editChangeSchema = z.strictObject({
1799
1791
  path: workspaceFilePathSchema,
1800
- expectedDigest: fileDigestSchema.nullable(),
1792
+ expectedDigest: expectedEditDigestSchema,
1801
1793
  content: z.string()
1802
1794
  }).superRefine((change, context) => {
1803
1795
  if (Buffer.byteLength(change.content, "utf8") > MAX_EDIT_FILE_BYTES) {
@@ -1838,7 +1830,7 @@ var editProposalInputSchema = z.strictObject({
1838
1830
  });
1839
1831
  var editProposalChangeSchema = z.strictObject({
1840
1832
  path: workspaceFilePathSchema,
1841
- expectedDigest: fileDigestSchema.nullable(),
1833
+ expectedDigest: expectedEditDigestSchema,
1842
1834
  contentDigest: fileDigestSchema,
1843
1835
  bytes: z.number().int().min(0).max(MAX_EDIT_FILE_BYTES)
1844
1836
  });
@@ -1915,6 +1907,94 @@ var editContractDocument = (kind, result) => ({
1915
1907
  result
1916
1908
  });
1917
1909
 
1910
+ // src/file-security.ts
1911
+ import { constants as fsConstants } from "node:fs";
1912
+ import { open as open2 } from "node:fs/promises";
1913
+ var sameOpenFile = (before, after) => before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs && before.mode === after.mode && before.nlink === after.nlink;
1914
+
1915
+ class UnsafeFileTypeError extends Error {
1916
+ constructor(label) {
1917
+ super(`${label} must be a regular file and must not be a symbolic link.`);
1918
+ this.name = "UnsafeFileTypeError";
1919
+ }
1920
+ }
1921
+
1922
+ class FileSizeLimitError extends Error {
1923
+ label;
1924
+ maxBytes;
1925
+ constructor(label, maxBytes) {
1926
+ super(`${label} exceeds the ${maxBytes}-byte limit.`);
1927
+ this.label = label;
1928
+ this.maxBytes = maxBytes;
1929
+ this.name = "FileSizeLimitError";
1930
+ }
1931
+ }
1932
+
1933
+ class FileChangedWhileReadingError extends Error {
1934
+ constructor(label) {
1935
+ super(`${label} changed while it was being read.`);
1936
+ this.name = "FileChangedWhileReadingError";
1937
+ }
1938
+ }
1939
+ var invalidFileError = (label) => new UnsafeFileTypeError(label);
1940
+ var openRegularFileNoFollow = async (target, label) => {
1941
+ try {
1942
+ return await open2(target, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
1943
+ } catch (error) {
1944
+ const code = error.code;
1945
+ if (code === "ELOOP" || code === "EMLINK")
1946
+ throw invalidFileError(label);
1947
+ throw error;
1948
+ }
1949
+ };
1950
+ var statRegularFileNoFollow = async (target, options) => {
1951
+ const handle = await openRegularFileNoFollow(target, options.label);
1952
+ try {
1953
+ const stat2 = await handle.stat();
1954
+ if (!stat2.isFile() || options.requireSingleLink && stat2.nlink !== 1) {
1955
+ throw invalidFileError(options.label);
1956
+ }
1957
+ return stat2;
1958
+ } finally {
1959
+ await handle.close();
1960
+ }
1961
+ };
1962
+ var readRegularFileNoFollow = async (target, options) => {
1963
+ const handle = await openRegularFileNoFollow(target, options.label);
1964
+ try {
1965
+ const before = await handle.stat();
1966
+ if (!before.isFile() || options.requireSingleLink && before.nlink !== 1) {
1967
+ throw invalidFileError(options.label);
1968
+ }
1969
+ if (!Number.isSafeInteger(before.size) || before.size < 0) {
1970
+ throw new Error(`${options.label} has an unsupported size.`);
1971
+ }
1972
+ if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
1973
+ throw new Error(`${options.label} has an invalid byte limit.`);
1974
+ }
1975
+ if (before.size > options.maxBytes) {
1976
+ throw new FileSizeLimitError(options.label, options.maxBytes);
1977
+ }
1978
+ const contents = Buffer.alloc(before.size);
1979
+ let offset = 0;
1980
+ while (offset < contents.byteLength) {
1981
+ const { bytesRead } = await handle.read(contents, offset, contents.byteLength - offset, offset);
1982
+ if (bytesRead === 0)
1983
+ break;
1984
+ offset += bytesRead;
1985
+ }
1986
+ const probe = Buffer.allocUnsafe(1);
1987
+ const { bytesRead: extraBytes } = await handle.read(probe, 0, 1, before.size);
1988
+ const after = await handle.stat();
1989
+ if (offset !== before.size || extraBytes !== 0 || !sameOpenFile(before, after) || options.requireSingleLink && after.nlink !== 1) {
1990
+ throw new FileChangedWhileReadingError(options.label);
1991
+ }
1992
+ return { contents, stat: after };
1993
+ } finally {
1994
+ await handle.close();
1995
+ }
1996
+ };
1997
+
1918
1998
  // src/package-manager.ts
1919
1999
  import { lstat as lstat3 } from "node:fs/promises";
1920
2000
  import path5 from "node:path";
@@ -2019,7 +2099,7 @@ var resolvePackageCheckCommand = async (root, manifest, check, expectedScript, a
2019
2099
 
2020
2100
  // src/workspace.ts
2021
2101
  import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
2022
- import { link, lstat as lstat4, mkdir as mkdir2, open as open2, readFile, readdir, realpath as realpath2, rename, stat as stat2, unlink } from "node:fs/promises";
2102
+ import { link, lstat as lstat4, mkdir as mkdir2, open as open3, readdir, realpath as realpath2, rename, stat as stat2, unlink } from "node:fs/promises";
2023
2103
  import path6 from "node:path";
2024
2104
 
2025
2105
  // src/process-runtime.ts
@@ -2100,9 +2180,11 @@ var runPortableProcess = async (command, options = {}) => {
2100
2180
  const argv = [...command];
2101
2181
  const stdout = new BoundedTextCapture(maxOutputCharacters);
2102
2182
  const stderr = new BoundedTextCapture(maxOutputCharacters);
2183
+ const useProcessGroup = process.platform !== "win32";
2103
2184
  const child = spawn(argv[0], argv.slice(1), {
2104
2185
  ...options.cwd ? { cwd: options.cwd } : {},
2105
2186
  env: processEnvironment(options.env),
2187
+ detached: useProcessGroup,
2106
2188
  shell: false,
2107
2189
  stdio: ["ignore", "pipe", "pipe"],
2108
2190
  windowsHide: true
@@ -2112,13 +2194,27 @@ var runPortableProcess = async (command, options = {}) => {
2112
2194
  let terminationTimer;
2113
2195
  child.stdout.on("data", (chunk) => stdout.write(chunk));
2114
2196
  child.stderr?.on("data", (chunk) => stderr.write(chunk));
2197
+ const signalProcessTree = (signal) => {
2198
+ if (useProcessGroup && child.pid !== undefined) {
2199
+ try {
2200
+ process.kill(-child.pid, signal);
2201
+ return;
2202
+ } catch (error) {
2203
+ if (error.code !== "ESRCH") {
2204
+ child.kill(signal);
2205
+ return;
2206
+ }
2207
+ }
2208
+ }
2209
+ if (child.exitCode === null && child.signalCode === null)
2210
+ child.kill(signal);
2211
+ };
2115
2212
  const terminate = () => {
2116
- if (child.exitCode !== null || child.signalCode !== null)
2213
+ if (terminationTimer)
2117
2214
  return;
2118
- child.kill("SIGTERM");
2215
+ signalProcessTree("SIGTERM");
2119
2216
  terminationTimer = setTimeout(() => {
2120
- if (child.exitCode === null && child.signalCode === null)
2121
- child.kill("SIGKILL");
2217
+ signalProcessTree("SIGKILL");
2122
2218
  }, DEFAULT_TERMINATION_GRACE_MS);
2123
2219
  terminationTimer.unref?.();
2124
2220
  };
@@ -2329,6 +2425,9 @@ class Workspace {
2329
2425
  let current = this.root;
2330
2426
  for (let index = 0;index < segments.length; index += 1) {
2331
2427
  current = path6.join(current, segments[index]);
2428
+ const leaf = index === segments.length - 1;
2429
+ if (leaf && options.skipLeafValidation)
2430
+ continue;
2332
2431
  let entry;
2333
2432
  try {
2334
2433
  entry = await lstat4(current);
@@ -2341,7 +2440,6 @@ class Workspace {
2341
2440
  if (entry.isSymbolicLink()) {
2342
2441
  throw new Error(`The path resolves outside the workspace or through a symbolic link: ${relativePath}`);
2343
2442
  }
2344
- const leaf = index === segments.length - 1;
2345
2443
  if (!leaf && !entry.isDirectory())
2346
2444
  throw new Error(`A path ancestor is not a directory: ${relativePath}`);
2347
2445
  if (leaf && options.requireFile && !entry.isFile())
@@ -2351,15 +2449,12 @@ class Workspace {
2351
2449
  }
2352
2450
  async readStableFile(relativePath, allowBinary = true) {
2353
2451
  this.indexMetrics.stableFileReads += 1;
2354
- const safe = await this.safePath(relativePath, { requireFile: true });
2355
- const before = await lstat4(safe.path);
2356
- if (before.size > MAX_FILE_BYTES)
2357
- throw new Error(`The file exceeds the ${MAX_FILE_BYTES}-byte limit.`);
2358
- const contents = await readFile(safe.path);
2359
- const after = await lstat4(safe.path);
2360
- if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
2361
- throw new Error(`The file changed while it was being read: ${relativePath}`);
2362
- }
2452
+ const safe = await this.safePath(relativePath, { skipLeafValidation: true });
2453
+ const file = await readRegularFileNoFollow(safe.path, {
2454
+ label: "The file",
2455
+ maxBytes: MAX_FILE_BYTES
2456
+ });
2457
+ const { contents } = file;
2363
2458
  if (!allowBinary && contents.includes(0))
2364
2459
  throw new Error("The file appears to be binary and cannot be read as text.");
2365
2460
  return {
@@ -2367,7 +2462,7 @@ class Workspace {
2367
2462
  absolutePath: safe.path,
2368
2463
  contents,
2369
2464
  digest: digestBytes(contents),
2370
- mode: before.mode & 511
2465
+ mode: file.stat.mode & 511
2371
2466
  };
2372
2467
  }
2373
2468
  fingerprint(relativePath, kind, entry, digest2) {
@@ -2390,23 +2485,20 @@ class Workspace {
2390
2485
  for (const filename of IGNORE_FILES) {
2391
2486
  try {
2392
2487
  const ignorePath = path6.join(directory, filename);
2393
- const before = await lstat4(ignorePath);
2394
- if (before.isSymbolicLink() || !before.isFile())
2488
+ const inspected = await statRegularFileNoFollow(ignorePath, { label: "The ignore file" });
2489
+ if (inspected.size > MAX_FILE_BYTES) {
2490
+ ignoreFiles.push(this.fingerprint(wirePath(path6.relative(this.root, ignorePath)), "ignore-file", inspected));
2395
2491
  continue;
2396
- if (before.size <= MAX_FILE_BYTES) {
2397
- const contents = await readFile(ignorePath);
2398
- const after = await lstat4(ignorePath);
2399
- if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, this.fingerprint("", "ignore-file", before))) {
2400
- throw new Error(`The ignore file changed while the workspace was being indexed: ${wirePath(path6.relative(this.root, ignorePath))}`);
2401
- }
2402
- const digest2 = digestBytes(contents);
2403
- ignoreFiles.push(this.fingerprint(wirePath(path6.relative(this.root, ignorePath)), "ignore-file", after, digest2));
2404
- rules.push(...parseIgnoreRules(contents.toString("utf8"), base));
2405
- } else {
2406
- ignoreFiles.push(this.fingerprint(wirePath(path6.relative(this.root, ignorePath)), "ignore-file", before));
2407
2492
  }
2493
+ const file = await readRegularFileNoFollow(ignorePath, {
2494
+ label: "The ignore file",
2495
+ maxBytes: MAX_FILE_BYTES
2496
+ });
2497
+ const digest2 = digestBytes(file.contents);
2498
+ ignoreFiles.push(this.fingerprint(wirePath(path6.relative(this.root, ignorePath)), "ignore-file", file.stat, digest2));
2499
+ rules.push(...parseIgnoreRules(file.contents.toString("utf8"), base));
2408
2500
  } catch (error) {
2409
- if (error.code !== "ENOENT")
2501
+ if (error.code !== "ENOENT" && !(error instanceof UnsafeFileTypeError))
2410
2502
  throw error;
2411
2503
  }
2412
2504
  }
@@ -2449,13 +2541,17 @@ class Workspace {
2449
2541
  }
2450
2542
  for (const fingerprint of index.ignoreFiles) {
2451
2543
  const absolute = path6.join(this.root, fingerprint.path);
2452
- const before = await lstat4(absolute);
2453
- if (before.isSymbolicLink() || !before.isFile() || !this.sameFingerprint(before, fingerprint))
2454
- return false;
2455
2544
  if (fingerprint.digest) {
2456
- const contents = await readFile(absolute);
2457
- const after = await lstat4(absolute);
2458
- if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, fingerprint) || digestBytes(contents) !== fingerprint.digest)
2545
+ const file = await readRegularFileNoFollow(absolute, {
2546
+ label: "The ignore file",
2547
+ maxBytes: MAX_FILE_BYTES
2548
+ });
2549
+ if (!this.sameFingerprint(file.stat, fingerprint) || digestBytes(file.contents) !== fingerprint.digest) {
2550
+ return false;
2551
+ }
2552
+ } else {
2553
+ const inspected = await statRegularFileNoFollow(absolute, { label: "The ignore file" });
2554
+ if (!this.sameFingerprint(inspected, fingerprint))
2459
2555
  return false;
2460
2556
  }
2461
2557
  }
@@ -2840,7 +2936,7 @@ class Workspace {
2840
2936
  async stageFile(target, contents, mode) {
2841
2937
  const directory = await this.secureWorkspaceDirectory(path6.dirname(target));
2842
2938
  const temporary = path6.join(directory, `.${path6.basename(target)}.zhivex-${randomUUID2()}.tmp`);
2843
- const handle = await open2(temporary, "wx", mode);
2939
+ const handle = await open3(temporary, "wx", mode);
2844
2940
  try {
2845
2941
  await handle.writeFile(contents);
2846
2942
  await handle.chmod(mode);
@@ -3063,17 +3159,7 @@ class Workspace {
3063
3159
  await rename(temporary, target);
3064
3160
  }
3065
3161
  async readInternalRegularFile(target, label, maxBytes) {
3066
- const before = await lstat4(target);
3067
- if (before.isSymbolicLink() || !before.isFile())
3068
- throw new Error(`${label} must be a regular non-symlink file.`);
3069
- if (before.size > maxBytes)
3070
- throw new Error(`${label} exceeds its size limit.`);
3071
- const contents = await readFile(target);
3072
- const after = await lstat4(target);
3073
- if (after.isSymbolicLink() || !after.isFile() || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
3074
- throw new Error(`${label} changed while it was being read.`);
3075
- }
3076
- return { contents, stat: before };
3162
+ return await readRegularFileNoFollow(target, { label, maxBytes });
3077
3163
  }
3078
3164
  async quarantineFile(input) {
3079
3165
  const parsed = quarantineFileInputSchema.parse(input);
@@ -3086,7 +3172,7 @@ class Workspace {
3086
3172
  if ((await this.readStableFile(parsed.path)).digest !== parsed.expectedDigest)
3087
3173
  throw new Error(`Stale quarantine rejected for ${parsed.path}.`);
3088
3174
  await link(source.absolutePath, dataPath);
3089
- if (digestBytes(await readFile(dataPath)) !== source.digest) {
3175
+ if (digestBytes((await this.readInternalRegularFile(dataPath, "Quarantine payload", MAX_FILE_BYTES)).contents) !== source.digest) {
3090
3176
  await unlink(dataPath).catch(() => {});
3091
3177
  throw new Error(`Quarantine payload changed before commit: ${parsed.path}.`);
3092
3178
  }
@@ -3143,8 +3229,8 @@ class Workspace {
3143
3229
  throw new Error("Quarantine payload changed before restore commit.");
3144
3230
  }
3145
3231
  await link(dataPath, destination.path);
3146
- const linkedDestination = await lstat4(destination.path);
3147
- if (linkedDestination.isSymbolicLink() || !linkedDestination.isFile() || linkedDestination.dev !== dataFile.stat.dev || linkedDestination.ino !== dataFile.stat.ino || digestBytes(await readFile(destination.path)) !== digest2) {
3232
+ const linkedDestination = await this.readInternalRegularFile(destination.path, "Restore destination", MAX_FILE_BYTES);
3233
+ if (linkedDestination.stat.dev !== dataFile.stat.dev || linkedDestination.stat.ino !== dataFile.stat.ino || digestBytes(linkedDestination.contents) !== digest2) {
3148
3234
  await unlink(destination.path).catch(() => {});
3149
3235
  throw new Error("Restore destination did not match the quarantined payload.");
3150
3236
  }
@@ -3217,6 +3303,8 @@ var TERMINAL_STATUSES2 = new Set([
3217
3303
  "cancelled",
3218
3304
  "timed_out"
3219
3305
  ]);
3306
+ var EXECUTION_ARTIFACT_DIRECTORY_PATTERN = /^[a-f0-9]{24}$/;
3307
+ var STAGED_EXECUTION_ARTIFACT_DIRECTORY_PATTERN = /^\.cleanup-([a-f0-9]{24})-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/;
3220
3308
  var BUILT_IN_TOOL_NAMES = new Set([
3221
3309
  "list_files",
3222
3310
  "read_file",
@@ -3388,7 +3476,9 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
3388
3476
  const seal = createHash6("sha256");
3389
3477
  const metadata = createHash6("sha256");
3390
3478
  const visit = async (directory, relativeDirectory = "") => {
3391
- for (const name of (await readdir2(directory)).sort()) {
3479
+ const children = (await readdir2(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name));
3480
+ for (const child of children) {
3481
+ const name = child.name;
3392
3482
  const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
3393
3483
  const absolute = path7.join(directory, name);
3394
3484
  if (!isInside3(stagedRoot, absolute) || relative.length > 1024) {
@@ -3402,37 +3492,53 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
3402
3492
  await rm(absolute, { recursive: true, force: true });
3403
3493
  continue;
3404
3494
  }
3405
- const entry = await lstat5(absolute);
3406
- if (entry.isSymbolicLink()) {
3495
+ if (child.isSymbolicLink()) {
3407
3496
  throw new Error(`OCI workspace export contains a symbolic link: ${relative}.`);
3408
3497
  }
3409
- if (entry.isDirectory()) {
3410
- updateSnapshotMetadata(metadata, "directory", relative, entry);
3411
- seal.update(`directory\x00${relative}\x00${entry.mode & 511}\x00`);
3498
+ if (child.isDirectory()) {
3499
+ const directoryEntry = await lstat5(absolute);
3500
+ if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
3501
+ throw new Error(`OCI workspace export directory changed while it was being validated: ${relative}.`);
3502
+ }
3503
+ updateSnapshotMetadata(metadata, "directory", relative, directoryEntry);
3504
+ seal.update(`directory\x00${relative}\x00${directoryEntry.mode & 511}\x00`);
3412
3505
  await visit(absolute, relative);
3413
3506
  continue;
3414
3507
  }
3415
- if (!entry.isFile() || entry.nlink !== 1) {
3508
+ if (!child.isFile()) {
3416
3509
  throw new Error(`OCI workspace export contains a special or linked file: ${relative}.`);
3417
3510
  }
3418
- const contents = await readFile2(absolute);
3419
- const after = await lstat5(absolute);
3420
- if (after.isSymbolicLink() || !after.isFile() || after.nlink !== 1 || entry.dev !== after.dev || entry.ino !== after.ino || entry.size !== after.size || entry.mtimeMs !== after.mtimeMs || entry.ctimeMs !== after.ctimeMs || (entry.mode & 511) !== (after.mode & 511)) {
3421
- throw new Error(`OCI workspace export changed while it was being validated: ${relative}.`);
3511
+ let file;
3512
+ try {
3513
+ file = await readRegularFileNoFollow(absolute, {
3514
+ label: `OCI workspace export file ${relative}`,
3515
+ maxBytes: maxWorkspaceBytes - totalBytes,
3516
+ requireSingleLink: true
3517
+ });
3518
+ } catch (error) {
3519
+ if (error instanceof FileSizeLimitError) {
3520
+ throw new Error(`OCI workspace export exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
3521
+ }
3522
+ throw error;
3422
3523
  }
3524
+ const contents = file.contents;
3525
+ const entry = file.stat;
3423
3526
  totalBytes += entry.size;
3424
3527
  if (totalBytes > maxWorkspaceBytes) {
3425
3528
  throw new Error(`OCI workspace export exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
3426
3529
  }
3427
3530
  const contentDigest = digest2(contents);
3428
- updateSnapshotMetadata(metadata, "file", relative, after);
3531
+ updateSnapshotMetadata(metadata, "file", relative, entry);
3429
3532
  seal.update(`file\x00${relative}\x00${entry.mode & 511}\x00${entry.size}\x00${contentDigest}\x00`);
3430
3533
  if (entry.size > maxFileWriteBytes) {
3431
3534
  const previous = path7.join(previousRoot, ...relative.split("/"));
3432
3535
  let unchanged = false;
3433
3536
  try {
3434
- const previousEntry = await lstat5(previous);
3435
- unchanged = previousEntry.isFile() && !previousEntry.isSymbolicLink() && previousEntry.size === entry.size && digest2(await readFile2(previous)) === contentDigest;
3537
+ const previousFile = await readRegularFileNoFollow(previous, {
3538
+ label: `Previous OCI workspace file ${relative}`,
3539
+ maxBytes: entry.size
3540
+ });
3541
+ unchanged = previousFile.stat.size === entry.size && digest2(previousFile.contents) === contentDigest;
3436
3542
  } catch (error) {
3437
3543
  if (error.code !== "ENOENT")
3438
3544
  throw error;
@@ -4097,17 +4203,14 @@ var readSnapshotFile = async (workspace, expected, metrics) => {
4097
4203
  if (!isInside3(workspace.root, absolute)) {
4098
4204
  throw new Error(`Snapshot file escaped its workspace: ${expected.path}.`);
4099
4205
  }
4100
- const before = await lstat5(absolute);
4101
- if (before.isSymbolicLink() || !before.isFile()) {
4102
- throw new Error(`Snapshot source is not a regular file: ${expected.path}.`);
4103
- }
4104
- const contents = await readFile2(absolute);
4105
- const after = await lstat5(absolute);
4106
- if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || (before.mode & 511) !== (after.mode & 511)) {
4107
- throw new Error(`Snapshot source changed while reading: ${expected.path}.`);
4108
- }
4206
+ const file = await readRegularFileNoFollow(absolute, {
4207
+ label: `Snapshot source ${expected.path}`,
4208
+ maxBytes: expected.bytes
4209
+ });
4210
+ const { contents } = file;
4211
+ const entry = file.stat;
4109
4212
  const actualDigest = digest2(contents);
4110
- if (actualDigest !== expected.digest || contents.byteLength !== expected.bytes || (before.mode & 511) !== expected.mode) {
4213
+ if (actualDigest !== expected.digest || contents.byteLength !== expected.bytes || (entry.mode & 511) !== expected.mode) {
4111
4214
  throw new Error(`Snapshot source changed after inventory: ${expected.path}.`);
4112
4215
  }
4113
4216
  if (metrics) {
@@ -4118,7 +4221,7 @@ var readSnapshotFile = async (workspace, expected, metrics) => {
4118
4221
  };
4119
4222
  var copySnapshotFile = async (source, target, metrics) => {
4120
4223
  try {
4121
- await copyFile(source, target, fsConstants.COPYFILE_FICLONE_FORCE);
4224
+ await copyFile(source, target, fsConstants2.COPYFILE_FICLONE_FORCE);
4122
4225
  } catch (error) {
4123
4226
  const code = error.code;
4124
4227
  if (!code || !COPY_ON_WRITE_UNSUPPORTED_CODES.has(code))
@@ -4398,10 +4501,11 @@ var createHarnessOciExecutionEnvironment = async (options) => {
4398
4501
  const snapshotRoot = path7.join(directory, "workspace");
4399
4502
  let metadata;
4400
4503
  try {
4401
- const entry = await lstat5(metadataPath);
4402
- if (entry.isSymbolicLink() || !entry.isFile())
4403
- throw new Error("Execution metadata must be a regular file.");
4404
- metadata = JSON.parse(await readFile2(metadataPath, "utf8"));
4504
+ const metadataFile = await readRegularFileNoFollow(metadataPath, {
4505
+ label: "Execution metadata",
4506
+ maxBytes: 1024 * 1024
4507
+ });
4508
+ metadata = JSON.parse(metadataFile.contents.toString("utf8"));
4405
4509
  if (metadata.schemaVersion !== HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION || metadata.runId !== request.runId || metadata.hostWorkspace !== options.workspace.root || metadata.binding.fingerprint !== binding.fingerprint || metadata.binding.workspaceId !== binding.workspaceId) {
4406
4510
  throw new Error(`Execution artifact binding changed for run ${request.runId}.`);
4407
4511
  }
@@ -4617,7 +4721,11 @@ var createHarnessOciExecutionEnvironment = async (options) => {
4617
4721
  async runCheck(check, expectedScript, allowedChecks, context) {
4618
4722
  let manifestDocument;
4619
4723
  try {
4620
- manifestDocument = JSON.parse(await readFile2(path7.join(snapshotRoot, "package.json"), "utf8"));
4724
+ const manifestFile = await readRegularFileNoFollow(path7.join(snapshotRoot, "package.json"), {
4725
+ label: "Snapshot package.json",
4726
+ maxBytes: 1024 * 1024
4727
+ });
4728
+ manifestDocument = JSON.parse(manifestFile.contents.toString("utf8"));
4621
4729
  } catch (error) {
4622
4730
  if (error.code === "ENOENT") {
4623
4731
  throw new Error("The environment snapshot does not contain package.json.");
@@ -4662,30 +4770,56 @@ var cleanupHarnessExecutionArtifacts = async (stateDirectory, before) => {
4662
4770
  throw error;
4663
4771
  }
4664
4772
  for (const entry of entries) {
4665
- if (!entry.isDirectory() || entry.isSymbolicLink() || !/^[a-f0-9]{24}$/.test(entry.name)) {
4773
+ const stagedMatch = STAGED_EXECUTION_ARTIFACT_DIRECTORY_PATTERN.exec(entry.name);
4774
+ const artifactName = EXECUTION_ARTIFACT_DIRECTORY_PATTERN.test(entry.name) ? entry.name : stagedMatch?.[1];
4775
+ if (!entry.isDirectory() || entry.isSymbolicLink() || !artifactName) {
4666
4776
  result.skipped += 1;
4667
4777
  continue;
4668
4778
  }
4669
4779
  result.scanned += 1;
4670
4780
  const directory = path7.join(root, entry.name);
4671
- const resolved = await realpath3(directory);
4672
- if (!isInside3(root, resolved)) {
4673
- result.skipped += 1;
4674
- continue;
4675
- }
4676
4781
  try {
4782
+ const resolved = await realpath3(directory);
4783
+ if (!isInside3(root, resolved))
4784
+ throw new Error("unsafe execution artifact directory");
4785
+ const directoryEntry = await lstat5(directory);
4786
+ if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
4787
+ throw new Error("unsafe execution artifact directory");
4788
+ }
4677
4789
  const metadataPath = path7.join(directory, "environment.json");
4678
- const metadataEntry = await lstat5(metadataPath);
4679
- if (metadataEntry.isSymbolicLink() || !metadataEntry.isFile())
4680
- throw new Error("unsafe metadata");
4681
- const metadata = JSON.parse(await readFile2(metadataPath, "utf8"));
4790
+ const metadataFile = await readRegularFileNoFollow(metadataPath, {
4791
+ label: "Execution cleanup metadata",
4792
+ maxBytes: 1024 * 1024
4793
+ });
4794
+ const metadata = JSON.parse(metadataFile.contents.toString("utf8"));
4682
4795
  const releasedAt = metadata.releasedAt ? Date.parse(metadata.releasedAt) : Number.NaN;
4683
4796
  if (!metadata.status || !TERMINAL_STATUSES2.has(metadata.status) || !Number.isFinite(releasedAt) || releasedAt >= before) {
4684
4797
  result.skipped += 1;
4685
4798
  continue;
4686
4799
  }
4687
- await rm(directory, { recursive: true, force: true });
4688
- result.deleted += 1;
4800
+ const stagedDirectory = path7.join(root, `.cleanup-${artifactName}-${randomUUID3()}`);
4801
+ let staged = false;
4802
+ try {
4803
+ await rename2(directory, stagedDirectory);
4804
+ staged = true;
4805
+ const movedEntry = await lstat5(stagedDirectory);
4806
+ if (movedEntry.isSymbolicLink() || !movedEntry.isDirectory() || movedEntry.dev !== directoryEntry.dev || movedEntry.ino !== directoryEntry.ino) {
4807
+ throw new Error("execution artifact directory changed before cleanup");
4808
+ }
4809
+ const recheckedMetadata = await readRegularFileNoFollow(path7.join(stagedDirectory, "environment.json"), { label: "Execution cleanup metadata", maxBytes: 1024 * 1024 });
4810
+ if (!recheckedMetadata.contents.equals(metadataFile.contents)) {
4811
+ throw new Error("execution artifact metadata changed before cleanup");
4812
+ }
4813
+ await rm(stagedDirectory, { recursive: true, force: true });
4814
+ staged = false;
4815
+ result.deleted += 1;
4816
+ } catch (error) {
4817
+ if (staged)
4818
+ await rename2(stagedDirectory, directory).catch(() => {
4819
+ return;
4820
+ });
4821
+ throw error;
4822
+ }
4689
4823
  } catch {
4690
4824
  result.skipped += 1;
4691
4825
  }
@@ -4750,7 +4884,7 @@ var selectHarnessModel = (candidates, required) => {
4750
4884
  };
4751
4885
  };
4752
4886
  // src/mcp.ts
4753
- import { lstat as lstat6, readFile as readFile3, realpath as realpath4 } from "node:fs/promises";
4887
+ import { lstat as lstat6, readFile, realpath as realpath4 } from "node:fs/promises";
4754
4888
  import path8 from "node:path";
4755
4889
  import {
4756
4890
  createMcpToolSet,
@@ -4906,7 +5040,7 @@ var loadHarnessMcpConfiguration = async (workspace, configPath) => {
4906
5040
  }
4907
5041
  let value;
4908
5042
  try {
4909
- value = JSON.parse(await readFile3(canonicalConfig, "utf8"));
5043
+ value = JSON.parse(await readFile(canonicalConfig, "utf8"));
4910
5044
  } catch (error) {
4911
5045
  throw new Error("MCP configuration is not valid JSON.", { cause: error });
4912
5046
  }
@@ -5840,7 +5974,7 @@ import { serializeJsonValue as serializeJsonValue2 } from "@zhivex-ai/core";
5840
5974
  import { z as z5 } from "zod";
5841
5975
 
5842
5976
  // src/operations.ts
5843
- import { chmod as chmod3, lstat as lstat7, mkdir as mkdir4, open as open3 } from "node:fs/promises";
5977
+ import { chmod as chmod3, lstat as lstat7, mkdir as mkdir4, open as open4 } from "node:fs/promises";
5844
5978
  import path9 from "node:path";
5845
5979
  import {
5846
5980
  cancelAgentRun,
@@ -6094,19 +6228,12 @@ var openHarnessPersistence = async (config, options = {}) => {
6094
6228
  const databasePath = path9.join(config.stateDirectory, HARNESS_SQLITE_FILE);
6095
6229
  let databaseEntry;
6096
6230
  try {
6231
+ const handle = await open4(databasePath, "wx", 384);
6232
+ await handle.close();
6097
6233
  databaseEntry = await lstat7(databasePath);
6098
6234
  } catch (error) {
6099
- if (error.code !== "ENOENT") {
6235
+ if (error.code !== "EEXIST")
6100
6236
  throw error;
6101
- }
6102
- try {
6103
- const handle = await open3(databasePath, "wx", 384);
6104
- await handle.close();
6105
- } catch (creationError) {
6106
- if (creationError.code !== "EEXIST") {
6107
- throw creationError;
6108
- }
6109
- }
6110
6237
  databaseEntry = await lstat7(databasePath);
6111
6238
  }
6112
6239
  if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
@@ -6149,8 +6276,7 @@ var openHarnessPersistence = async (config, options = {}) => {
6149
6276
 
6150
6277
  // src/context-engineering.ts
6151
6278
  import { createHash as createHash9 } from "node:crypto";
6152
- import { constants as fsConstants2 } from "node:fs";
6153
- import { lstat as lstat8, open as open4, readdir as readdir3 } from "node:fs/promises";
6279
+ import { lstat as lstat8, readdir as readdir3 } from "node:fs/promises";
6154
6280
  import path10 from "node:path";
6155
6281
  import { z as z4 } from "zod";
6156
6282
  var HARNESS_CONTEXT_CONFIG_SCHEMA_VERSION = 1;
@@ -6229,7 +6355,6 @@ var validateRelativePath = (value) => {
6229
6355
  }
6230
6356
  return normalized;
6231
6357
  };
6232
- var stableEntry = (before, after) => before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
6233
6358
  var resolveSafeWorkspaceEntry = async (workspace, relativePath, options = {}) => {
6234
6359
  const normalized = validateRelativePath(relativePath);
6235
6360
  const absolutePath = path10.resolve(workspace.root, normalized);
@@ -6241,6 +6366,9 @@ var resolveSafeWorkspaceEntry = async (workspace, relativePath, options = {}) =>
6241
6366
  const segments = normalized.split("/");
6242
6367
  for (let index = 0;index < segments.length; index += 1) {
6243
6368
  current = path10.join(current, segments[index]);
6369
+ const leaf = index === segments.length - 1;
6370
+ if (leaf && options.skipLeafValidation)
6371
+ continue;
6244
6372
  let entry;
6245
6373
  try {
6246
6374
  entry = await lstat8(current);
@@ -6252,7 +6380,6 @@ var resolveSafeWorkspaceEntry = async (workspace, relativePath, options = {}) =>
6252
6380
  if (entry.isSymbolicLink()) {
6253
6381
  throw new Error(`The harness context path cannot contain a symbolic link: ${relativePath}`);
6254
6382
  }
6255
- const leaf = index === segments.length - 1;
6256
6383
  if (!leaf && !entry.isDirectory()) {
6257
6384
  throw new Error(`A harness context path ancestor is not a directory: ${relativePath}`);
6258
6385
  }
@@ -6266,42 +6393,39 @@ var resolveSafeWorkspaceEntry = async (workspace, relativePath, options = {}) =>
6266
6393
  return { path: absolutePath, relativePath: normalized };
6267
6394
  };
6268
6395
  var readStableWorkspaceText = async (workspace, relativePath, maxBytes, allowMissing = false) => {
6269
- const resolved = await resolveSafeWorkspaceEntry(workspace, relativePath, { allowMissing });
6396
+ const resolved = await resolveSafeWorkspaceEntry(workspace, relativePath, {
6397
+ allowMissing,
6398
+ skipLeafValidation: true
6399
+ });
6270
6400
  if (!resolved)
6271
6401
  return;
6272
- const before = await lstat8(resolved.path);
6273
- if (before.size > maxBytes) {
6274
- throw new Error(`Harness context file ${resolved.relativePath} exceeds the ${maxBytes}-byte limit.`);
6402
+ let file;
6403
+ try {
6404
+ file = await readRegularFileNoFollow(resolved.path, {
6405
+ label: `Harness context file ${resolved.relativePath}`,
6406
+ maxBytes
6407
+ });
6408
+ } catch (error) {
6409
+ if (allowMissing && error.code === "ENOENT")
6410
+ return;
6411
+ throw error;
6412
+ }
6413
+ const { contents } = file;
6414
+ if (contents.includes(0)) {
6415
+ throw new Error(`Harness context file must be UTF-8 text: ${resolved.relativePath}`);
6275
6416
  }
6276
- const handle = await open4(resolved.path, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
6417
+ let content;
6277
6418
  try {
6278
- const opened = await handle.stat();
6279
- if (!opened.isFile() || !stableEntry(before, opened)) {
6280
- throw new Error(`Harness context file changed while opening: ${resolved.relativePath}`);
6281
- }
6282
- const contents = await handle.readFile();
6283
- const after = await handle.stat();
6284
- if (!stableEntry(opened, after) || contents.byteLength !== after.size) {
6285
- throw new Error(`Harness context file changed while reading: ${resolved.relativePath}`);
6286
- }
6287
- if (contents.includes(0)) {
6288
- throw new Error(`Harness context file must be UTF-8 text: ${resolved.relativePath}`);
6289
- }
6290
- let content;
6291
- try {
6292
- content = new TextDecoder("utf-8", { fatal: true }).decode(contents);
6293
- } catch {
6294
- throw new Error(`Harness context file must be valid UTF-8: ${resolved.relativePath}`);
6295
- }
6296
- return {
6297
- path: resolved.relativePath,
6298
- digest: digestBytes2(contents),
6299
- bytes: contents.byteLength,
6300
- content
6301
- };
6302
- } finally {
6303
- await handle.close();
6419
+ content = new TextDecoder("utf-8", { fatal: true }).decode(contents);
6420
+ } catch {
6421
+ throw new Error(`Harness context file must be valid UTF-8: ${resolved.relativePath}`);
6304
6422
  }
6423
+ return {
6424
+ path: resolved.relativePath,
6425
+ digest: digestBytes2(contents),
6426
+ bytes: contents.byteLength,
6427
+ content
6428
+ };
6305
6429
  };
6306
6430
  var parseConfiguration = (file) => {
6307
6431
  let document;
@@ -7797,6 +7921,43 @@ var timeToSafeFixDriverResultSchema = z6.strictObject({
7797
7921
  });
7798
7922
  }
7799
7923
  });
7924
+ var isApprovalDeniedFailure = (message) => message.includes("approval") && message.includes("denied") || message.includes("unsupported or attack-bearing");
7925
+ var isAsciiWordCharacter = (code) => code >= 48 && code <= 57 || code >= 97 && code <= 122 || code === 95;
7926
+ var isToolExecutionFailure = (message) => {
7927
+ let sawToolOnLine = false;
7928
+ let previousToken;
7929
+ for (let index = 0;index < message.length; ) {
7930
+ const code = message.charCodeAt(index);
7931
+ if (code === 10 || code === 13) {
7932
+ sawToolOnLine = false;
7933
+ previousToken = undefined;
7934
+ index += 1;
7935
+ continue;
7936
+ }
7937
+ if (!isAsciiWordCharacter(code)) {
7938
+ index += 1;
7939
+ continue;
7940
+ }
7941
+ const start = index;
7942
+ while (index < message.length && isAsciiWordCharacter(message.charCodeAt(index))) {
7943
+ index += 1;
7944
+ }
7945
+ const length = index - start;
7946
+ const isTerminal2 = length === 8 && message.startsWith("terminal", start);
7947
+ const isTool = length === 4 && message.startsWith("tool", start);
7948
+ const isExecution = length === 9 && message.startsWith("execution", start);
7949
+ const isFailed = length === 6 && message.startsWith("failed", start);
7950
+ if (isTool && previousToken === "terminal" || isExecution && previousToken === "tool") {
7951
+ return true;
7952
+ }
7953
+ if (isFailed && sawToolOnLine)
7954
+ return true;
7955
+ if (isTool)
7956
+ sawToolOnLine = true;
7957
+ previousToken = isTerminal2 ? "terminal" : isTool ? "tool" : undefined;
7958
+ }
7959
+ return false;
7960
+ };
7800
7961
  var classifyTimeToSafeFixFailure = (error, options = {}) => {
7801
7962
  const message = error instanceof Error ? error.message : String(error ?? "");
7802
7963
  const normalized = message.toLowerCase();
@@ -7814,11 +7975,11 @@ var classifyTimeToSafeFixFailure = (error, options = {}) => {
7814
7975
  code = "PATCH_DRIFT";
7815
7976
  } else if (/verifier failed|verifier exited|exit code/.test(normalized)) {
7816
7977
  code = "VERIFIER_FAILED";
7817
- } else if (/approval.*denied|denied.*approval|unsupported or attack-bearing/.test(normalized)) {
7978
+ } else if (isApprovalDeniedFailure(normalized)) {
7818
7979
  code = "APPROVAL_DENIED";
7819
7980
  } else if (/import/.test(normalized)) {
7820
7981
  code = "PATCH_IMPORT_FAILED";
7821
- } else if (/tool(?: ["']?.+["']?)? failed|tool execution|terminal tool/.test(normalized)) {
7982
+ } else if (isToolExecutionFailure(normalized)) {
7822
7983
  code = "TOOL_EXECUTION_FAILED";
7823
7984
  } else if (/provider|model|generation|response/.test(normalized)) {
7824
7985
  code = "MODEL_EXECUTION_FAILED";