@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/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { randomUUID as randomUUID6 } from "node:crypto";
6
6
  import { constants as fsConstants3, realpathSync as realpathSync2 } from "node:fs";
7
- import { access, lstat as lstat9, readFile as readFile4, stat as stat4 } from "node:fs/promises";
7
+ import { access, lstat as lstat9, stat as stat4 } from "node:fs/promises";
8
8
  import path12 from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { z as z6 } from "zod";
@@ -759,13 +759,14 @@ var MAX_EDIT_FILE_BYTES = 1024 * 1024;
759
759
  var MAX_EDIT_PROPOSAL_BYTES = 4 * 1024 * 1024;
760
760
  var SHA256_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
761
761
  var fileDigestSchema = z.string().regex(SHA256_DIGEST_PATTERN, "Digest must use the format sha256:<64 lowercase hexadecimal characters>.");
762
+ 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.");
762
763
  var workspaceFilePathSchema = z.string().min(1).max(1024).refine((value) => !value.includes("\x00"), "Path cannot contain a NUL byte.").refine((value) => !path2.isAbsolute(value) && !/^[A-Za-z]:[\\/]/.test(value), "Path must be relative.").refine((value) => {
763
764
  const segments = value.split(/[\\/]+/);
764
765
  return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
765
766
  }, "Path must be normalized and cannot contain empty, dot, or parent segments.");
