@zhivex-ai/harness 0.8.0 → 0.9.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.
@@ -365,7 +365,7 @@ var HARNESS_CONFIG_SCHEMA_VERSION = 4;
365
365
  var HARNESS_STORE_BACKENDS = ["sqlite", "file"];
366
366
  var HARNESS_EXECUTION_BACKENDS = ["none", "oci"];
367
367
  var HARNESS_OCI_RUNTIMES = ["docker", "podman"];
368
- var HARNESS_EXECUTION_POLICY_VERSION = "2026-08-17-v1";
368
+ var HARNESS_EXECUTION_POLICY_VERSION = "2026-08-20-v2";
369
369
  var DEFAULT_OCI_EXECUTION = {
370
370
  runtime: "docker",
371
371
  image: "oven/bun:1.3.7-slim",
@@ -650,8 +650,10 @@ var createProviderModel = (config, env = process.env, registry = DEFAULT_PROVIDE
650
650
 
651
651
  // src/execution-environment.ts
652
652
  import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
653
+ import { constants as fsConstants } from "fs";
653
654
  import {
654
655
  chmod,
656
+ copyFile,
655
657
  lstat as lstat2,
656
658
  mkdir as mkdir2,
657
659
  readFile as readFile2,
@@ -808,6 +810,10 @@ var IGNORE_FILES = [".gitignore", ".zhivex-harnessignore"];
808
810
  var DEFAULT_CHECKS = ["test", "typecheck", "lint", "build"];
809
811
  var MAX_FILE_BYTES = 1024 * 1024;
810
812
  var MAX_TOOL_OUTPUT = 20000;
813
+ var MAX_READ_BATCH_FILES = 20;
814
+ var MAX_READ_BATCH_BYTES = 2 * MAX_FILE_BYTES;
815
+ var MAX_SEARCH_MANY_QUERIES = 10;
816
+ var MAX_SEARCH_MANY_MATCHES = 500;
811
817
  var isSensitiveName = (name) => {
812
818
  const normalized = name.toLocaleLowerCase();
813
819
  return normalized === ".env" || normalized.startsWith(".env.") && normalized !== ".env.example" || normalized === ".npmrc" || normalized === "id_rsa" || normalized === "id_ed25519" || normalized.endsWith(".key") || normalized.endsWith(".pem") || normalized.endsWith(".p12") || normalized.endsWith(".pfx");
@@ -829,6 +835,30 @@ var cursorDecode = (value) => {
829
835
  throw new Error("The pagination cursor is invalid.");
830
836
  }
831
837
  };
838
+ var firstPathAtLeast = (files, target) => {
839
+ let low = 0;
840
+ let high = files.length;
841
+ while (low < high) {
842
+ const middle = low + Math.floor((high - low) / 2);
843
+ if ((files[middle]?.path ?? "") < target)
844
+ low = middle + 1;
845
+ else
846
+ high = middle;
847
+ }
848
+ return low;
849
+ };
850
+ var firstPathAfter = (files, target) => {
851
+ let low = 0;
852
+ let high = files.length;
853
+ while (low < high) {
854
+ const middle = low + Math.floor((high - low) / 2);
855
+ if ((files[middle]?.path ?? "") <= target)
856
+ low = middle + 1;
857
+ else
858
+ high = middle;
859
+ }
860
+ return low;
861
+ };
832
862
  var escapeRegex = (value) => value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
833
863
  var globRegex = (pattern) => {
834
864
  let source = "";
@@ -920,6 +950,11 @@ var spawnBounded = async (command, cwd, timeoutMs = 120000) => {
920
950
  class Workspace {
921
951
  root;
922
952
  auditEntries = [];
953
+ indexIdentity = randomUUID();
954
+ indexRevision = 0;
955
+ workspaceIndex;
956
+ indexBuild;
957
+ indexMetrics = { builds: 0, reuses: 0, stableFileReads: 0 };
923
958
  constructor(root) {
924
959
  this.root = root;
925
960
  }
@@ -971,6 +1006,7 @@ class Workspace {
971
1006
  return { path: candidate, exists: true };
972
1007
  }
973
1008
  async readStableFile(relativePath, allowBinary = true) {
1009
+ this.indexMetrics.stableFileReads += 1;
974
1010
  const safe = await this.safePath(relativePath, { requireFile: true });
975
1011
  const before = await lstat(safe.path);
976
1012
  if (before.size > MAX_FILE_BYTES)
@@ -990,14 +1026,40 @@ class Workspace {
990
1026
  mode: before.mode & 511
991
1027
  };
992
1028
  }
993
- async rulesForDirectory(directory, base, inherited) {
1029
+ fingerprint(relativePath, kind, entry, digest2) {
1030
+ return {
1031
+ path: relativePath || ".",
1032
+ kind,
1033
+ dev: entry.dev,
1034
+ ino: entry.ino,
1035
+ size: entry.size,
1036
+ mtimeMs: entry.mtimeMs,
1037
+ ctimeMs: entry.ctimeMs,
1038
+ ...digest2 ? { digest: digest2 } : {}
1039
+ };
1040
+ }
1041
+ sameFingerprint(entry, fingerprint) {
1042
+ return entry.dev === fingerprint.dev && entry.ino === fingerprint.ino && entry.size === fingerprint.size && entry.mtimeMs === fingerprint.mtimeMs && entry.ctimeMs === fingerprint.ctimeMs;
1043
+ }
1044
+ async rulesForDirectory(directory, base, inherited, ignoreFiles) {
994
1045
  const rules = [...inherited];
995
1046
  for (const filename of IGNORE_FILES) {
996
1047
  try {
997
1048
  const ignorePath = path3.join(directory, filename);
998
- const entry = await lstat(ignorePath);
999
- if (entry.isFile() && entry.size <= MAX_FILE_BYTES) {
1000
- rules.push(...parseIgnoreRules(await readFile(ignorePath, "utf8"), base));
1049
+ const before = await lstat(ignorePath);
1050
+ if (before.isSymbolicLink() || !before.isFile())
1051
+ continue;
1052
+ if (before.size <= MAX_FILE_BYTES) {
1053
+ const contents = await readFile(ignorePath);
1054
+ const after = await lstat(ignorePath);
1055
+ if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, this.fingerprint("", "ignore-file", before))) {
1056
+ throw new Error(`The ignore file changed while the workspace was being indexed: ${wirePath(path3.relative(this.root, ignorePath))}`);
1057
+ }
1058
+ const digest2 = digestBytes(contents);
1059
+ ignoreFiles.push(this.fingerprint(wirePath(path3.relative(this.root, ignorePath)), "ignore-file", after, digest2));
1060
+ rules.push(...parseIgnoreRules(contents.toString("utf8"), base));
1061
+ } else {
1062
+ ignoreFiles.push(this.fingerprint(wirePath(path3.relative(this.root, ignorePath)), "ignore-file", before));
1001
1063
  }
1002
1064
  } catch (error) {
1003
1065
  if (error.code !== "ENOENT")
@@ -1006,10 +1068,17 @@ class Workspace {
1006
1068
  }
1007
1069
  return rules;
1008
1070
  }
1009
- async collectFiles() {
1071
+ async collectFiles(version) {
1010
1072
  const files = [];
1073
+ const directories = [];
1074
+ const ignoreFiles = [];
1011
1075
  const walk = async (directory, base, inherited) => {
1012
- const rules = await this.rulesForDirectory(directory, base, inherited);
1076
+ const directoryEntry = await lstat(directory);
1077
+ if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
1078
+ throw new Error(`A workspace directory changed while it was being indexed: ${base || "."}`);
1079
+ }
1080
+ directories.push(this.fingerprint(base, "directory", directoryEntry));
1081
+ const rules = await this.rulesForDirectory(directory, base, inherited, ignoreFiles);
1013
1082
  const entries = (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1014
1083
  for (const entry of entries) {
1015
1084
  const relative = base ? `${base}/${entry.name}` : entry.name;
@@ -1019,11 +1088,99 @@ class Workspace {
1019
1088
  if (entry.isDirectory())
1020
1089
  await walk(absolute, relative, rules);
1021
1090
  else if (entry.isFile())
1022
- files.push({ path: relative, size: (await lstat(absolute)).size });
1091
+ files.push({ path: relative });
1023
1092
  }
1024
1093
  };
1025
1094
  await walk(this.root, "", []);
1026
- return files;
1095
+ files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
1096
+ return { version, files, directories, ignoreFiles };
1097
+ }
1098
+ async isWorkspaceIndexFresh(index) {
1099
+ try {
1100
+ for (const fingerprint of index.directories) {
1101
+ const absolute = fingerprint.path === "." ? this.root : path3.join(this.root, fingerprint.path);
1102
+ const entry = await lstat(absolute);
1103
+ if (entry.isSymbolicLink() || !entry.isDirectory() || !this.sameFingerprint(entry, fingerprint))
1104
+ return false;
1105
+ }
1106
+ for (const fingerprint of index.ignoreFiles) {
1107
+ const absolute = path3.join(this.root, fingerprint.path);
1108
+ const before = await lstat(absolute);
1109
+ if (before.isSymbolicLink() || !before.isFile() || !this.sameFingerprint(before, fingerprint))
1110
+ return false;
1111
+ if (fingerprint.digest) {
1112
+ const contents = await readFile(absolute);
1113
+ const after = await lstat(absolute);
1114
+ if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, fingerprint) || digestBytes(contents) !== fingerprint.digest)
1115
+ return false;
1116
+ }
1117
+ }
1118
+ return true;
1119
+ } catch {
1120
+ return false;
1121
+ }
1122
+ }
1123
+ invalidateWorkspaceIndex() {
1124
+ this.indexRevision += 1;
1125
+ this.workspaceIndex = undefined;
1126
+ }
1127
+ async buildWorkspaceIndex(revision) {
1128
+ const version = `${this.indexIdentity}:${revision}`;
1129
+ for (let attempt = 0;attempt < 3; attempt += 1) {
1130
+ try {
1131
+ const index = await this.collectFiles(version);
1132
+ if (await this.isWorkspaceIndexFresh(index))
1133
+ return index;
1134
+ } catch (error) {
1135
+ const code = error.code;
1136
+ const changedDuringIndexing = error instanceof Error && error.message.includes("while the workspace was being indexed");
1137
+ if (attempt === 2 || code !== "ENOENT" && !changedDuringIndexing)
1138
+ throw error;
1139
+ }
1140
+ }
1141
+ throw new Error("The workspace kept changing while its file index was being built.");
1142
+ }
1143
+ async getWorkspaceIndex() {
1144
+ const cached = this.workspaceIndex;
1145
+ if (cached) {
1146
+ if (await this.isWorkspaceIndexFresh(cached)) {
1147
+ this.indexMetrics.reuses += 1;
1148
+ return cached;
1149
+ }
1150
+ this.invalidateWorkspaceIndex();
1151
+ }
1152
+ const revision = this.indexRevision;
1153
+ if (!this.indexBuild || this.indexBuild.revision !== revision) {
1154
+ const promise = this.buildWorkspaceIndex(revision);
1155
+ this.indexBuild = { revision, promise };
1156
+ }
1157
+ const build = this.indexBuild;
1158
+ let index;
1159
+ try {
1160
+ index = await build.promise;
1161
+ } catch (error) {
1162
+ if (this.indexBuild === build)
1163
+ this.indexBuild = undefined;
1164
+ throw error;
1165
+ }
1166
+ if (revision !== this.indexRevision)
1167
+ return this.getWorkspaceIndex();
1168
+ if (this.indexBuild === build)
1169
+ this.indexBuild = undefined;
1170
+ if (this.workspaceIndex?.version !== index.version) {
1171
+ this.workspaceIndex = index;
1172
+ this.indexMetrics.builds += 1;
1173
+ }
1174
+ return this.workspaceIndex;
1175
+ }
1176
+ workspaceIndexDiagnostics() {
1177
+ const index = this.workspaceIndex;
1178
+ return {
1179
+ ...this.indexMetrics,
1180
+ ...index ? { version: index.version } : {},
1181
+ files: index?.files.length ?? 0,
1182
+ directories: index?.directories.length ?? 0
1183
+ };
1027
1184
  }
1028
1185
  async listFiles(relativePath = ".", options = {}) {
1029
1186
  const start = await this.safePath(relativePath);
@@ -1035,36 +1192,52 @@ class Workspace {
1035
1192
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 5000)
1036
1193
  throw new Error("limit must be between 1 and 5000.");
1037
1194
  let after = "";
1195
+ let cursorIndexVersion;
1038
1196
  if (input.cursor) {
1039
1197
  const parsed = cursorDecode(input.cursor);
1040
- if (parsed.v !== 1 || parsed.kind !== "list" || parsed.path !== requestPath || parsed.limit !== limit || typeof parsed.after !== "string") {
1198
+ if (parsed.v !== 2 || parsed.kind !== "list" || parsed.path !== requestPath || parsed.limit !== limit || typeof parsed.indexVersion !== "string" || typeof parsed.after !== "string") {
1041
1199
  throw new Error("The pagination cursor does not match this list request.");
1042
1200
  }
1043
1201
  after = parsed.after;
1202
+ cursorIndexVersion = parsed.indexVersion;
1203
+ }
1204
+ const index = await this.getWorkspaceIndex();
1205
+ if (cursorIndexVersion && cursorIndexVersion !== index.version) {
1206
+ throw new Error("The pagination cursor is stale because the workspace changed.");
1044
1207
  }
1045
1208
  const prefix = requestPath === "." ? "" : `${requestPath}/`;
1046
- const candidates = (await this.collectFiles()).filter((file) => (requestPath === "." || file.path.startsWith(prefix)) && file.path > after);
1047
- const selected = candidates.slice(0, limit);
1209
+ let candidateIndex = after ? firstPathAfter(index.files, after) : requestPath === "." ? 0 : firstPathAtLeast(index.files, prefix);
1210
+ const selected = [];
1211
+ while (candidateIndex < index.files.length && selected.length <= limit) {
1212
+ const candidate = index.files[candidateIndex];
1213
+ if (!candidate || requestPath !== "." && !candidate.path.startsWith(prefix))
1214
+ break;
1215
+ selected.push(candidate);
1216
+ candidateIndex += 1;
1217
+ }
1218
+ const truncated = selected.length > limit;
1219
+ if (truncated)
1220
+ selected.pop();
1048
1221
  const files = [];
1049
1222
  for (const candidate of selected) {
1050
1223
  const file = await this.readStableFile(candidate.path);
1051
1224
  files.push({ path: file.path, size: file.contents.byteLength, digest: file.digest });
1052
1225
  }
1053
- const truncated = candidates.length > selected.length;
1054
1226
  const last = selected.at(-1);
1055
1227
  return {
1056
1228
  files,
1057
1229
  truncated,
1058
- ...truncated && last ? { nextCursor: cursorEncode({ v: 1, kind: "list", path: requestPath, limit, after: last.path }) } : {}
1230
+ ...truncated && last ? {
1231
+ nextCursor: cursorEncode({ v: 2, kind: "list", path: requestPath, limit, after: last.path, indexVersion: index.version })
1232
+ } : {}
1059
1233
  };
1060
1234
  }
1061
- async readFile(relativePath, startLine = 1, endLine) {
1235
+ renderReadFile(file, startLine = 1, endLine) {
1062
1236
  if (!Number.isSafeInteger(startLine) || startLine < 1)
1063
1237
  throw new Error("startLine must be a positive integer.");
1064
1238
  if (endLine !== undefined && (!Number.isSafeInteger(endLine) || endLine < startLine)) {
1065
1239
  throw new Error("endLine must be greater than or equal to startLine.");
1066
1240
  }
1067
- const file = await this.readStableFile(relativePath, false);
1068
1241
  const lines = file.contents.toString("utf8").split(/\r?\n/);
1069
1242
  const boundedEnd = Math.min(endLine ?? startLine + 399, startLine + 1999, lines.length);
1070
1243
  return {
@@ -1078,6 +1251,37 @@ class Workspace {
1078
1251
  truncated: boundedEnd < lines.length
1079
1252
  };
1080
1253
  }
1254
+ async readFile(relativePath, startLine = 1, endLine) {
1255
+ return this.renderReadFile(await this.readStableFile(relativePath, false), startLine, endLine);
1256
+ }
1257
+ async readFiles(requests) {
1258
+ if (!Array.isArray(requests) || requests.length < 1 || requests.length > MAX_READ_BATCH_FILES) {
1259
+ throw new Error(`readFiles requires between 1 and ${MAX_READ_BATCH_FILES} file requests.`);
1260
+ }
1261
+ const ordered = requests.map((request) => ({
1262
+ ...request,
1263
+ path: wirePath(path3.relative(this.root, this.lexicalPath(request.path)))
1264
+ })).sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0) || (a.startLine ?? 1) - (b.startLine ?? 1) || (a.endLine ?? 0) - (b.endLine ?? 0));
1265
+ const stableFiles = new Map;
1266
+ let totalBytes = 0;
1267
+ for (const request of ordered) {
1268
+ if (!Number.isSafeInteger(request.startLine ?? 1) || (request.startLine ?? 1) < 1 || request.endLine !== undefined && (!Number.isSafeInteger(request.endLine) || request.endLine < (request.startLine ?? 1))) {
1269
+ throw new Error(`Invalid line range for ${request.path}.`);
1270
+ }
1271
+ if (stableFiles.has(request.path))
1272
+ continue;
1273
+ const file = await this.readStableFile(request.path, false);
1274
+ totalBytes += file.contents.byteLength;
1275
+ if (totalBytes > MAX_READ_BATCH_BYTES) {
1276
+ throw new Error(`readFiles exceeds the aggregate ${MAX_READ_BATCH_BYTES}-byte source limit.`);
1277
+ }
1278
+ stableFiles.set(request.path, file);
1279
+ }
1280
+ return {
1281
+ files: ordered.map((request) => this.renderReadFile(stableFiles.get(request.path), request.startLine ?? 1, request.endLine)),
1282
+ sourceBytes: totalBytes
1283
+ };
1284
+ }
1081
1285
  async inspectFile(relativePath) {
1082
1286
  const file = await this.readStableFile(relativePath);
1083
1287
  return {
@@ -1100,21 +1304,29 @@ class Workspace {
1100
1304
  const caseSensitive = options.caseSensitive ?? false;
1101
1305
  let afterPath = "";
1102
1306
  let afterLine = 0;
1307
+ let cursorIndexVersion;
1103
1308
  if (options.cursor) {
1104
1309
  const parsed = cursorDecode(options.cursor);
1105
- if (parsed.v !== 1 || parsed.kind !== "search" || parsed.path !== requestPath || parsed.query !== query || parsed.caseSensitive !== caseSensitive || parsed.limit !== limit || typeof parsed.afterPath !== "string" || !Number.isSafeInteger(parsed.afterLine)) {
1310
+ if (parsed.v !== 2 || parsed.kind !== "search" || parsed.path !== requestPath || parsed.query !== query || parsed.caseSensitive !== caseSensitive || parsed.limit !== limit || typeof parsed.afterPath !== "string" || !Number.isSafeInteger(parsed.afterLine) || typeof parsed.indexVersion !== "string") {
1106
1311
  throw new Error("The pagination cursor does not match this search request.");
1107
1312
  }
1108
1313
  afterPath = parsed.afterPath;
1109
1314
  afterLine = parsed.afterLine;
1315
+ cursorIndexVersion = parsed.indexVersion;
1316
+ }
1317
+ const index = await this.getWorkspaceIndex();
1318
+ if (cursorIndexVersion && cursorIndexVersion !== index.version) {
1319
+ throw new Error("The pagination cursor is stale because the workspace changed.");
1110
1320
  }
1111
1321
  const prefix = requestPath === "." ? "" : `${requestPath}/`;
1112
1322
  const needle = caseSensitive ? query : query.toLocaleLowerCase();
1113
1323
  const matches = [];
1114
1324
  let hasMore = false;
1115
- for (const candidate of await this.collectFiles()) {
1116
- if (requestPath !== "." && !candidate.path.startsWith(prefix) || candidate.size > MAX_FILE_BYTES || candidate.path < afterPath)
1117
- continue;
1325
+ let candidateIndex = afterPath ? firstPathAtLeast(index.files, afterPath) : requestPath === "." ? 0 : firstPathAtLeast(index.files, prefix);
1326
+ for (;candidateIndex < index.files.length; candidateIndex += 1) {
1327
+ const candidate = index.files[candidateIndex];
1328
+ if (!candidate || requestPath !== "." && !candidate.path.startsWith(prefix))
1329
+ break;
1118
1330
  let file;
1119
1331
  try {
1120
1332
  file = await this.readStableFile(candidate.path, false);
@@ -1122,11 +1334,11 @@ class Workspace {
1122
1334
  continue;
1123
1335
  }
1124
1336
  const lines = file.contents.toString("utf8").split(/\r?\n/);
1125
- for (let index = 0;index < lines.length; index += 1) {
1126
- const lineNumber = index + 1;
1337
+ for (let index2 = 0;index2 < lines.length; index2 += 1) {
1338
+ const lineNumber = index2 + 1;
1127
1339
  if (candidate.path === afterPath && lineNumber <= afterLine)
1128
1340
  continue;
1129
- const line = lines[index] ?? "";
1341
+ const line = lines[index2] ?? "";
1130
1342
  if (!(caseSensitive ? line : line.toLocaleLowerCase()).includes(needle))
1131
1343
  continue;
1132
1344
  if (matches.length >= limit) {
@@ -1143,10 +1355,88 @@ class Workspace {
1143
1355
  matches,
1144
1356
  truncated: hasMore,
1145
1357
  ...hasMore && last ? {
1146
- nextCursor: cursorEncode({ v: 1, kind: "search", path: requestPath, query, caseSensitive, limit, afterPath: last.path, afterLine: last.line })
1358
+ nextCursor: cursorEncode({
1359
+ v: 2,
1360
+ kind: "search",
1361
+ path: requestPath,
1362
+ query,
1363
+ caseSensitive,
1364
+ limit,
1365
+ afterPath: last.path,
1366
+ afterLine: last.line,
1367
+ indexVersion: index.version
1368
+ })
1147
1369
  } : {}
1148
1370
  };
1149
1371
  }
1372
+ async searchMany(queries, relativePath = ".", options = {}) {
1373
+ if (!Array.isArray(queries) || queries.length < 1 || queries.length > MAX_SEARCH_MANY_QUERIES) {
1374
+ throw new Error(`searchMany requires between 1 and ${MAX_SEARCH_MANY_QUERIES} queries.`);
1375
+ }
1376
+ const limitPerQuery = options.limitPerQuery ?? 50;
1377
+ if (!Number.isSafeInteger(limitPerQuery) || limitPerQuery < 1 || limitPerQuery > MAX_SEARCH_MANY_MATCHES || limitPerQuery * queries.length > MAX_SEARCH_MANY_MATCHES) {
1378
+ throw new Error(`searchMany allows at most ${MAX_SEARCH_MANY_MATCHES} aggregate matches.`);
1379
+ }
1380
+ const seen = new Set;
1381
+ const states = queries.map((input) => {
1382
+ if (!input.query || input.query.length > 200)
1383
+ throw new Error("Each search query must be between 1 and 200 characters.");
1384
+ const caseSensitive = input.caseSensitive ?? false;
1385
+ const key = `${caseSensitive ? "1" : "0"}\x00${input.query}`;
1386
+ if (seen.has(key))
1387
+ throw new Error("searchMany queries must be unique.");
1388
+ seen.add(key);
1389
+ return {
1390
+ query: input.query,
1391
+ caseSensitive,
1392
+ needle: caseSensitive ? input.query : input.query.toLocaleLowerCase(),
1393
+ matches: [],
1394
+ truncated: false
1395
+ };
1396
+ });
1397
+ const start = await this.safePath(relativePath);
1398
+ if (!(await lstat(start.path)).isDirectory())
1399
+ throw new Error("searchMany requires a directory.");
1400
+ const requestPath = wirePath(path3.relative(this.root, start.path)) || ".";
1401
+ const prefix = requestPath === "." ? "" : `${requestPath}/`;
1402
+ const index = await this.getWorkspaceIndex();
1403
+ let candidateIndex = requestPath === "." ? 0 : firstPathAtLeast(index.files, prefix);
1404
+ for (;candidateIndex < index.files.length; candidateIndex += 1) {
1405
+ const candidate = index.files[candidateIndex];
1406
+ if (!candidate || requestPath !== "." && !candidate.path.startsWith(prefix))
1407
+ break;
1408
+ let file;
1409
+ try {
1410
+ file = await this.readStableFile(candidate.path, false);
1411
+ } catch {
1412
+ continue;
1413
+ }
1414
+ const lines = file.contents.toString("utf8").split(/\r?\n/);
1415
+ for (let lineIndex = 0;lineIndex < lines.length; lineIndex += 1) {
1416
+ const line = lines[lineIndex] ?? "";
1417
+ const foldedLine = line.toLocaleLowerCase();
1418
+ for (const state of states) {
1419
+ if (state.truncated || !(state.caseSensitive ? line : foldedLine).includes(state.needle))
1420
+ continue;
1421
+ if (state.matches.length >= limitPerQuery) {
1422
+ state.truncated = true;
1423
+ continue;
1424
+ }
1425
+ state.matches.push({
1426
+ path: candidate.path,
1427
+ line: lineIndex + 1,
1428
+ text: truncate(line, 500),
1429
+ digest: file.digest
1430
+ });
1431
+ }
1432
+ }
1433
+ if (states.every((state) => state.truncated))
1434
+ break;
1435
+ }
1436
+ return {
1437
+ results: states.map(({ needle: _needle, ...state }) => state)
1438
+ };
1439
+ }
1150
1440
  audit(entry) {
1151
1441
  const record = { id: randomUUID(), timestamp: new Date().toISOString(), ...entry };
1152
1442
  this.auditEntries.push(record);
@@ -1310,6 +1600,7 @@ class Workspace {
1310
1600
  throw new AggregateError([error, ...rollbackErrors], "Patch failed and rollback was incomplete.");
1311
1601
  throw error;
1312
1602
  }
1603
+ this.invalidateWorkspaceIndex();
1313
1604
  const changes = prepared.map((item) => this.audit({
1314
1605
  operation: item.before ? "update" : "create",
1315
1606
  path: item.change.path,
@@ -1370,6 +1661,7 @@ class Workspace {
1370
1661
  await unlink(destination.path).catch(() => {});
1371
1662
  throw error;
1372
1663
  }
1664
+ this.invalidateWorkspaceIndex();
1373
1665
  const audit = this.audit({ operation: "move", path: parsed.source, destination: parsed.destination, beforeDigest: source.digest, afterDigest: source.digest });
1374
1666
  return { source: parsed.source, destination: parsed.destination, digest: source.digest, audit };
1375
1667
  }
@@ -1458,6 +1750,7 @@ class Workspace {
1458
1750
  await unlink(path3.join(directory, `${quarantineId}.json`)).catch(() => {});
1459
1751
  throw error;
1460
1752
  }
1753
+ this.invalidateWorkspaceIndex();
1461
1754
  const audit = this.audit({ operation: "quarantine", path: parsed.path, beforeDigest: source.digest, quarantineId });
1462
1755
  return { quarantineId, path: parsed.path, digest: source.digest, audit };
1463
1756
  }
@@ -1526,6 +1819,7 @@ class Workspace {
1526
1819
  }
1527
1820
  throw rollbackErrors.length ? new AggregateError([error, ...rollbackErrors], "Restore failed and rollback was incomplete.") : error;
1528
1821
  }
1822
+ this.invalidateWorkspaceIndex();
1529
1823
  const audit = this.audit({ operation: "restore", path: destinationPath, afterDigest: digest2, quarantineId: parsed.quarantineId });
1530
1824
  return { quarantineId: parsed.quarantineId, path: destinationPath, digest: digest2, audit };
1531
1825
  }
@@ -1546,7 +1840,11 @@ class Workspace {
1546
1840
  throw new Error(`package.json does not define the "${check}" script.`);
1547
1841
  if (scripts[check] !== expectedScript)
1548
1842
  throw new Error(`The "${check}" script changed or does not match expectedScript.`);
1549
- return spawnBounded(["bun", "--no-env-file", "run", check], this.root);
1843
+ try {
1844
+ return await spawnBounded(["bun", "--no-env-file", "run", check], this.root);
1845
+ } finally {
1846
+ this.invalidateWorkspaceIndex();
1847
+ }
1550
1848
  }
1551
1849
  async gitDiff() {
1552
1850
  const [statusResult, diffResult, stagedResult] = await Promise.all([
@@ -1571,7 +1869,9 @@ var TERMINAL_STATUSES = new Set([
1571
1869
  var BUILT_IN_TOOL_NAMES = new Set([
1572
1870
  "list_files",
1573
1871
  "read_file",
1872
+ "read_files",
1574
1873
  "search_files",
1874
+ "search_many",
1575
1875
  "propose_edits",
1576
1876
  "apply_patch",
1577
1877
  "move_file",
@@ -1695,11 +1995,26 @@ var parseRuntimeVersion = (stdout, runtime) => {
1695
1995
  var validContainerIds = (stdout) => stdout.split(/\r?\n/).map((value) => value.trim()).filter((value) => /^[a-f0-9]{12,64}$/.test(value));
1696
1996
  var validVolumeNames = (stdout) => stdout.split(/\r?\n/).map((value) => value.trim()).filter((value) => /^zhivex-harness-[A-Za-z0-9_.-]+$/.test(value));
1697
1997
  var MAX_SYNC_ENTRIES = 1e4;
1998
+ var updateSnapshotMetadata = (target, kind, relative, entry) => {
1999
+ target.update([
2000
+ kind,
2001
+ relative,
2002
+ entry.dev,
2003
+ entry.ino,
2004
+ entry.size,
2005
+ entry.mtimeMs,
2006
+ entry.ctimeMs,
2007
+ entry.mode & 511
2008
+ ].join("\x00"));
2009
+ target.update("\x00");
2010
+ };
1698
2011
  var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspaceBytes, maxFileWriteBytes) => {
1699
2012
  let entries = 0;
1700
2013
  let totalBytes = 0;
2014
+ const seal = createHash5("sha256");
2015
+ const metadata = createHash5("sha256");
1701
2016
  const visit = async (directory, relativeDirectory = "") => {
1702
- for (const name of await readdir2(directory)) {
2017
+ for (const name of (await readdir2(directory)).sort()) {
1703
2018
  const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
1704
2019
  const absolute = path4.join(directory, name);
1705
2020
  if (!isInside2(stagedRoot, absolute) || relative.length > 1024) {
@@ -1718,22 +2033,32 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
1718
2033
  throw new Error(`OCI workspace export contains a symbolic link: ${relative}.`);
1719
2034
  }
1720
2035
  if (entry.isDirectory()) {
2036
+ updateSnapshotMetadata(metadata, "directory", relative, entry);
2037
+ seal.update(`directory\x00${relative}\x00${entry.mode & 511}\x00`);
1721
2038
  await visit(absolute, relative);
1722
2039
  continue;
1723
2040
  }
1724
2041
  if (!entry.isFile() || entry.nlink !== 1) {
1725
2042
  throw new Error(`OCI workspace export contains a special or linked file: ${relative}.`);
1726
2043
  }
2044
+ const contents = await readFile2(absolute);
2045
+ const after = await lstat2(absolute);
2046
+ 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)) {
2047
+ throw new Error(`OCI workspace export changed while it was being validated: ${relative}.`);
2048
+ }
1727
2049
  totalBytes += entry.size;
1728
2050
  if (totalBytes > maxWorkspaceBytes) {
1729
2051
  throw new Error(`OCI workspace export exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
1730
2052
  }
2053
+ const contentDigest = digest2(contents);
2054
+ updateSnapshotMetadata(metadata, "file", relative, after);
2055
+ seal.update(`file\x00${relative}\x00${entry.mode & 511}\x00${entry.size}\x00${contentDigest}\x00`);
1731
2056
  if (entry.size > maxFileWriteBytes) {
1732
2057
  const previous = path4.join(previousRoot, ...relative.split("/"));
1733
2058
  let unchanged = false;
1734
2059
  try {
1735
2060
  const previousEntry = await lstat2(previous);
1736
- unchanged = previousEntry.isFile() && !previousEntry.isSymbolicLink() && previousEntry.size === entry.size && digest2(await readFile2(previous)) === digest2(await readFile2(absolute));
2061
+ unchanged = previousEntry.isFile() && !previousEntry.isSymbolicLink() && previousEntry.size === entry.size && digest2(await readFile2(previous)) === contentDigest;
1737
2062
  } catch (error) {
1738
2063
  if (error.code !== "ENOENT")
1739
2064
  throw error;
@@ -1745,6 +2070,51 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
1745
2070
  }
1746
2071
  };
1747
2072
  await visit(stagedRoot);
2073
+ return {
2074
+ entries,
2075
+ totalBytes,
2076
+ seal: `sha256:${seal.digest("hex")}`,
2077
+ metadataFingerprint: `sha256:${metadata.digest("hex")}`
2078
+ };
2079
+ };
2080
+ var snapshotMetadataFingerprint = async (root, maxWorkspaceBytes) => {
2081
+ let entries = 0;
2082
+ let totalBytes = 0;
2083
+ const metadata = createHash5("sha256");
2084
+ const visit = async (directory, relativeDirectory = "") => {
2085
+ for (const name of (await readdir2(directory)).sort()) {
2086
+ const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
2087
+ if (relative === "node_modules")
2088
+ continue;
2089
+ const absolute = path4.join(directory, name);
2090
+ if (!isInside2(root, absolute) || relative.length > 1024) {
2091
+ throw new Error(`OCI workspace snapshot contains an invalid path: ${relative}.`);
2092
+ }
2093
+ entries += 1;
2094
+ if (entries > MAX_SYNC_ENTRIES) {
2095
+ throw new Error(`OCI workspace snapshot exceeds the ${MAX_SYNC_ENTRIES}-entry limit.`);
2096
+ }
2097
+ const entry = await lstat2(absolute);
2098
+ if (entry.isSymbolicLink()) {
2099
+ throw new Error(`OCI workspace snapshot contains a symbolic link: ${relative}.`);
2100
+ }
2101
+ if (entry.isDirectory()) {
2102
+ updateSnapshotMetadata(metadata, "directory", relative, entry);
2103
+ await visit(absolute, relative);
2104
+ continue;
2105
+ }
2106
+ if (!entry.isFile() || entry.nlink !== 1) {
2107
+ throw new Error(`OCI workspace snapshot contains a special or linked file: ${relative}.`);
2108
+ }
2109
+ totalBytes += entry.size;
2110
+ if (totalBytes > maxWorkspaceBytes) {
2111
+ throw new Error(`OCI workspace snapshot exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
2112
+ }
2113
+ updateSnapshotMetadata(metadata, "file", relative, entry);
2114
+ }
2115
+ };
2116
+ await visit(root);
2117
+ return `sha256:${metadata.digest("hex")}`;
1748
2118
  };
1749
2119
  var replaceSnapshot = async (snapshotRoot, stagedRoot) => {
1750
2120
  const backupRoot = `${snapshotRoot}.${randomUUID2()}.previous`;
@@ -1759,10 +2129,42 @@ var replaceSnapshot = async (snapshotRoot, stagedRoot) => {
1759
2129
  }
1760
2130
  await rm(backupRoot, { recursive: true, force: true });
1761
2131
  };
2132
+ var OCI_WORKSPACE_SEAL_SCRIPT = [
2133
+ "import { createHash } from 'node:crypto';",
2134
+ "import { lstat, readFile, readdir, readlink, rm } from 'node:fs/promises';",
2135
+ "import path from 'node:path';",
2136
+ "const root='/workspace';",
2137
+ "const seal=createHash('sha256');",
2138
+ "const modules=path.join(root,'node_modules');",
2139
+ "if(process.env.ZHIVEX_HARNESS_HAS_DEPENDENCIES==='1'){const entry=await lstat(modules);if(!entry.isSymbolicLink()||(await readlink(modules))!=='/dependencies')throw new Error('read-only dependency mount was replaced');}else{await rm(modules,{recursive:true,force:true});}",
2140
+ "const digest=(value)=>`sha256:${createHash('sha256').update(value).digest('hex')}`;",
2141
+ "const same=(before,after)=>before.dev===after.dev&&before.ino===after.ino&&before.size===after.size&&before.mtimeMs===after.mtimeMs&&before.ctimeMs===after.ctimeMs&&(before.mode&0o777)===(after.mode&0o777);",
2142
+ "const visit=async(directory,base='')=>{",
2143
+ "for(const name of (await readdir(directory)).sort()){",
2144
+ "const relative=base?`${base}/${name}`:name;",
2145
+ "if(relative==='node_modules')continue;",
2146
+ "const absolute=path.join(directory,name);",
2147
+ "const entry=await lstat(absolute);",
2148
+ "if(entry.isSymbolicLink())throw new Error(`symbolic link: ${relative}`);",
2149
+ "if(entry.isDirectory()){seal.update(`directory\\0${relative}\\0${entry.mode&0o777}\\0`);await visit(absolute,relative);continue;}",
2150
+ "if(!entry.isFile()||entry.nlink!==1)throw new Error(`special or linked file: ${relative}`);",
2151
+ "const contents=await readFile(absolute);",
2152
+ "const after=await lstat(absolute);",
2153
+ "if(!same(entry,after))throw new Error(`changed while sealing: ${relative}`);",
2154
+ "seal.update(`file\\0${relative}\\0${entry.mode&0o777}\\0${entry.size}\\0${digest(contents)}\\0`);",
2155
+ "}",
2156
+ "};",
2157
+ "await visit(root);",
2158
+ "for(const name of await readdir('/tmp'))await rm(path.join('/tmp',name),{recursive:true,force:true});",
2159
+ "console.log(`sha256:${seal.digest('hex')}`);"
2160
+ ].join("");
2161
+ var OCI_SESSION_CONTROLLER_SCRIPT = "setInterval(()=>{},2147483647);";
1762
2162
 
1763
2163
  class CliOciRuntimeAdapter {
1764
2164
  runtime;
1765
2165
  hostEnvironment;
2166
+ sessions = new Map;
2167
+ runQueues = new Map;
1766
2168
  constructor(runtime) {
1767
2169
  this.runtime = runtime;
1768
2170
  this.hostEnvironment = Object.fromEntries(["DOCKER_CONFIG", "DOCKER_CONTEXT", "DOCKER_HOST", "HOME", "PATH", "XDG_RUNTIME_DIR"].flatMap((name) => process.env[name] ? [[name, process.env[name]]] : []));
@@ -1815,10 +2217,44 @@ ${result.stdout}`)) {
1815
2217
  throw new Error(`${this.runtime} could not remove OCI workspace volume ${name}: ${result.stderr.trim()}`);
1816
2218
  }
1817
2219
  }
1818
- async run(request) {
2220
+ requestFingerprint(request) {
2221
+ return digest2(JSON.stringify({
2222
+ runId: request.runId,
2223
+ snapshotRoot: request.snapshotRoot,
2224
+ dependencyRoot: request.dependencyRoot ?? null,
2225
+ imageId: request.imageId,
2226
+ limits: request.limits
2227
+ }));
2228
+ }
2229
+ async withRunLock(runId, operation) {
2230
+ const key = runHash(runId);
2231
+ const previous = this.runQueues.get(key) ?? Promise.resolve();
2232
+ let release;
2233
+ const gate = new Promise((resolve) => {
2234
+ release = resolve;
2235
+ });
2236
+ const tail = previous.then(() => gate, () => gate);
2237
+ this.runQueues.set(key, tail);
2238
+ await previous.catch(() => {
2239
+ return;
2240
+ });
2241
+ try {
2242
+ return await operation();
2243
+ } finally {
2244
+ release();
2245
+ if (this.runQueues.get(key) === tail)
2246
+ this.runQueues.delete(key);
2247
+ }
2248
+ }
2249
+ async destroySession(session) {
2250
+ if (this.sessions.get(session.key) === session)
2251
+ this.sessions.delete(session.key);
2252
+ await this.forceRemove(session.name);
2253
+ await this.removeVolume(session.volumeName);
2254
+ }
2255
+ async createSession(request, requestFingerprint, snapshotSeal, hostFingerprint) {
1819
2256
  const resourceName = `zhivex-harness-${runHash(request.runId)}-${randomUUID2().slice(0, 8)}`;
1820
2257
  const name = `${resourceName}-job`;
1821
- const exportName = `${resourceName}-export`;
1822
2258
  const volumeName = `${resourceName}-workspace`;
1823
2259
  const uid = process.getuid?.() ?? 65532;
1824
2260
  const gid = process.getgid?.() ?? 65532;
@@ -1874,8 +2310,6 @@ ${result.stdout}`)) {
1874
2310
  "BUN_INSTALL_CACHE_DIR=/tmp/bun-cache",
1875
2311
  "--env",
1876
2312
  "CI=1",
1877
- "--volume",
1878
- `${request.snapshotRoot}:/seed:ro`,
1879
2313
  ...request.dependencyRoot ? ["--volume", `${request.dependencyRoot}:/dependencies:ro`] : [],
1880
2314
  "--workdir",
1881
2315
  "/workspace",
@@ -1883,26 +2317,19 @@ ${result.stdout}`)) {
1883
2317
  "bun",
1884
2318
  request.imageId,
1885
2319
  "-e",
1886
- `await Bun.sleep(${request.limits.maxProcessRuntimeMs + 60000})`
2320
+ OCI_SESSION_CONTROLLER_SCRIPT
1887
2321
  ];
1888
- let forcedRemoval;
1889
- const forceRemove = () => {
1890
- forcedRemoval ??= this.forceRemove(name);
1891
- };
2322
+ let volumeCreated = false;
1892
2323
  try {
1893
2324
  await this.cli(volumeArgs);
2325
+ volumeCreated = true;
1894
2326
  await this.cli(args);
1895
2327
  await this.cli(["start", name]);
1896
2328
  await this.cli([
1897
- "exec",
1898
- "--workdir",
1899
- "/workspace",
1900
- "--user",
1901
- `${uid}:${gid}`,
1902
- name,
1903
- "bun",
1904
- "-e",
1905
- "import { cp } from 'node:fs/promises'; await cp('/seed', '/workspace', { recursive: true, force: false, errorOnExist: true })"
2329
+ "cp",
2330
+ "-a",
2331
+ `${request.snapshotRoot}${path4.sep}.`,
2332
+ `${name}:/workspace`
1906
2333
  ], 60000);
1907
2334
  if (request.dependencyRoot) {
1908
2335
  await this.cli([
@@ -1917,88 +2344,183 @@ ${result.stdout}`)) {
1917
2344
  "import { symlink } from 'node:fs/promises'; await symlink('/dependencies', '/workspace/node_modules', 'dir')"
1918
2345
  ]);
1919
2346
  }
1920
- const result = await spawnBounded2([
1921
- this.runtime,
2347
+ await this.cli(["pause", name]);
2348
+ const session = {
2349
+ key: runHash(request.runId),
2350
+ requestFingerprint,
2351
+ name,
2352
+ volumeName,
2353
+ snapshotSeal,
2354
+ hostFingerprint,
2355
+ paused: true
2356
+ };
2357
+ this.sessions.set(session.key, session);
2358
+ return session;
2359
+ } catch (error) {
2360
+ await this.forceRemove(name);
2361
+ if (volumeCreated)
2362
+ await this.removeVolume(volumeName).catch(() => {
2363
+ return;
2364
+ });
2365
+ throw error;
2366
+ }
2367
+ }
2368
+ async onlyControllerProcessRemains(session) {
2369
+ const result = await this.cli(this.runtime === "podman" ? ["top", session.name, "pid"] : ["top", session.name, "-eo", "pid"]);
2370
+ const processIds = result.stdout.split(/\r?\n/).filter((line) => /^\s*\d+\s*$/.test(line));
2371
+ return processIds.length === 1;
2372
+ }
2373
+ async containerWorkspaceSeal(session, uid, gid, hasDependencies) {
2374
+ if (session.paused) {
2375
+ await this.cli(["unpause", session.name]);
2376
+ session.paused = false;
2377
+ }
2378
+ try {
2379
+ const result = await this.cli([
1922
2380
  "exec",
1923
2381
  "--workdir",
1924
2382
  "/workspace",
1925
2383
  "--user",
1926
2384
  `${uid}:${gid}`,
1927
- name,
1928
- ...request.command
1929
- ], {
1930
- env: this.hostEnvironment,
1931
- timeoutMs: request.limits.maxProcessRuntimeMs,
1932
- maxOutputBytes: request.limits.maxProcessOutputBytes,
1933
- ...request.abortSignal ? { abortSignal: request.abortSignal } : {},
1934
- onStop: forceRemove
1935
- });
1936
- if (result.timedOut || result.cancelled || result.outputLimitExceeded) {
1937
- forceRemove();
1938
- await forcedRemoval;
1939
- } else if (result.exitCode === 0) {
1940
- await this.cli(["pause", name]);
1941
- const stagedRoot = path4.join(path4.dirname(request.snapshotRoot), `.workspace-sync-${randomUUID2()}`);
1942
- try {
1943
- await privateDirectory(stagedRoot);
1944
- try {
1945
- await this.cli([
1946
- "run",
1947
- "--rm",
1948
- "--name",
1949
- exportName,
1950
- "--label",
1951
- `${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`,
1952
- "--label",
1953
- `com.zhivex.harness.run=${runHash(request.runId)}`,
1954
- "--network",
1955
- "none",
1956
- "--read-only",
1957
- "--cap-drop",
1958
- "ALL",
1959
- "--security-opt",
1960
- "no-new-privileges",
1961
- "--pids-limit",
1962
- "32",
1963
- "--memory",
1964
- "256m",
1965
- "--cpus",
1966
- "1",
1967
- "--tmpfs",
1968
- "/tmp:rw,noexec,nosuid,nodev,size=16m",
1969
- "--user",
1970
- `${uid}:${gid}`,
1971
- "--volume",
1972
- `${volumeName}:/workspace:ro`,
1973
- "--volume",
1974
- `${stagedRoot}:/export:rw`,
1975
- "--workdir",
1976
- "/workspace",
1977
- "--entrypoint",
1978
- "bun",
1979
- request.imageId,
1980
- "-e",
1981
- "import { cp } from 'node:fs/promises'; await cp('/workspace', '/export', { recursive: true, force: false, errorOnExist: true })"
1982
- ], 60000);
1983
- } finally {
1984
- await this.forceRemove(exportName);
1985
- }
1986
- await validateSynchronizedSnapshot(stagedRoot, request.snapshotRoot, request.limits.maxWorkspaceBytes, request.limits.maxFileWriteBytes);
1987
- await replaceSnapshot(request.snapshotRoot, stagedRoot);
1988
- } finally {
1989
- await rm(stagedRoot, { recursive: true, force: true });
1990
- }
2385
+ "--env",
2386
+ `ZHIVEX_HARNESS_HAS_DEPENDENCIES=${hasDependencies ? "1" : "0"}`,
2387
+ session.name,
2388
+ "bun",
2389
+ "-e",
2390
+ OCI_WORKSPACE_SEAL_SCRIPT
2391
+ ], 60000, 20000);
2392
+ const seal = result.stdout.trim();
2393
+ if (!/^sha256:[a-f0-9]{64}$/.test(seal)) {
2394
+ throw new Error("OCI workspace seal helper returned an invalid digest.");
1991
2395
  }
1992
- return {
1993
- ...result,
1994
- command: request.command
1995
- };
2396
+ return seal;
1996
2397
  } finally {
2398
+ if (!session.paused) {
2399
+ await this.cli(["pause", session.name]);
2400
+ session.paused = true;
2401
+ }
2402
+ }
2403
+ }
2404
+ async runInSession(request, session, sessionReused) {
2405
+ const uid = process.getuid?.() ?? 65532;
2406
+ const gid = process.getgid?.() ?? 65532;
2407
+ if (session.paused) {
2408
+ await this.cli(["unpause", session.name]);
2409
+ session.paused = false;
2410
+ }
2411
+ let forcedRemoval;
2412
+ const forceRemove = () => {
2413
+ forcedRemoval ??= this.forceRemove(session.name);
2414
+ };
2415
+ const result = await spawnBounded2([
2416
+ this.runtime,
2417
+ "exec",
2418
+ "--workdir",
2419
+ "/workspace",
2420
+ "--user",
2421
+ `${uid}:${gid}`,
2422
+ session.name,
2423
+ ...request.command
2424
+ ], {
2425
+ env: this.hostEnvironment,
2426
+ timeoutMs: request.limits.maxProcessRuntimeMs,
2427
+ maxOutputBytes: request.limits.maxProcessOutputBytes,
2428
+ ...request.abortSignal ? { abortSignal: request.abortSignal } : {},
2429
+ onStop: forceRemove
2430
+ });
2431
+ if (result.exitCode !== 0 || result.timedOut || result.cancelled || result.outputLimitExceeded) {
1997
2432
  forceRemove();
1998
2433
  await forcedRemoval;
1999
- await this.removeVolume(volumeName);
2434
+ await this.destroySession(session);
2435
+ return {
2436
+ ...result,
2437
+ command: request.command,
2438
+ sessionReused,
2439
+ workspacePublished: false,
2440
+ workspaceExported: false
2441
+ };
2442
+ }
2443
+ try {
2444
+ if (!await this.onlyControllerProcessRemains(session)) {
2445
+ await this.destroySession(session);
2446
+ return {
2447
+ ...result,
2448
+ command: request.command,
2449
+ exitCode: 126,
2450
+ stderr: [
2451
+ result.stderr,
2452
+ "OCI command left background processes running; its workspace changes were discarded."
2453
+ ].filter(Boolean).join(`
2454
+ `),
2455
+ sessionReused,
2456
+ workspacePublished: false,
2457
+ workspaceExported: false
2458
+ };
2459
+ }
2460
+ const workspaceSeal = await this.containerWorkspaceSeal(session, uid, gid, Boolean(request.dependencyRoot));
2461
+ if (workspaceSeal === session.snapshotSeal) {
2462
+ return {
2463
+ ...result,
2464
+ command: request.command,
2465
+ sessionReused,
2466
+ workspacePublished: true,
2467
+ workspaceExported: false
2468
+ };
2469
+ }
2470
+ const stagedRoot = path4.join(path4.dirname(request.snapshotRoot), `.workspace-sync-${randomUUID2()}`);
2471
+ try {
2472
+ await privateDirectory(stagedRoot);
2473
+ await this.cli(["cp", `${session.name}:/workspace/.`, stagedRoot], 60000);
2474
+ const validated = await validateSynchronizedSnapshot(stagedRoot, request.snapshotRoot, request.limits.maxWorkspaceBytes, request.limits.maxFileWriteBytes);
2475
+ await replaceSnapshot(request.snapshotRoot, stagedRoot);
2476
+ session.snapshotSeal = validated.seal;
2477
+ session.hostFingerprint = validated.metadataFingerprint;
2478
+ } finally {
2479
+ await rm(stagedRoot, { recursive: true, force: true });
2480
+ }
2481
+ return {
2482
+ ...result,
2483
+ command: request.command,
2484
+ sessionReused,
2485
+ workspacePublished: true,
2486
+ workspaceExported: true
2487
+ };
2488
+ } catch (error) {
2489
+ await this.destroySession(session);
2490
+ throw error;
2000
2491
  }
2001
2492
  }
2493
+ async runLocked(request) {
2494
+ const key = runHash(request.runId);
2495
+ const requestFingerprint = this.requestFingerprint(request);
2496
+ let session = this.sessions.get(key);
2497
+ let sessionReused = Boolean(session);
2498
+ let validated;
2499
+ if (session) {
2500
+ const hostFingerprint = await snapshotMetadataFingerprint(request.snapshotRoot, request.limits.maxWorkspaceBytes);
2501
+ if (session.requestFingerprint !== requestFingerprint || session.hostFingerprint !== hostFingerprint) {
2502
+ await this.destroySession(session);
2503
+ session = undefined;
2504
+ sessionReused = false;
2505
+ }
2506
+ }
2507
+ if (!session) {
2508
+ validated = await validateSynchronizedSnapshot(request.snapshotRoot, request.snapshotRoot, request.limits.maxWorkspaceBytes, request.limits.maxFileWriteBytes);
2509
+ session = await this.createSession(request, requestFingerprint, validated.seal, validated.metadataFingerprint);
2510
+ }
2511
+ try {
2512
+ return await this.runInSession(request, session, sessionReused);
2513
+ } catch (error) {
2514
+ if (this.sessions.get(key) === session)
2515
+ await this.destroySession(session).catch(() => {
2516
+ return;
2517
+ });
2518
+ throw error;
2519
+ }
2520
+ }
2521
+ async run(request) {
2522
+ return this.withRunLock(request.runId, () => this.runLocked(request));
2523
+ }
2002
2524
  async removeByFilters(filters) {
2003
2525
  const listed = await spawnBounded2([this.runtime, "ps", "-aq", ...filters.flatMap((filter) => ["--filter", filter])], {
2004
2526
  env: this.hostEnvironment,
@@ -2044,15 +2566,23 @@ ${result.stdout}`)) {
2044
2566
  return removed;
2045
2567
  }
2046
2568
  async removeRunContainers(runId) {
2569
+ const key = runHash(runId);
2570
+ const session = this.sessions.get(key);
2571
+ let cachedResources = 0;
2572
+ if (session) {
2573
+ await this.destroySession(session);
2574
+ cachedResources = 2;
2575
+ }
2047
2576
  const filters = [
2048
2577
  `label=${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`,
2049
- `label=com.zhivex.harness.run=${runHash(runId)}`
2578
+ `label=com.zhivex.harness.run=${key}`
2050
2579
  ];
2051
2580
  const containers = await this.removeByFilters(filters);
2052
2581
  const volumes = await this.removeVolumesByFilters(filters);
2053
- return containers + volumes;
2582
+ return cachedResources + containers + volumes;
2054
2583
  }
2055
2584
  async cleanupOrphans() {
2585
+ this.sessions.clear();
2056
2586
  let removed = 0;
2057
2587
  for (const status of ["created", "exited", "dead", "paused"]) {
2058
2588
  removed += await this.removeByFilters([
@@ -2066,34 +2596,81 @@ ${result.stdout}`)) {
2066
2596
  return removed;
2067
2597
  }
2068
2598
  }
2069
- var collectSnapshotFiles = async (workspace) => {
2599
+ var SNAPSHOT_INVENTORY_PAGE_SIZE = 5000;
2600
+ var COPY_ON_WRITE_UNSUPPORTED_CODES = new Set(["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP"]);
2601
+ var collectSnapshotInventory = async (workspace, metrics) => {
2070
2602
  const files = new Map;
2603
+ if (metrics)
2604
+ metrics.inventoryPasses += 1;
2071
2605
  let cursor;
2072
2606
  do {
2073
- const page = await workspace.listFiles(".", { limit: 500, ...cursor ? { cursor } : {} });
2607
+ const page = await workspace.listFiles(".", {
2608
+ limit: SNAPSHOT_INVENTORY_PAGE_SIZE,
2609
+ ...cursor ? { cursor } : {}
2610
+ });
2611
+ if (metrics)
2612
+ metrics.inventoryPages += 1;
2074
2613
  for (const file of page.files) {
2075
2614
  const absolute = path4.join(workspace.root, ...file.path.split("/"));
2076
2615
  if (!isInside2(workspace.root, absolute))
2077
2616
  throw new Error(`Snapshot file escaped its workspace: ${file.path}.`);
2078
- const before = await lstat2(absolute);
2079
- if (before.isSymbolicLink() || !before.isFile())
2617
+ const entry = await lstat2(absolute);
2618
+ if (entry.isSymbolicLink() || !entry.isFile()) {
2080
2619
  throw new Error(`Snapshot source is not a regular file: ${file.path}.`);
2081
- const contents = await readFile2(absolute);
2082
- const after = await lstat2(absolute);
2083
- if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
2084
- throw new Error(`Snapshot source changed while reading: ${file.path}.`);
2085
2620
  }
2086
- if (digest2(contents) !== file.digest)
2087
- throw new Error(`Snapshot digest changed while reading: ${file.path}.`);
2088
- files.set(file.path, { path: file.path, digest: file.digest, contents, mode: before.mode & 511 });
2621
+ if (entry.size !== file.size) {
2622
+ throw new Error(`Snapshot source changed while inventorying: ${file.path}.`);
2623
+ }
2624
+ files.set(file.path, {
2625
+ path: file.path,
2626
+ digest: file.digest,
2627
+ mode: entry.mode & 511,
2628
+ bytes: file.size
2629
+ });
2089
2630
  }
2090
2631
  cursor = page.nextCursor;
2091
2632
  } while (cursor);
2092
2633
  return files;
2093
2634
  };
2094
- var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceBytes) => {
2095
- const files = await collectSnapshotFiles(source);
2096
- const totalBytes = [...files.values()].reduce((total, file) => total + file.contents.byteLength, 0);
2635
+ var readSnapshotFile = async (workspace, expected, metrics) => {
2636
+ const absolute = path4.join(workspace.root, ...expected.path.split("/"));
2637
+ if (!isInside2(workspace.root, absolute)) {
2638
+ throw new Error(`Snapshot file escaped its workspace: ${expected.path}.`);
2639
+ }
2640
+ const before = await lstat2(absolute);
2641
+ if (before.isSymbolicLink() || !before.isFile()) {
2642
+ throw new Error(`Snapshot source is not a regular file: ${expected.path}.`);
2643
+ }
2644
+ const contents = await readFile2(absolute);
2645
+ const after = await lstat2(absolute);
2646
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || (before.mode & 511) !== (after.mode & 511)) {
2647
+ throw new Error(`Snapshot source changed while reading: ${expected.path}.`);
2648
+ }
2649
+ const actualDigest = digest2(contents);
2650
+ if (actualDigest !== expected.digest || contents.byteLength !== expected.bytes || (before.mode & 511) !== expected.mode) {
2651
+ throw new Error(`Snapshot source changed after inventory: ${expected.path}.`);
2652
+ }
2653
+ if (metrics) {
2654
+ metrics.verifiedContentReads += 1;
2655
+ metrics.verifiedContentBytes += contents.byteLength;
2656
+ }
2657
+ return { ...expected, contents };
2658
+ };
2659
+ var copySnapshotFile = async (source, target, metrics) => {
2660
+ try {
2661
+ await copyFile(source, target, fsConstants.COPYFILE_FICLONE_FORCE);
2662
+ } catch (error) {
2663
+ const code = error.code;
2664
+ if (!code || !COPY_ON_WRITE_UNSUPPORTED_CODES.has(code))
2665
+ throw error;
2666
+ if (metrics)
2667
+ metrics.snapshotCloneFallbacks += 1;
2668
+ await copyFile(source, target);
2669
+ }
2670
+ };
2671
+ var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceBytes, metrics) => {
2672
+ const files = await collectSnapshotInventory(source, metrics);
2673
+ const totalBytes = [...files.values()].reduce((total, file) => total + file.bytes, 0);
2097
2674
  if (totalBytes > maxWorkspaceBytes) {
2098
2675
  throw new Error(`Workspace snapshot exceeds the ${maxWorkspaceBytes}-byte OCI limit.`);
2099
2676
  }
@@ -2101,13 +2678,22 @@ var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceB
2101
2678
  await rm(root, { recursive: true, force: true });
2102
2679
  await privateDirectory(root);
2103
2680
  }
2104
- for (const file of files.values()) {
2105
- for (const root of [baseRoot, snapshotRoot]) {
2106
- const target = path4.join(root, ...file.path.split("/"));
2107
- await mkdir2(path4.dirname(target), { recursive: true, mode: 448 });
2108
- await writeFile(target, file.contents, { mode: file.mode });
2109
- await chmod(target, file.mode);
2110
- }
2681
+ for (const expected of files.values()) {
2682
+ const file = await readSnapshotFile(source, expected, metrics);
2683
+ const baseTarget = path4.join(baseRoot, ...file.path.split("/"));
2684
+ const snapshotTarget = path4.join(snapshotRoot, ...file.path.split("/"));
2685
+ await Promise.all([
2686
+ mkdir2(path4.dirname(baseTarget), { recursive: true, mode: 448 }),
2687
+ mkdir2(path4.dirname(snapshotTarget), { recursive: true, mode: 448 })
2688
+ ]);
2689
+ await writeFile(baseTarget, file.contents, { mode: file.mode });
2690
+ await chmod(baseTarget, file.mode);
2691
+ await copySnapshotFile(baseTarget, snapshotTarget, metrics);
2692
+ await chmod(snapshotTarget, file.mode);
2693
+ }
2694
+ if (metrics) {
2695
+ metrics.snapshotFiles += files.size;
2696
+ metrics.snapshotBytes += totalBytes;
2111
2697
  }
2112
2698
  return { files: files.size, bytes: totalBytes };
2113
2699
  };
@@ -2126,9 +2712,11 @@ var textContent = (file, maxFileWriteBytes) => {
2126
2712
  }
2127
2713
  return file.contents.toString("utf8");
2128
2714
  };
2129
- var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes) => {
2130
- const before = await collectSnapshotFiles(base);
2131
- const after = await collectSnapshotFiles(current);
2715
+ var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes, metrics) => {
2716
+ const [before, after] = await Promise.all([
2717
+ collectSnapshotInventory(base, metrics),
2718
+ collectSnapshotInventory(current, metrics)
2719
+ ]);
2132
2720
  const paths = [...new Set([...before.keys(), ...after.keys()])].sort();
2133
2721
  const entries = [];
2134
2722
  let totalBytes = 0;
@@ -2138,9 +2726,9 @@ var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes) =>
2138
2726
  if (oldFile?.digest === newFile?.digest && oldFile?.mode === newFile?.mode)
2139
2727
  continue;
2140
2728
  const operation = !oldFile ? "create" : !newFile ? "delete" : "update";
2141
- const beforeContent = oldFile ? textContent(oldFile, maxFileWriteBytes) : undefined;
2142
- const afterContent = newFile ? textContent(newFile, maxFileWriteBytes) : undefined;
2143
- const bytes = newFile?.contents.byteLength ?? 0;
2729
+ const beforeContent = oldFile ? textContent(await readSnapshotFile(base, oldFile, metrics), maxFileWriteBytes) : undefined;
2730
+ const afterContent = newFile ? textContent(await readSnapshotFile(current, newFile, metrics), maxFileWriteBytes) : undefined;
2731
+ const bytes = newFile?.bytes ?? 0;
2144
2732
  totalBytes += bytes;
2145
2733
  entries.push({
2146
2734
  path: filePath,
@@ -2180,8 +2768,8 @@ var inspectHostPrecondition = async (workspace, filePath, expectedDigest, expect
2180
2768
  throw error;
2181
2769
  }
2182
2770
  };
2183
- var importPatch = async (runId, expectedPatchId, host, base, current, maxFileWriteBytes) => {
2184
- const patch = await createEnvironmentPatch(runId, base, current, maxFileWriteBytes);
2771
+ var importPatch = async (runId, expectedPatchId, host, base, current, maxFileWriteBytes, metrics) => {
2772
+ const patch = await createEnvironmentPatch(runId, base, current, maxFileWriteBytes, metrics);
2185
2773
  if (patch.patchId !== expectedPatchId) {
2186
2774
  throw new Error("Environment patch changed after review; inspect it again before import.");
2187
2775
  }
@@ -2275,7 +2863,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
2275
2863
  version: `${HARNESS_EXECUTION_POLICY_VERSION}:${image.imageDigest}`,
2276
2864
  backend: "container",
2277
2865
  assurance: "enforced",
2278
- isolation: "per-tool-call",
2866
+ isolation: "per-run",
2279
2867
  workspace: {
2280
2868
  id: workspaceIdentity,
2281
2869
  root: "/workspace",
@@ -2312,6 +2900,8 @@ var createHarnessOciExecutionEnvironment = async (options) => {
2312
2900
  maxPids: options.config.maxPids,
2313
2901
  maxCpus: options.config.maxCpus,
2314
2902
  tmpfsMb: options.config.tmpfsMb,
2903
+ containerLifecycle: "warm-per-acquired-run",
2904
+ failedCommandRecovery: "discard-and-reseed",
2315
2905
  patchImportApproval: true,
2316
2906
  hostMutationMode: "reviewed-environment-patch"
2317
2907
  }
@@ -2323,6 +2913,19 @@ var createHarnessOciExecutionEnvironment = async (options) => {
2323
2913
  image,
2324
2914
  runtime,
2325
2915
  async acquire(request) {
2916
+ const ioMetrics = {
2917
+ inventoryPasses: 0,
2918
+ inventoryPages: 0,
2919
+ verifiedContentReads: 0,
2920
+ verifiedContentBytes: 0,
2921
+ snapshotFiles: 0,
2922
+ snapshotBytes: 0,
2923
+ snapshotCloneFallbacks: 0,
2924
+ containerStarts: 0,
2925
+ containerReuses: 0,
2926
+ workspacePublishes: 0,
2927
+ workspaceExports: 0
2928
+ };
2326
2929
  await privateDirectory(environmentRoot);
2327
2930
  const directory = path4.join(environmentRoot, runHash(request.runId));
2328
2931
  await privateDirectory(directory);
@@ -2341,7 +2944,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
2341
2944
  } catch (error) {
2342
2945
  if (error.code !== "ENOENT")
2343
2946
  throw error;
2344
- const snapshot = await copyWorkspaceSnapshot(options.workspace, baseRoot, snapshotRoot, options.config.maxWorkspaceBytes);
2947
+ const snapshot = await copyWorkspaceSnapshot(options.workspace, baseRoot, snapshotRoot, options.config.maxWorkspaceBytes, ioMetrics);
2345
2948
  const now = new Date().toISOString();
2346
2949
  metadata = {
2347
2950
  schemaVersion: HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION,
@@ -2398,6 +3001,14 @@ var createHarnessOciExecutionEnvironment = async (options) => {
2398
3001
  },
2399
3002
  ...context?.abortSignal ?? request.abortSignal ? { abortSignal: context?.abortSignal ?? request.abortSignal } : {}
2400
3003
  });
3004
+ if (result.sessionReused === false)
3005
+ ioMetrics.containerStarts += 1;
3006
+ if (result.sessionReused === true)
3007
+ ioMetrics.containerReuses += 1;
3008
+ if (result.workspacePublished === true)
3009
+ ioMetrics.workspacePublishes += 1;
3010
+ if (result.workspaceExported === true)
3011
+ ioMetrics.workspaceExports += 1;
2401
3012
  const forcedExitCode = result.cancelled ? 130 : result.timedOut ? 124 : result.outputLimitExceeded ? 125 : result.exitCode;
2402
3013
  return {
2403
3014
  command: result.command,
@@ -2444,7 +3055,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
2444
3055
  return await operation();
2445
3056
  },
2446
3057
  async status() {
2447
- const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes);
3058
+ const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes, ioMetrics);
2448
3059
  return {
2449
3060
  schemaVersion: HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION,
2450
3061
  kind: "environment-status",
@@ -2458,7 +3069,8 @@ var createHarnessOciExecutionEnvironment = async (options) => {
2458
3069
  rootFilesystem: "read-only",
2459
3070
  workspace: "ephemeral-snapshot",
2460
3071
  patchId: patch.patchId,
2461
- changedFiles: patch.entries.length
3072
+ changedFiles: patch.entries.length,
3073
+ io: { ...ioMetrics }
2462
3074
  };
2463
3075
  },
2464
3076
  runCommand,
@@ -2483,7 +3095,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
2483
3095
  return runCommand("bun", ["--no-env-file", "run", check], context);
2484
3096
  },
2485
3097
  async inspectPatch() {
2486
- const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes);
3098
+ const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes, ioMetrics);
2487
3099
  const payload = patchPayload(request.runId, patch.entries);
2488
3100
  return {
2489
3101
  ...payload,
@@ -2492,7 +3104,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
2492
3104
  };
2493
3105
  },
2494
3106
  importPatch(host, patchId) {
2495
- return importPatch(request.runId, patchId, host, base, workspace, options.config.maxFileWriteBytes);
3107
+ return importPatch(request.runId, patchId, host, base, workspace, options.config.maxFileWriteBytes, ioMetrics);
2496
3108
  },
2497
3109
  async release(result) {
2498
3110
  await runtime.removeRunContainers(request.runId);
@@ -3311,7 +3923,7 @@ var HARNESS_SUBAGENT_PROFILE_DESCRIPTORS = [
3311
3923
  toolName: "delegate_explorer",
3312
3924
  description: "Delegate bounded repository discovery to a read-only explorer.",
3313
3925
  instructions: "Inspect the repository with read-only tools. Return evidence with paths and concrete findings. Never propose or perform mutations.",
3314
- toolNames: ["list_files", "read_file", "search_files", "git_diff"]
3926
+ toolNames: ["list_files", "read_file", "read_files", "search_files", "search_many", "git_diff"]
3315
3927
  },
3316
3928
  {
3317
3929
  id: "implementer",
@@ -3325,14 +3937,14 @@ var HARNESS_SUBAGENT_PROFILE_DESCRIPTORS = [
3325
3937
  toolName: "delegate_tester",
3326
3938
  description: "Delegate focused verification to a tester with read-only inspection and approved Bun checks.",
3327
3939
  instructions: "Verify the delegated behavior with repository evidence and declared Bun checks. Do not edit files. Report exact failures and limits.",
3328
- toolNames: ["list_files", "read_file", "search_files", "run_check", "git_diff"]
3940
+ toolNames: ["list_files", "read_file", "read_files", "search_files", "search_many", "run_check", "git_diff"]
3329
3941
  },
3330
3942
  {
3331
3943
  id: "reviewer",
3332
3944
  toolName: "delegate_reviewer",
3333
3945
  description: "Delegate an independent read-only review of correctness, safety, and regressions.",
3334
3946
  instructions: "Review the requested scope independently. Inspect concrete code and diffs, prioritize actionable defects, and do not mutate the workspace.",
3335
- toolNames: ["list_files", "read_file", "search_files", "mutation_audit", "git_diff"]
3947
+ toolNames: ["list_files", "read_file", "read_files", "search_files", "search_many", "mutation_audit", "git_diff"]
3336
3948
  }
3337
3949
  ];
3338
3950
  var pickTools = (tools, names) => {
@@ -3429,7 +4041,7 @@ var createHarnessSubagents = (options) => {
3429
4041
 
3430
4042
  // src/harness.ts
3431
4043
  var APPROVAL_VERSION = "2026-08-17-v5";
3432
- var TOOL_CONTRACT_VERSION = "enforced-execution-v1";
4044
+ var TOOL_CONTRACT_VERSION = "workspace-batch-v2";
3433
4045
  var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, providerTransportFingerprint, executionEnvironment) => ({
3434
4046
  schemaVersion: 1,
3435
4047
  id: "zhivex-harness",
@@ -3459,7 +4071,7 @@ var HARNESS_INSTRUCTIONS = `You are Zhivex Harness, a provider-portable coding a
3459
4071
 
3460
4072
  Rules:
3461
4073
  - Match the user's language.
3462
- - Inspect the repository before proposing changes. Prefer search_files and read_file over assumptions.
4074
+ - Inspect the repository before proposing changes. Prefer search_many and read_files for independent lookups, then use search_files or read_file when pagination or one focused read is clearer.
3463
4075
  - Use only workspace-relative paths. Never request or expose secrets.
3464
4076
  - Make the smallest coherent change that fully addresses the task.
3465
4077
  - For every file edit, first read its digest and call propose_edits. Apply exactly that reviewed proposal with apply_patch.
@@ -3531,6 +4143,23 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
3531
4143
  metadata: readOnlyMetadata,
3532
4144
  execute: async ({ path: path8, startLine, endLine }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).readFile(path8, startLine, endLine))
3533
4145
  }),
4146
+ read_files: tool({
4147
+ name: "read_files",
4148
+ description: "Read up to 20 independent UTF-8 file slices in one bounded call. Duplicate paths are read once and results use deterministic path/range order.",
4149
+ schema: z3.object({
4150
+ files: z3.array(z3.object({
4151
+ path: z3.string().min(1),
4152
+ startLine: z3.number().int().min(1).default(1),
4153
+ endLine: z3.number().int().min(1).optional()
4154
+ })).min(1).max(20)
4155
+ }),
4156
+ metadata: readOnlyMetadata,
4157
+ execute: async ({ files }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).readFiles(files.map(({ path: path8, startLine, endLine }) => ({
4158
+ path: path8,
4159
+ startLine,
4160
+ ...endLine !== undefined ? { endLine } : {}
4161
+ }))))
4162
+ }),
3534
4163
  search_files: tool({
3535
4164
  name: "search_files",
3536
4165
  description: "Search for a literal string in text files using a stable cursor.",
@@ -3548,6 +4177,28 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
3548
4177
  ...cursor ? { cursor } : {}
3549
4178
  }))
3550
4179
  }),
4180
+ search_many: tool({
4181
+ name: "search_many",
4182
+ description: "Search up to 10 independent literal queries in one workspace pass, with at most 500 aggregate matches.",
4183
+ schema: z3.object({
4184
+ queries: z3.array(z3.object({
4185
+ query: z3.string().min(1).max(200),
4186
+ caseSensitive: z3.boolean().default(false)
4187
+ })).min(1).max(10),
4188
+ path: z3.string().min(1).default("."),
4189
+ limitPerQuery: z3.number().int().min(1).max(500).default(50)
4190
+ }).superRefine((input, context) => {
4191
+ if (input.queries.length * input.limitPerQuery > 500) {
4192
+ context.addIssue({
4193
+ code: "custom",
4194
+ path: ["limitPerQuery"],
4195
+ message: "search_many allows at most 500 aggregate matches."
4196
+ });
4197
+ }
4198
+ }),
4199
+ metadata: readOnlyMetadata,
4200
+ execute: async ({ queries, path: path8, limitPerQuery }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).searchMany(queries, path8, { limitPerQuery }))
4201
+ }),
3551
4202
  propose_edits: tool({
3552
4203
  name: "propose_edits",
3553
4204
  description: "Validate a bounded multi-file edit against current SHA-256 digests and return a deterministic proposalId for operator review. This tool does not write files.",