766
767
  var editChangeSchema = z.strictObject({
767
768
  path: workspaceFilePathSchema,
768
- expectedDigest: fileDigestSchema.nullable(),
769
+ expectedDigest: expectedEditDigestSchema,
769
770
  content: z.string()
770
771
  }).superRefine((change, context) => {
771
772
  if (Buffer.byteLength(change.content, "utf8") > MAX_EDIT_FILE_BYTES) {
@@ -806,7 +807,7 @@ var editProposalInputSchema = z.strictObject({
806
807
  });
807
808
  var editProposalChangeSchema = z.strictObject({
808
809
  path: workspaceFilePathSchema,
809
- expectedDigest: fileDigestSchema.nullable(),
810
+ expectedDigest: expectedEditDigestSchema,
810
811
  contentDigest: fileDigestSchema,
811
812
  bytes: z.number().int().min(0).max(MAX_EDIT_FILE_BYTES)
812
813
  });
@@ -885,9 +886,97 @@ var editContractDocument = (kind, result) => ({
885
886
 
886
887
  // src/workspace.ts
887
888
  import { createHash as createHash4, randomUUID } from "node:crypto";
888
- import { link, lstat as lstat2, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "node:fs/promises";
889
+ import { link, lstat as lstat2, mkdir, open as open2, readdir, realpath, rename, stat, unlink } from "node:fs/promises";
889
890
  import path4 from "node:path";
890
891
 
892
+ // src/file-security.ts
893
+ import { constants as fsConstants } from "node:fs";
894
+ import { open } from "node:fs/promises";
895
+ 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;
896
+
897
+ class UnsafeFileTypeError extends Error {
898
+ constructor(label) {
899
+ super(`${label} must be a regular file and must not be a symbolic link.`);
900
+ this.name = "UnsafeFileTypeError";
901
+ }
902
+ }
903
+
904
+ class FileSizeLimitError extends Error {
905
+ label;
906
+ maxBytes;
907
+ constructor(label, maxBytes) {
908
+ super(`${label} exceeds the ${maxBytes}-byte limit.`);
909
+ this.label = label;
910
+ this.maxBytes = maxBytes;
911
+ this.name = "FileSizeLimitError";
912
+ }
913
+ }
914
+
915
+ class FileChangedWhileReadingError extends Error {
916
+ constructor(label) {
917
+ super(`${label} changed while it was being read.`);
918
+ this.name = "FileChangedWhileReadingError";
919
+ }
920
+ }
921
+ var invalidFileError = (label) => new UnsafeFileTypeError(label);
922
+ var openRegularFileNoFollow = async (target, label) => {
923
+ try {
924
+ return await open(target, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
925
+ } catch (error) {
926
+ const code = error.code;
927
+ if (code === "ELOOP" || code === "EMLINK")
928
+ throw invalidFileError(label);
929
+ throw error;
930
+ }
931
+ };
932
+ var statRegularFileNoFollow = async (target, options) => {
933
+ const handle = await openRegularFileNoFollow(target, options.label);
934
+ try {
935
+ const stat = await handle.stat();
936
+ if (!stat.isFile() || options.requireSingleLink && stat.nlink !== 1) {
937
+ throw invalidFileError(options.label);
938
+ }
939
+ return stat;
940
+ } finally {
941
+ await handle.close();
942
+ }
943
+ };
944
+ var readRegularFileNoFollow = async (target, options) => {
945
+ const handle = await openRegularFileNoFollow(target, options.label);
946
+ try {
947
+ const before = await handle.stat();
948
+ if (!before.isFile() || options.requireSingleLink && before.nlink !== 1) {
949
+ throw invalidFileError(options.label);
950
+ }
951
+ if (!Number.isSafeInteger(before.size) || before.size < 0) {
952
+ throw new Error(`${options.label} has an unsupported size.`);
953
+ }
954
+ if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
955
+ throw new Error(`${options.label} has an invalid byte limit.`);
956
+ }
957
+ if (before.size > options.maxBytes) {
958
+ throw new FileSizeLimitError(options.label, options.maxBytes);
959
+ }
960
+ const contents = Buffer.alloc(before.size);
961
+ let offset = 0;
962
+ while (offset < contents.byteLength) {
963
+ const { bytesRead } = await handle.read(contents, offset, contents.byteLength - offset, offset);
964
+ if (bytesRead === 0)
965
+ break;
966
+ offset += bytesRead;
967
+ }
968
+ const probe = Buffer.allocUnsafe(1);
969
+ const { bytesRead: extraBytes } = await handle.read(probe, 0, 1, before.size);
970
+ const after = await handle.stat();
971
+ if (offset !== before.size || extraBytes !== 0 || !sameOpenFile(before, after) || options.requireSingleLink && after.nlink !== 1) {
972
+ throw new FileChangedWhileReadingError(options.label);
973
+ }
974
+ return { contents, stat: after };
975
+ } finally {
976
+ await handle.close();
977
+ }
978
+ };
979
+
891
980
  // src/package-manager.ts
892
981
  import { lstat } from "node:fs/promises";
893
982
  import path3 from "node:path";
@@ -1068,9 +1157,11 @@ var runPortableProcess = async (command, options = {}) => {
1068
1157
  const argv = [...command];
1069
1158
  const stdout = new BoundedTextCapture(maxOutputCharacters);
1070
1159
  const stderr = new BoundedTextCapture(maxOutputCharacters);
1160
+ const useProcessGroup = process.platform !== "win32";
1071
1161
  const child = spawn(argv[0], argv.slice(1), {
1072
1162
  ...options.cwd ? { cwd: options.cwd } : {},
1073
1163
  env: processEnvironment(options.env),
1164
+ detached: useProcessGroup,
1074
1165
  shell: false,
1075
1166
  stdio: ["ignore", "pipe", "pipe"],
1076
1167
  windowsHide: true
@@ -1080,13 +1171,27 @@ var runPortableProcess = async (command, options = {}) => {
1080
1171
  let terminationTimer;
1081
1172
  child.stdout.on("data", (chunk) => stdout.write(chunk));
1082
1173
  child.stderr?.on("data", (chunk) => stderr.write(chunk));
1174
+ const signalProcessTree = (signal) => {
1175
+ if (useProcessGroup && child.pid !== undefined) {
1176
+ try {
1177
+ process.kill(-child.pid, signal);
1178
+ return;
1179
+ } catch (error) {
1180
+ if (error.code !== "ESRCH") {
1181
+ child.kill(signal);
1182
+ return;
1183
+ }
1184
+ }
1185
+ }
1186
+ if (child.exitCode === null && child.signalCode === null)
1187
+ child.kill(signal);
1188
+ };
1083
1189
  const terminate = () => {
1084
- if (child.exitCode !== null || child.signalCode !== null)
1190
+ if (terminationTimer)
1085
1191
  return;
1086
- child.kill("SIGTERM");
1192
+ signalProcessTree("SIGTERM");
1087
1193
  terminationTimer = setTimeout(() => {
1088
- if (child.exitCode === null && child.signalCode === null)
1089
- child.kill("SIGKILL");
1194
+ signalProcessTree("SIGKILL");
1090
1195
  }, DEFAULT_TERMINATION_GRACE_MS);
1091
1196
  terminationTimer.unref?.();
1092
1197
  };
@@ -1297,6 +1402,9 @@ class Workspace {
1297
1402
  let current = this.root;
1298
1403
  for (let index = 0;index < segments.length; index += 1) {
1299
1404
  current = path4.join(current, segments[index]);
1405
+ const leaf = index === segments.length - 1;
1406
+ if (leaf && options.skipLeafValidation)
1407
+ continue;
1300
1408
  let entry;
1301
1409
  try {
1302
1410
  entry = await lstat2(current);
@@ -1309,7 +1417,6 @@ class Workspace {
1309
1417
  if (entry.isSymbolicLink()) {
1310
1418
  throw new Error(`The path resolves outside the workspace or through a symbolic link: ${relativePath}`);
1311
1419
  }
1312
- const leaf = index === segments.length - 1;
1313
1420
  if (!leaf && !entry.isDirectory())
1314
1421
  throw new Error(`A path ancestor is not a directory: ${relativePath}`);
1315
1422
  if (leaf && options.requireFile && !entry.isFile())
@@ -1319,15 +1426,12 @@ class Workspace {
1319
1426
  }
1320
1427
  async readStableFile(relativePath, allowBinary = true) {
1321
1428
  this.indexMetrics.stableFileReads += 1;
1322
- const safe = await this.safePath(relativePath, { requireFile: true });
1323
- const before = await lstat2(safe.path);
1324
- if (before.size > MAX_FILE_BYTES)
1325
- throw new Error(`The file exceeds the ${MAX_FILE_BYTES}-byte limit.`);
1326
- const contents = await readFile(safe.path);
1327
- const after = await lstat2(safe.path);
1328
- if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
1329
- throw new Error(`The file changed while it was being read: ${relativePath}`);
1330
- }
1429
+ const safe = await this.safePath(relativePath, { skipLeafValidation: true });
1430
+ const file = await readRegularFileNoFollow(safe.path, {
1431
+ label: "The file",
1432
+ maxBytes: MAX_FILE_BYTES
1433
+ });
1434
+ const { contents } = file;
1331
1435
  if (!allowBinary && contents.includes(0))
1332
1436
  throw new Error("The file appears to be binary and cannot be read as text.");
1333
1437
  return {
@@ -1335,7 +1439,7 @@ class Workspace {
1335
1439
  absolutePath: safe.path,
1336
1440
  contents,
1337
1441
  digest: digestBytes(contents),
1338
- mode: before.mode & 511
1442
+ mode: file.stat.mode & 511
1339
1443
  };
1340
1444
  }
1341
1445
  fingerprint(relativePath, kind, entry, digest2) {
@@ -1358,23 +1462,20 @@ class Workspace {
1358
1462
  for (const filename of IGNORE_FILES) {
1359
1463
  try {
1360
1464
  const ignorePath = path4.join(directory, filename);
1361
- const before = await lstat2(ignorePath);
1362
- if (before.isSymbolicLink() || !before.isFile())
1465
+ const inspected = await statRegularFileNoFollow(ignorePath, { label: "The ignore file" });
1466
+ if (inspected.size > MAX_FILE_BYTES) {
1467
+ ignoreFiles.push(this.fingerprint(wirePath(path4.relative(this.root, ignorePath)), "ignore-file", inspected));
1363
1468
  continue;
1364
- if (before.size <= MAX_FILE_BYTES) {
1365
- const contents = await readFile(ignorePath);
1366
- const after = await lstat2(ignorePath);
1367
- if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, this.fingerprint("", "ignore-file", before))) {
1368
- throw new Error(`The ignore file changed while the workspace was being indexed: ${wirePath(path4.relative(this.root, ignorePath))}`);
1369
- }
1370
- const digest2 = digestBytes(contents);
1371
- ignoreFiles.push(this.fingerprint(wirePath(path4.relative(this.root, ignorePath)), "ignore-file", after, digest2));
1372
- rules.push(...parseIgnoreRules(contents.toString("utf8"), base));
1373
- } else {
1374
- ignoreFiles.push(this.fingerprint(wirePath(path4.relative(this.root, ignorePath)), "ignore-file", before));
1375
1469
  }
1470
+ const file = await readRegularFileNoFollow(ignorePath, {
1471
+ label: "The ignore file",
1472
+ maxBytes: MAX_FILE_BYTES
1473
+ });
1474
+ const digest2 = digestBytes(file.contents);
1475
+ ignoreFiles.push(this.fingerprint(wirePath(path4.relative(this.root, ignorePath)), "ignore-file", file.stat, digest2));
1476
+ rules.push(...parseIgnoreRules(file.contents.toString("utf8"), base));
1376
1477
  } catch (error) {
1377
- if (error.code !== "ENOENT")
1478
+ if (error.code !== "ENOENT" && !(error instanceof UnsafeFileTypeError))
1378
1479
  throw error;
1379
1480
  }
1380
1481
  }
@@ -1417,13 +1518,17 @@ class Workspace {
1417
1518
  }
1418
1519
  for (const fingerprint of index.ignoreFiles) {
1419
1520
  const absolute = path4.join(this.root, fingerprint.path);
1420
- const before = await lstat2(absolute);
1421
- if (before.isSymbolicLink() || !before.isFile() || !this.sameFingerprint(before, fingerprint))
1422
- return false;
1423
1521
  if (fingerprint.digest) {
1424
- const contents = await readFile(absolute);
1425
- const after = await lstat2(absolute);
1426
- if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, fingerprint) || digestBytes(contents) !== fingerprint.digest)
1522
+ const file = await readRegularFileNoFollow(absolute, {
1523
+ label: "The ignore file",
1524
+ maxBytes: MAX_FILE_BYTES
1525
+ });
1526
+ if (!this.sameFingerprint(file.stat, fingerprint) || digestBytes(file.contents) !== fingerprint.digest) {
1527
+ return false;
1528
+ }
1529
+ } else {
1530
+ const inspected = await statRegularFileNoFollow(absolute, { label: "The ignore file" });
1531
+ if (!this.sameFingerprint(inspected, fingerprint))
1427
1532
  return false;
1428
1533
  }
1429
1534
  }
@@ -1808,7 +1913,7 @@ class Workspace {
1808
1913
  async stageFile(target, contents, mode) {
1809
1914
  const directory = await this.secureWorkspaceDirectory(path4.dirname(target));
1810
1915
  const temporary = path4.join(directory, `.${path4.basename(target)}.zhivex-${randomUUID()}.tmp`);
1811
- const handle = await open(temporary, "wx", mode);
1916
+ const handle = await open2(temporary, "wx", mode);
1812
1917
  try {
1813
1918
  await handle.writeFile(contents);
1814
1919
  await handle.chmod(mode);
@@ -2031,17 +2136,7 @@ class Workspace {
2031
2136
  await rename(temporary, target);
2032
2137
  }
2033
2138
  async readInternalRegularFile(target, label, maxBytes) {
2034
- const before = await lstat2(target);
2035
- if (before.isSymbolicLink() || !before.isFile())
2036
- throw new Error(`${label} must be a regular non-symlink file.`);
2037
- if (before.size > maxBytes)
2038
- throw new Error(`${label} exceeds its size limit.`);
2039
- const contents = await readFile(target);
2040
- const after = await lstat2(target);
2041
- if (after.isSymbolicLink() || !after.isFile() || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
2042
- throw new Error(`${label} changed while it was being read.`);
2043
- }
2044
- return { contents, stat: before };
2139
+ return await readRegularFileNoFollow(target, { label, maxBytes });
2045
2140
  }
2046
2141
  async quarantineFile(input) {
2047
2142
  const parsed = quarantineFileInputSchema.parse(input);
@@ -2054,7 +2149,7 @@ class Workspace {
2054
2149
  if ((await this.readStableFile(parsed.path)).digest !== parsed.expectedDigest)
2055
2150
  throw new Error(`Stale quarantine rejected for ${parsed.path}.`);
2056
2151
  await link(source.absolutePath, dataPath);
2057
- if (digestBytes(await readFile(dataPath)) !== source.digest) {
2152
+ if (digestBytes((await this.readInternalRegularFile(dataPath, "Quarantine payload", MAX_FILE_BYTES)).contents) !== source.digest) {
2058
2153
  await unlink(dataPath).catch(() => {});
2059
2154
  throw new Error(`Quarantine payload changed before commit: ${parsed.path}.`);
2060
2155
  }
@@ -2111,8 +2206,8 @@ class Workspace {
2111
2206
  throw new Error("Quarantine payload changed before restore commit.");
2112
2207
  }
2113
2208
  await link(dataPath, destination.path);
2114
- const linkedDestination = await lstat2(destination.path);
2115
- if (linkedDestination.isSymbolicLink() || !linkedDestination.isFile() || linkedDestination.dev !== dataFile.stat.dev || linkedDestination.ino !== dataFile.stat.ino || digestBytes(await readFile(destination.path)) !== digest2) {
2209
+ const linkedDestination = await this.readInternalRegularFile(destination.path, "Restore destination", MAX_FILE_BYTES);
2210
+ if (linkedDestination.stat.dev !== dataFile.stat.dev || linkedDestination.stat.ino !== dataFile.stat.ino || digestBytes(linkedDestination.contents) !== digest2) {
2116
2211
  await unlink(destination.path).catch(() => {});
2117
2212
  throw new Error("Restore destination did not match the quarantined payload.");
2118
2213
  }
@@ -2175,7 +2270,7 @@ class Workspace {
2175
2270
  }
2176
2271
 
2177
2272
  // src/operations.ts
2178
- import { chmod, lstat as lstat4, mkdir as mkdir2, open as open2 } from "node:fs/promises";
2273
+ import { chmod, lstat as lstat4, mkdir as mkdir2, open as open3 } from "node:fs/promises";
2179
2274
  import path6 from "node:path";
2180
2275
  import {
2181
2276
  cancelAgentRun,
@@ -2552,19 +2647,12 @@ var openHarnessPersistence = async (config, options = {}) => {
2552
2647
  const databasePath = path6.join(config.stateDirectory, HARNESS_SQLITE_FILE);
2553
2648
  let databaseEntry;
2554
2649
  try {
2650
+ const handle = await open3(databasePath, "wx", 384);
2651
+ await handle.close();
2555
2652
  databaseEntry = await lstat4(databasePath);
2556
2653
  } catch (error) {
2557
- if (error.code !== "ENOENT") {
2654
+ if (error.code !== "EEXIST")
2558
2655
  throw error;
2559
- }
2560
- try {
2561
- const handle = await open2(databasePath, "wx", 384);
2562
- await handle.close();
2563
- } catch (creationError) {
2564
- if (creationError.code !== "EEXIST") {
2565
- throw creationError;
2566
- }
2567
- }
2568
2656
  databaseEntry = await lstat4(databasePath);
2569
2657
  }
2570
2658
  if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
@@ -2606,7 +2694,7 @@ var openHarnessPersistence = async (config, options = {}) => {
2606
2694
  };
2607
2695
 
2608
2696
  // src/mcp.ts
2609
- import { lstat as lstat5, readFile as readFile2, realpath as realpath2 } from "node:fs/promises";
2697
+ import { lstat as lstat5, readFile, realpath as realpath2 } from "node:fs/promises";
2610
2698
  import path7 from "node:path";
2611
2699
  import {
2612
2700
  createMcpToolSet,
@@ -2762,7 +2850,7 @@ var loadHarnessMcpConfiguration = async (workspace, configPath) => {
2762
2850
  }
2763
2851
  let value;
2764
2852
  try {
2765
- value = JSON.parse(await readFile2(canonicalConfig, "utf8"));
2853
+ value = JSON.parse(await readFile(canonicalConfig, "utf8"));
2766
2854
  } catch (error) {
2767
2855
  throw new Error("MCP configuration is not valid JSON.", { cause: error });
2768
2856
  }
@@ -3176,13 +3264,12 @@ var runHarnessReviewGroup = async (runtime, input, profiles = ["explorer", "revi
3176
3264
  // src/execution-environment.ts
3177
3265
  import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypto";
3178
3266
  import { spawn as spawn2 } from "node:child_process";
3179
- import { constants as fsConstants } from "node:fs";
3267
+ import { constants as fsConstants2 } from "node:fs";
3180
3268
  import {
3181
3269
  chmod as chmod2,
3182
3270
  copyFile,
3183
3271
  lstat as lstat6,
3184
3272
  mkdir as mkdir3,
3185
- readFile as readFile3,
3186
3273
  readdir as readdir2,
3187
3274
  realpath as realpath3,
3188
3275
  rename as rename2,
@@ -3201,6 +3288,8 @@ var TERMINAL_STATUSES = new Set([
3201
3288
  "cancelled",
3202
3289
  "timed_out"
3203
3290
  ]);
3291
+ var EXECUTION_ARTIFACT_DIRECTORY_PATTERN = /^[a-f0-9]{24}$/;
3292
+ var STAGED_EXECUTION_ARTIFACT_DIRECTORY_PATTERN = /^\.cleanup-([a-f0-9]{24})-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/;
3204
3293
  var BUILT_IN_TOOL_NAMES = new Set([
3205
3294
  "list_files",
3206
3295
  "read_file",
@@ -3372,7 +3461,9 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
3372
3461
  const seal = createHash6("sha256");
3373
3462
  const metadata = createHash6("sha256");
3374
3463
  const visit = async (directory, relativeDirectory = "") => {
3375
- for (const name of (await readdir2(directory)).sort()) {
3464
+ const children = (await readdir2(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name));
3465
+ for (const child of children) {
3466
+ const name = child.name;
3376
3467
  const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
3377
3468
  const absolute = path8.join(directory, name);
3378
3469
  if (!isInside3(stagedRoot, absolute) || relative.length > 1024) {
@@ -3386,37 +3477,53 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
3386
3477
  await rm(absolute, { recursive: true, force: true });
3387
3478
  continue;
3388
3479
  }
3389
- const entry = await lstat6(absolute);
3390
- if (entry.isSymbolicLink()) {
3480
+ if (child.isSymbolicLink()) {
3391
3481
  throw new Error(`OCI workspace export contains a symbolic link: ${relative}.`);
3392
3482
  }
3393
- if (entry.isDirectory()) {
3394
- updateSnapshotMetadata(metadata, "directory", relative, entry);
3395
- seal.update(`directory\x00${relative}\x00${entry.mode & 511}\x00`);
3483
+ if (child.isDirectory()) {
3484
+ const directoryEntry = await lstat6(absolute);
3485
+ if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
3486
+ throw new Error(`OCI workspace export directory changed while it was being validated: ${relative}.`);
3487
+ }
3488
+ updateSnapshotMetadata(metadata, "directory", relative, directoryEntry);
3489
+ seal.update(`directory\x00${relative}\x00${directoryEntry.mode & 511}\x00`);
3396
3490
  await visit(absolute, relative);
3397
3491
  continue;
3398
3492
  }
3399
- if (!entry.isFile() || entry.nlink !== 1) {
3493
+ if (!child.isFile()) {
3400
3494
  throw new Error(`OCI workspace export contains a special or linked file: ${relative}.`);
3401
3495
  }
3402
- const contents = await readFile3(absolute);
3403
- const after = await lstat6(absolute);
3404
- 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)) {
3405
- throw new Error(`OCI workspace export changed while it was being validated: ${relative}.`);
3496
+ let file;
3497
+ try {
3498
+ file = await readRegularFileNoFollow(absolute, {
3499
+ label: `OCI workspace export file ${relative}`,
3500
+ maxBytes: maxWorkspaceBytes - totalBytes,
3501
+ requireSingleLink: true
3502
+ });
3503
+ } catch (error) {
3504
+ if (error instanceof FileSizeLimitError) {
3505
+ throw new Error(`OCI workspace export exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
3506
+ }
3507
+ throw error;
3406
3508
  }
3509
+ const contents = file.contents;
3510
+ const entry = file.stat;
3407
3511
  totalBytes += entry.size;
3408
3512
  if (totalBytes > maxWorkspaceBytes) {
3409
3513
  throw new Error(`OCI workspace export exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
3410
3514
  }
3411
3515
  const contentDigest = digest2(contents);
3412
- updateSnapshotMetadata(metadata, "file", relative, after);
3516
+ updateSnapshotMetadata(metadata, "file", relative, entry);
3413
3517
  seal.update(`file\x00${relative}\x00${entry.mode & 511}\x00${entry.size}\x00${contentDigest}\x00`);
3414
3518
  if (entry.size > maxFileWriteBytes) {
3415
3519
  const previous = path8.join(previousRoot, ...relative.split("/"));
3416
3520
  let unchanged = false;
3417
3521
  try {
3418
- const previousEntry = await lstat6(previous);
3419
- unchanged = previousEntry.isFile() && !previousEntry.isSymbolicLink() && previousEntry.size === entry.size && digest2(await readFile3(previous)) === contentDigest;
3522
+ const previousFile = await readRegularFileNoFollow(previous, {
3523
+ label: `Previous OCI workspace file ${relative}`,
3524
+ maxBytes: entry.size
3525
+ });
3526
+ unchanged = previousFile.stat.size === entry.size && digest2(previousFile.contents) === contentDigest;
3420
3527
  } catch (error) {
3421
3528
  if (error.code !== "ENOENT")
3422
3529
  throw error;
@@ -4081,17 +4188,14 @@ var readSnapshotFile = async (workspace, expected, metrics) => {
4081
4188
  if (!isInside3(workspace.root, absolute)) {
4082
4189
  throw new Error(`Snapshot file escaped its workspace: ${expected.path}.`);
4083
4190
  }
4084
- const before = await lstat6(absolute);
4085
- if (before.isSymbolicLink() || !before.isFile()) {
4086
- throw new Error(`Snapshot source is not a regular file: ${expected.path}.`);
4087
- }
4088
- const contents = await readFile3(absolute);
4089
- const after = await lstat6(absolute);
4090
- if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || (before.mode & 511) !== (after.mode & 511)) {
4091
- throw new Error(`Snapshot source changed while reading: ${expected.path}.`);
4092
- }
4191
+ const file = await readRegularFileNoFollow(absolute, {
4192
+ label: `Snapshot source ${expected.path}`,
4193
+ maxBytes: expected.bytes
4194
+ });
4195
+ const { contents } = file;
4196
+ const entry = file.stat;
4093
4197
  const actualDigest = digest2(contents);
4094
- if (actualDigest !== expected.digest || contents.byteLength !== expected.bytes || (before.mode & 511) !== expected.mode) {
4198
+ if (actualDigest !== expected.digest || contents.byteLength !== expected.bytes || (entry.mode & 511) !== expected.mode) {
4095
4199
  throw new Error(`Snapshot source changed after inventory: ${expected.path}.`);
4096
4200
  }
4097
4201
  if (metrics) {
@@ -4102,7 +4206,7 @@ var readSnapshotFile = async (workspace, expected, metrics) => {
4102
4206
  };
4103
4207
  var copySnapshotFile = async (source, target, metrics) => {
4104
4208
  try {
4105
- await copyFile(source, target, fsConstants.COPYFILE_FICLONE_FORCE);
4209
+ await copyFile(source, target, fsConstants2.COPYFILE_FICLONE_FORCE);
4106
4210
  } catch (error) {
4107
4211
  const code = error.code;
4108
4212
  if (!code || !COPY_ON_WRITE_UNSUPPORTED_CODES.has(code))
@@ -4382,10 +4486,11 @@ var createHarnessOciExecutionEnvironment = async (options) => {
4382
4486
  const snapshotRoot = path8.join(directory, "workspace");
4383
4487
  let metadata;
4384
4488
  try {
4385
- const entry = await lstat6(metadataPath);
4386
- if (entry.isSymbolicLink() || !entry.isFile())
4387
- throw new Error("Execution metadata must be a regular file.");
4388
- metadata = JSON.parse(await readFile3(metadataPath, "utf8"));
4489
+ const metadataFile = await readRegularFileNoFollow(metadataPath, {
4490
+ label: "Execution metadata",
4491
+ maxBytes: 1024 * 1024
4492
+ });
4493
+ metadata = JSON.parse(metadataFile.contents.toString("utf8"));
4389
4494
  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) {
4390
4495
  throw new Error(`Execution artifact binding changed for run ${request.runId}.`);
4391
4496
  }
@@ -4601,7 +4706,11 @@ var createHarnessOciExecutionEnvironment = async (options) => {
4601
4706
  async runCheck(check, expectedScript, allowedChecks, context) {
4602
4707
  let manifestDocument;
4603
4708
  try {
4604
- manifestDocument = JSON.parse(await readFile3(path8.join(snapshotRoot, "package.json"), "utf8"));
4709
+ const manifestFile = await readRegularFileNoFollow(path8.join(snapshotRoot, "package.json"), {
4710
+ label: "Snapshot package.json",
4711
+ maxBytes: 1024 * 1024
4712
+ });
4713
+ manifestDocument = JSON.parse(manifestFile.contents.toString("utf8"));
4605
4714
  } catch (error) {
4606
4715
  if (error.code === "ENOENT") {
4607
4716
  throw new Error("The environment snapshot does not contain package.json.");
@@ -4646,30 +4755,56 @@ var cleanupHarnessExecutionArtifacts = async (stateDirectory, before) => {
4646
4755
  throw error;
4647
4756
  }
4648
4757
  for (const entry of entries) {
4649
- if (!entry.isDirectory() || entry.isSymbolicLink() || !/^[a-f0-9]{24}$/.test(entry.name)) {
4758
+ const stagedMatch = STAGED_EXECUTION_ARTIFACT_DIRECTORY_PATTERN.exec(entry.name);
4759
+ const artifactName = EXECUTION_ARTIFACT_DIRECTORY_PATTERN.test(entry.name) ? entry.name : stagedMatch?.[1];
4760
+ if (!entry.isDirectory() || entry.isSymbolicLink() || !artifactName) {
4650
4761
  result.skipped += 1;
4651
4762
  continue;
4652
4763
  }
4653
4764
  result.scanned += 1;
4654
4765
  const directory = path8.join(root, entry.name);
4655
- const resolved = await realpath3(directory);
4656
- if (!isInside3(root, resolved)) {
4657
- result.skipped += 1;
4658
- continue;
4659
- }
4660
4766
  try {
4767
+ const resolved = await realpath3(directory);
4768
+ if (!isInside3(root, resolved))
4769
+ throw new Error("unsafe execution artifact directory");
4770
+ const directoryEntry = await lstat6(directory);
4771
+ if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
4772
+ throw new Error("unsafe execution artifact directory");
4773
+ }
4661
4774
  const metadataPath = path8.join(directory, "environment.json");
4662
- const metadataEntry = await lstat6(metadataPath);
4663
- if (metadataEntry.isSymbolicLink() || !metadataEntry.isFile())
4664
- throw new Error("unsafe metadata");
4665
- const metadata = JSON.parse(await readFile3(metadataPath, "utf8"));
4775
+ const metadataFile = await readRegularFileNoFollow(metadataPath, {
4776
+ label: "Execution cleanup metadata",
4777
+ maxBytes: 1024 * 1024
4778
+ });
4779
+ const metadata = JSON.parse(metadataFile.contents.toString("utf8"));
4666
4780
  const releasedAt = metadata.releasedAt ? Date.parse(metadata.releasedAt) : Number.NaN;
4667
4781
  if (!metadata.status || !TERMINAL_STATUSES.has(metadata.status) || !Number.isFinite(releasedAt) || releasedAt >= before) {
4668
4782
  result.skipped += 1;
4669
4783
  continue;
4670
4784
  }
4671
- await rm(directory, { recursive: true, force: true });
4672
- result.deleted += 1;
4785
+ const stagedDirectory = path8.join(root, `.cleanup-${artifactName}-${randomUUID3()}`);
4786
+ let staged = false;
4787
+ try {
4788
+ await rename2(directory, stagedDirectory);
4789
+ staged = true;
4790
+ const movedEntry = await lstat6(stagedDirectory);
4791
+ if (movedEntry.isSymbolicLink() || !movedEntry.isDirectory() || movedEntry.dev !== directoryEntry.dev || movedEntry.ino !== directoryEntry.ino) {
4792
+ throw new Error("execution artifact directory changed before cleanup");
4793
+ }
4794
+ const recheckedMetadata = await readRegularFileNoFollow(path8.join(stagedDirectory, "environment.json"), { label: "Execution cleanup metadata", maxBytes: 1024 * 1024 });
4795
+ if (!recheckedMetadata.contents.equals(metadataFile.contents)) {
4796
+ throw new Error("execution artifact metadata changed before cleanup");
4797
+ }
4798
+ await rm(stagedDirectory, { recursive: true, force: true });
4799
+ staged = false;
4800
+ result.deleted += 1;
4801
+ } catch (error) {
4802
+ if (staged)
4803
+ await rename2(stagedDirectory, directory).catch(() => {
4804
+ return;
4805
+ });
4806
+ throw error;
4807
+ }
4673
4808
  } catch {
4674
4809
  result.skipped += 1;
4675
4810
  }
@@ -4685,8 +4820,7 @@ var describeOciCommand = (command, args) => commandDisplay([command, ...args]);
4685
4820
 
4686
4821
  // src/context-engineering.ts
4687
4822
  import { createHash as createHash7 } from "node:crypto";
4688
- import { constants as fsConstants2 } from "node:fs";
4689
- import { lstat as lstat7, open as open3, readdir as readdir3 } from "node:fs/promises";
4823
+ import { lstat as lstat7, readdir as readdir3 } from "node:fs/promises";
4690
4824
  import path9 from "node:path";
4691
4825
  import { z as z3 } from "zod";
4692
4826
  var HARNESS_CONTEXT_CONFIG_SCHEMA_VERSION = 1;
@@ -4765,7 +4899,6 @@ var validateRelativePath = (value) => {
4765
4899
  }
4766
4900
  return normalized;
4767
4901
  };
4768
- var stableEntry = (before, after) => before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
4769
4902
  var resolveSafeWorkspaceEntry = async (workspace, relativePath, options = {}) => {
4770
4903
  const normalized = validateRelativePath(relativePath);
4771
4904
  const absolutePath = path9.resolve(workspace.root, normalized);
@@ -4777,6 +4910,9 @@ var resolveSafeWorkspaceEntry = async (workspace, relativePath, options = {}) =>
4777
4910
  const segments = normalized.split("/");
4778
4911
  for (let index = 0;index < segments.length; index += 1) {
4779
4912
  current = path9.join(current, segments[index]);
4913
+ const leaf = index === segments.length - 1;
4914
+ if (leaf && options.skipLeafValidation)
4915
+ continue;
4780
4916
  let entry;
4781
4917
  try {
4782
4918
  entry = await lstat7(current);
@@ -4788,7 +4924,6 @@ var resolveSafeWorkspaceEntry = async (workspace, relativePath, options = {}) =>
4788
4924
  if (entry.isSymbolicLink()) {
4789
4925
  throw new Error(`The harness context path cannot contain a symbolic link: ${relativePath}`);
4790
4926
  }
4791
- const leaf = index === segments.length - 1;
4792
4927
  if (!leaf && !entry.isDirectory()) {
4793
4928
  throw new Error(`A harness context path ancestor is not a directory: ${relativePath}`);
4794
4929
  }
@@ -4802,42 +4937,39 @@ var resolveSafeWorkspaceEntry = async (workspace, relativePath, options = {}) =>
4802
4937
  return { path: absolutePath, relativePath: normalized };
4803
4938
  };
4804
4939
  var readStableWorkspaceText = async (workspace, relativePath, maxBytes, allowMissing = false) => {
4805
- const resolved = await resolveSafeWorkspaceEntry(workspace, relativePath, { allowMissing });
4940
+ const resolved = await resolveSafeWorkspaceEntry(workspace, relativePath, {
4941
+ allowMissing,
4942
+ skipLeafValidation: true
4943
+ });
4806
4944
  if (!resolved)
4807
4945
  return;
4808
- const before = await lstat7(resolved.path);
4809
- if (before.size > maxBytes) {
4810
- throw new Error(`Harness context file ${resolved.relativePath} exceeds the ${maxBytes}-byte limit.`);
4946
+ let file;
4947
+ try {
4948
+ file = await readRegularFileNoFollow(resolved.path, {
4949
+ label: `Harness context file ${resolved.relativePath}`,
4950
+ maxBytes
4951
+ });
4952
+ } catch (error) {
4953
+ if (allowMissing && error.code === "ENOENT")
4954
+ return;
4955
+ throw error;
4956
+ }
4957
+ const { contents } = file;
4958
+ if (contents.includes(0)) {
4959
+ throw new Error(`Harness context file must be UTF-8 text: ${resolved.relativePath}`);
4811
4960
  }
4812
- const handle = await open3(resolved.path, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
4961
+ let content;
4813
4962
  try {
4814
- const opened = await handle.stat();
4815
- if (!opened.isFile() || !stableEntry(before, opened)) {
4816
- throw new Error(`Harness context file changed while opening: ${resolved.relativePath}`);
4817
- }
4818
- const contents = await handle.readFile();
4819
- const after = await handle.stat();
4820
- if (!stableEntry(opened, after) || contents.byteLength !== after.size) {
4821
- throw new Error(`Harness context file changed while reading: ${resolved.relativePath}`);
4822
- }
4823
- if (contents.includes(0)) {
4824
- throw new Error(`Harness context file must be UTF-8 text: ${resolved.relativePath}`);
4825
- }
4826
- let content;
4827
- try {
4828
- content = new TextDecoder("utf-8", { fatal: true }).decode(contents);
4829
- } catch {
4830
- throw new Error(`Harness context file must be valid UTF-8: ${resolved.relativePath}`);
4831
- }
4832
- return {
4833
- path: resolved.relativePath,
4834
- digest: digestBytes2(contents),
4835
- bytes: contents.byteLength,
4836
- content
4837
- };
4838
- } finally {
4839
- await handle.close();
4963
+ content = new TextDecoder("utf-8", { fatal: true }).decode(contents);
4964
+ } catch {
4965
+ throw new Error(`Harness context file must be valid UTF-8: ${resolved.relativePath}`);
4840
4966
  }
4967
+ return {
4968
+ path: resolved.relativePath,
4969
+ digest: digestBytes2(contents),
4970
+ bytes: contents.byteLength,
4971
+ content
4972
+ };
4841
4973
  };
4842
4974
  var parseConfiguration = (file) => {
4843
4975
  let document;
@@ -6469,7 +6601,6 @@ var normalizeScope = (scope) => {
6469
6601
  var stableKey = (kind, value) => createHash9("sha256").update(kind).update("\x00").update(value).digest("hex");
6470
6602
  var sessionId = () => `ses_${randomUUID5()}`;
6471
6603
  var turnId = () => `turn_${randomUUID5()}`;
6472
- var isMissing = (error) => error.code === "ENOENT";
6473
6604
  var ensurePrivateDatabase = async (workspace, stateDirectory) => {
6474
6605
  const requestedStateDirectory = path11.resolve(stateDirectory);
6475
6606
  await validateStateDirectory(workspace, requestedStateDirectory);
@@ -6483,19 +6614,12 @@ var ensurePrivateDatabase = async (workspace, stateDirectory) => {
6483
6614
  const databasePath = path11.join(canonicalStateDirectory2, HARNESS_SESSION_INDEX_FILE);
6484
6615
  let databaseEntry;
6485
6616
  try {
6617
+ const handle = await open4(databasePath, "wx", 384);
6618
+ await handle.close();
6486
6619
  databaseEntry = await lstat8(databasePath);
6487
6620
  } catch (error) {
6488
- if (!isMissing(error)) {
6621
+ if (error.code !== "EEXIST")
6489
6622
  throw error;
6490
- }
6491
- try {
6492
- const handle = await open4(databasePath, "wx", 384);
6493
- await handle.close();
6494
- } catch (creationError) {
6495
- if (creationError.code !== "EEXIST") {
6496
- throw creationError;
6497
- }
6498
- }
6499
6623
  databaseEntry = await lstat8(databasePath);
6500
6624
  }
6501
6625
  if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
@@ -9274,14 +9398,11 @@ var inspectGit = async (workspace) => {
9274
9398
  var inspectScripts = async (workspace, allowedChecks) => {
9275
9399
  const packagePath = path12.join(workspace, "package.json");
9276
9400
  try {
9277
- const packageStat = await stat4(packagePath);
9278
- if (!packageStat.isFile() || packageStat.size > 1024 * 1024) {
9279
- return diagnostic("scripts", "warn", "package.json is not a readable regular file.", {
9280
- packageJson: false,
9281
- available: []
9282
- });
9283
- }
9284
- const packageJson = JSON.parse(await readFile4(packagePath, "utf8"));
9401
+ const packageFile = await readRegularFileNoFollow(packagePath, {
9402
+ label: "package.json",
9403
+ maxBytes: 1024 * 1024
9404
+ });
9405
+ const packageJson = JSON.parse(packageFile.contents.toString("utf8"));
9285
9406
  const scripts = packageJson.scripts && typeof packageJson.scripts === "object" ? packageJson.scripts : {};
9286
9407
  const available = allowedChecks.filter((name) => typeof scripts[name] === "string");
9287
9408
  const missing = allowedChecks.filter((name) => typeof scripts[name] !== "string");
@@ -9816,22 +9937,22 @@ var MAX_CHANGE_ARTIFACT_BYTES = 64 * 1024 * 1024;
9816
9937
  var readStableCliArtifact = async (filePath, maxBytes) => {
9817
9938
  const absolute = path12.resolve(process.cwd(), filePath);
9818
9939
  try {
9819
- const before = await lstat9(absolute);
9820
- if (before.isSymbolicLink() || !before.isFile()) {
9940
+ return (await readRegularFileNoFollow(absolute, {
9941
+ label: `Change artifact ${filePath}`,
9942
+ maxBytes
9943
+ })).contents;
9944
+ } catch (error) {
9945
+ if (error instanceof CliUsageError)
9946
+ throw error;
9947
+ if (error instanceof UnsafeFileTypeError) {
9821
9948
  throw new CliUsageError(`Change artifact must be a regular non-symlink file: ${filePath}`);
9822
9949
  }
9823
- if (before.size > maxBytes) {
9950
+ if (error instanceof FileSizeLimitError) {
9824
9951
  throw new CliUsageError(`Change artifact exceeds the ${maxBytes}-byte limit: ${filePath}`);
9825
9952
  }
9826
- const contents = await readFile4(absolute);
9827
- const after = await lstat9(absolute);
9828
- if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
9953
+ if (error instanceof FileChangedWhileReadingError) {
9829
9954
  throw new CliUsageError(`Change artifact changed while it was being read: ${filePath}`);
9830
9955
  }
9831
- return contents;
9832
- } catch (error) {
9833
- if (error instanceof CliUsageError)
9834
- throw error;
9835
9956
  const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
9836
9957
  if (typeof code === "string") {
9837
9958
  throw new CliUsageError(`Change artifact cannot be read (${code}): ${filePath}`);