@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.
package/dist/index.js CHANGED
@@ -359,7 +359,7 @@ var HARNESS_CONFIG_SCHEMA_VERSION = 4;
359
359
  var HARNESS_STORE_BACKENDS = ["sqlite", "file"];
360
360
  var HARNESS_EXECUTION_BACKENDS = ["none", "oci"];
361
361
  var HARNESS_OCI_RUNTIMES = ["docker", "podman"];
362
- var HARNESS_EXECUTION_POLICY_VERSION = "2026-08-17-v1";
362
+ var HARNESS_EXECUTION_POLICY_VERSION = "2026-08-20-v2";
363
363
  var DEFAULT_OCI_EXECUTION = {
364
364
  runtime: "docker",
365
365
  image: "oven/bun:1.3.7-slim",
@@ -1513,8 +1513,10 @@ var openCliSessionStore = async (options) => {
1513
1513
  var openSessionStore = openCliSessionStore;
1514
1514
  // src/execution-environment.ts
1515
1515
  import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
1516
+ import { constants as fsConstants } from "fs";
1516
1517
  import {
1517
1518
  chmod as chmod2,
1519
+ copyFile,
1518
1520
  lstat as lstat4,
1519
1521
  mkdir as mkdir3,
1520
1522
  readFile as readFile2,
@@ -1671,6 +1673,10 @@ var IGNORE_FILES = [".gitignore", ".zhivex-harnessignore"];
1671
1673
  var DEFAULT_CHECKS = ["test", "typecheck", "lint", "build"];
1672
1674
  var MAX_FILE_BYTES = 1024 * 1024;
1673
1675
  var MAX_TOOL_OUTPUT = 20000;
1676
+ var MAX_READ_BATCH_FILES = 20;
1677
+ var MAX_READ_BATCH_BYTES = 2 * MAX_FILE_BYTES;
1678
+ var MAX_SEARCH_MANY_QUERIES = 10;
1679
+ var MAX_SEARCH_MANY_MATCHES = 500;
1674
1680
  var isSensitiveName = (name) => {
1675
1681
  const normalized = name.toLocaleLowerCase();
1676
1682
  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");
@@ -1692,6 +1698,30 @@ var cursorDecode = (value) => {
1692
1698
  throw new Error("The pagination cursor is invalid.");
1693
1699
  }
1694
1700
  };
1701
+ var firstPathAtLeast = (files, target) => {
1702
+ let low = 0;
1703
+ let high = files.length;
1704
+ while (low < high) {
1705
+ const middle = low + Math.floor((high - low) / 2);
1706
+ if ((files[middle]?.path ?? "") < target)
1707
+ low = middle + 1;
1708
+ else
1709
+ high = middle;
1710
+ }
1711
+ return low;
1712
+ };
1713
+ var firstPathAfter = (files, target) => {
1714
+ let low = 0;
1715
+ let high = files.length;
1716
+ while (low < high) {
1717
+ const middle = low + Math.floor((high - low) / 2);
1718
+ if ((files[middle]?.path ?? "") <= target)
1719
+ low = middle + 1;
1720
+ else
1721
+ high = middle;
1722
+ }
1723
+ return low;
1724
+ };
1695
1725
  var escapeRegex = (value) => value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
1696
1726
  var globRegex = (pattern) => {
1697
1727
  let source = "";
@@ -1783,6 +1813,11 @@ var spawnBounded = async (command, cwd, timeoutMs = 120000) => {
1783
1813
  class Workspace {
1784
1814
  root;
1785
1815
  auditEntries = [];
1816
+ indexIdentity = randomUUID2();
1817
+ indexRevision = 0;
1818
+ workspaceIndex;
1819
+ indexBuild;
1820
+ indexMetrics = { builds: 0, reuses: 0, stableFileReads: 0 };
1786
1821
  constructor(root) {
1787
1822
  this.root = root;
1788
1823
  }
@@ -1834,6 +1869,7 @@ class Workspace {
1834
1869
  return { path: candidate, exists: true };
1835
1870
  }
1836
1871
  async readStableFile(relativePath, allowBinary = true) {
1872
+ this.indexMetrics.stableFileReads += 1;
1837
1873
  const safe = await this.safePath(relativePath, { requireFile: true });
1838
1874
  const before = await lstat3(safe.path);
1839
1875
  if (before.size > MAX_FILE_BYTES)
@@ -1853,14 +1889,40 @@ class Workspace {
1853
1889
  mode: before.mode & 511
1854
1890
  };
1855
1891
  }
1856
- async rulesForDirectory(directory, base, inherited) {
1892
+ fingerprint(relativePath, kind, entry, digest2) {
1893
+ return {
1894
+ path: relativePath || ".",
1895
+ kind,
1896
+ dev: entry.dev,
1897
+ ino: entry.ino,
1898
+ size: entry.size,
1899
+ mtimeMs: entry.mtimeMs,
1900
+ ctimeMs: entry.ctimeMs,
1901
+ ...digest2 ? { digest: digest2 } : {}
1902
+ };
1903
+ }
1904
+ sameFingerprint(entry, fingerprint) {
1905
+ return entry.dev === fingerprint.dev && entry.ino === fingerprint.ino && entry.size === fingerprint.size && entry.mtimeMs === fingerprint.mtimeMs && entry.ctimeMs === fingerprint.ctimeMs;
1906
+ }
1907
+ async rulesForDirectory(directory, base, inherited, ignoreFiles) {
1857
1908
  const rules = [...inherited];
1858
1909
  for (const filename of IGNORE_FILES) {
1859
1910
  try {
1860
1911
  const ignorePath = path5.join(directory, filename);
1861
- const entry = await lstat3(ignorePath);
1862
- if (entry.isFile() && entry.size <= MAX_FILE_BYTES) {
1863
- rules.push(...parseIgnoreRules(await readFile(ignorePath, "utf8"), base));
1912
+ const before = await lstat3(ignorePath);
1913
+ if (before.isSymbolicLink() || !before.isFile())
1914
+ continue;
1915
+ if (before.size <= MAX_FILE_BYTES) {
1916
+ const contents = await readFile(ignorePath);
1917
+ const after = await lstat3(ignorePath);
1918
+ if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, this.fingerprint("", "ignore-file", before))) {
1919
+ throw new Error(`The ignore file changed while the workspace was being indexed: ${wirePath(path5.relative(this.root, ignorePath))}`);
1920
+ }
1921
+ const digest2 = digestBytes(contents);
1922
+ ignoreFiles.push(this.fingerprint(wirePath(path5.relative(this.root, ignorePath)), "ignore-file", after, digest2));
1923
+ rules.push(...parseIgnoreRules(contents.toString("utf8"), base));
1924
+ } else {
1925
+ ignoreFiles.push(this.fingerprint(wirePath(path5.relative(this.root, ignorePath)), "ignore-file", before));
1864
1926
  }
1865
1927
  } catch (error) {
1866
1928
  if (error.code !== "ENOENT")
@@ -1869,10 +1931,17 @@ class Workspace {
1869
1931
  }
1870
1932
  return rules;
1871
1933
  }
1872
- async collectFiles() {
1934
+ async collectFiles(version) {
1873
1935
  const files = [];
1936
+ const directories = [];
1937
+ const ignoreFiles = [];
1874
1938
  const walk = async (directory, base, inherited) => {
1875
- const rules = await this.rulesForDirectory(directory, base, inherited);
1939
+ const directoryEntry = await lstat3(directory);
1940
+ if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
1941
+ throw new Error(`A workspace directory changed while it was being indexed: ${base || "."}`);
1942
+ }
1943
+ directories.push(this.fingerprint(base, "directory", directoryEntry));
1944
+ const rules = await this.rulesForDirectory(directory, base, inherited, ignoreFiles);
1876
1945
  const entries = (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1877
1946
  for (const entry of entries) {
1878
1947
  const relative = base ? `${base}/${entry.name}` : entry.name;
@@ -1882,11 +1951,99 @@ class Workspace {
1882
1951
  if (entry.isDirectory())
1883
1952
  await walk(absolute, relative, rules);
1884
1953
  else if (entry.isFile())
1885
- files.push({ path: relative, size: (await lstat3(absolute)).size });
1954
+ files.push({ path: relative });
1886
1955
  }
1887
1956
  };
1888
1957
  await walk(this.root, "", []);
1889
- return files;
1958
+ files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
1959
+ return { version, files, directories, ignoreFiles };
1960
+ }
1961
+ async isWorkspaceIndexFresh(index) {
1962
+ try {
1963
+ for (const fingerprint of index.directories) {
1964
+ const absolute = fingerprint.path === "." ? this.root : path5.join(this.root, fingerprint.path);
1965
+ const entry = await lstat3(absolute);
1966
+ if (entry.isSymbolicLink() || !entry.isDirectory() || !this.sameFingerprint(entry, fingerprint))
1967
+ return false;
1968
+ }
1969
+ for (const fingerprint of index.ignoreFiles) {
1970
+ const absolute = path5.join(this.root, fingerprint.path);
1971
+ const before = await lstat3(absolute);
1972
+ if (before.isSymbolicLink() || !before.isFile() || !this.sameFingerprint(before, fingerprint))
1973
+ return false;
1974
+ if (fingerprint.digest) {
1975
+ const contents = await readFile(absolute);
1976
+ const after = await lstat3(absolute);
1977
+ if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, fingerprint) || digestBytes(contents) !== fingerprint.digest)
1978
+ return false;
1979
+ }
1980
+ }
1981
+ return true;
1982
+ } catch {
1983
+ return false;
1984
+ }
1985
+ }
1986
+ invalidateWorkspaceIndex() {
1987
+ this.indexRevision += 1;
1988
+ this.workspaceIndex = undefined;
1989
+ }
1990
+ async buildWorkspaceIndex(revision) {
1991
+ const version = `${this.indexIdentity}:${revision}`;
1992
+ for (let attempt = 0;attempt < 3; attempt += 1) {
1993
+ try {
1994
+ const index = await this.collectFiles(version);
1995
+ if (await this.isWorkspaceIndexFresh(index))
1996
+ return index;
1997
+ } catch (error) {
1998
+ const code = error.code;
1999
+ const changedDuringIndexing = error instanceof Error && error.message.includes("while the workspace was being indexed");
2000
+ if (attempt === 2 || code !== "ENOENT" && !changedDuringIndexing)
2001
+ throw error;
2002
+ }
2003
+ }
2004
+ throw new Error("The workspace kept changing while its file index was being built.");
2005
+ }
2006
+ async getWorkspaceIndex() {
2007
+ const cached = this.workspaceIndex;
2008
+ if (cached) {
2009
+ if (await this.isWorkspaceIndexFresh(cached)) {
2010
+ this.indexMetrics.reuses += 1;
2011
+ return cached;
2012
+ }
2013
+ this.invalidateWorkspaceIndex();
2014
+ }
2015
+ const revision = this.indexRevision;
2016
+ if (!this.indexBuild || this.indexBuild.revision !== revision) {
2017
+ const promise = this.buildWorkspaceIndex(revision);
2018
+ this.indexBuild = { revision, promise };
2019
+ }
2020
+ const build = this.indexBuild;
2021
+ let index;
2022
+ try {
2023
+ index = await build.promise;
2024
+ } catch (error) {
2025
+ if (this.indexBuild === build)
2026
+ this.indexBuild = undefined;
2027
+ throw error;
2028
+ }
2029
+ if (revision !== this.indexRevision)
2030
+ return this.getWorkspaceIndex();
2031
+ if (this.indexBuild === build)
2032
+ this.indexBuild = undefined;
2033
+ if (this.workspaceIndex?.version !== index.version) {
2034
+ this.workspaceIndex = index;
2035
+ this.indexMetrics.builds += 1;
2036
+ }
2037
+ return this.workspaceIndex;
2038
+ }
2039
+ workspaceIndexDiagnostics() {
2040
+ const index = this.workspaceIndex;
2041
+ return {
2042
+ ...this.indexMetrics,
2043
+ ...index ? { version: index.version } : {},
2044
+ files: index?.files.length ?? 0,
2045
+ directories: index?.directories.length ?? 0
2046
+ };
1890
2047
  }
1891
2048
  async listFiles(relativePath = ".", options = {}) {
1892
2049
  const start = await this.safePath(relativePath);
@@ -1898,36 +2055,52 @@ class Workspace {
1898
2055
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 5000)
1899
2056
  throw new Error("limit must be between 1 and 5000.");
1900
2057
  let after = "";
2058
+ let cursorIndexVersion;
1901
2059
  if (input.cursor) {
1902
2060
  const parsed = cursorDecode(input.cursor);
1903
- if (parsed.v !== 1 || parsed.kind !== "list" || parsed.path !== requestPath || parsed.limit !== limit || typeof parsed.after !== "string") {
2061
+ if (parsed.v !== 2 || parsed.kind !== "list" || parsed.path !== requestPath || parsed.limit !== limit || typeof parsed.indexVersion !== "string" || typeof parsed.after !== "string") {
1904
2062
  throw new Error("The pagination cursor does not match this list request.");
1905
2063
  }
1906
2064
  after = parsed.after;
2065
+ cursorIndexVersion = parsed.indexVersion;
2066
+ }
2067
+ const index = await this.getWorkspaceIndex();
2068
+ if (cursorIndexVersion && cursorIndexVersion !== index.version) {
2069
+ throw new Error("The pagination cursor is stale because the workspace changed.");
1907
2070
  }
1908
2071
  const prefix = requestPath === "." ? "" : `${requestPath}/`;
1909
- const candidates = (await this.collectFiles()).filter((file) => (requestPath === "." || file.path.startsWith(prefix)) && file.path > after);
1910
- const selected = candidates.slice(0, limit);
2072
+ let candidateIndex = after ? firstPathAfter(index.files, after) : requestPath === "." ? 0 : firstPathAtLeast(index.files, prefix);
2073
+ const selected = [];
2074
+ while (candidateIndex < index.files.length && selected.length <= limit) {
2075
+ const candidate = index.files[candidateIndex];
2076
+ if (!candidate || requestPath !== "." && !candidate.path.startsWith(prefix))
2077
+ break;
2078
+ selected.push(candidate);
2079
+ candidateIndex += 1;
2080
+ }
2081
+ const truncated = selected.length > limit;
2082
+ if (truncated)
2083
+ selected.pop();
1911
2084
  const files = [];
1912
2085
  for (const candidate of selected) {
1913
2086
  const file = await this.readStableFile(candidate.path);
1914
2087
  files.push({ path: file.path, size: file.contents.byteLength, digest: file.digest });
1915
2088
  }
1916
- const truncated = candidates.length > selected.length;
1917
2089
  const last = selected.at(-1);
1918
2090
  return {
1919
2091
  files,
1920
2092
  truncated,
1921
- ...truncated && last ? { nextCursor: cursorEncode({ v: 1, kind: "list", path: requestPath, limit, after: last.path }) } : {}
2093
+ ...truncated && last ? {
2094
+ nextCursor: cursorEncode({ v: 2, kind: "list", path: requestPath, limit, after: last.path, indexVersion: index.version })
2095
+ } : {}
1922
2096
  };
1923
2097
  }
1924
- async readFile(relativePath, startLine = 1, endLine) {
2098
+ renderReadFile(file, startLine = 1, endLine) {
1925
2099
  if (!Number.isSafeInteger(startLine) || startLine < 1)
1926
2100
  throw new Error("startLine must be a positive integer.");
1927
2101
  if (endLine !== undefined && (!Number.isSafeInteger(endLine) || endLine < startLine)) {
1928
2102
  throw new Error("endLine must be greater than or equal to startLine.");
1929
2103
  }
1930
- const file = await this.readStableFile(relativePath, false);
1931
2104
  const lines = file.contents.toString("utf8").split(/\r?\n/);
1932
2105
  const boundedEnd = Math.min(endLine ?? startLine + 399, startLine + 1999, lines.length);
1933
2106
  return {
@@ -1941,6 +2114,37 @@ class Workspace {
1941
2114
  truncated: boundedEnd < lines.length
1942
2115
  };
1943
2116
  }
2117
+ async readFile(relativePath, startLine = 1, endLine) {
2118
+ return this.renderReadFile(await this.readStableFile(relativePath, false), startLine, endLine);
2119
+ }
2120
+ async readFiles(requests) {
2121
+ if (!Array.isArray(requests) || requests.length < 1 || requests.length > MAX_READ_BATCH_FILES) {
2122
+ throw new Error(`readFiles requires between 1 and ${MAX_READ_BATCH_FILES} file requests.`);
2123
+ }
2124
+ const ordered = requests.map((request) => ({
2125
+ ...request,
2126
+ path: wirePath(path5.relative(this.root, this.lexicalPath(request.path)))
2127
+ })).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));
2128
+ const stableFiles = new Map;
2129
+ let totalBytes = 0;
2130
+ for (const request of ordered) {
2131
+ if (!Number.isSafeInteger(request.startLine ?? 1) || (request.startLine ?? 1) < 1 || request.endLine !== undefined && (!Number.isSafeInteger(request.endLine) || request.endLine < (request.startLine ?? 1))) {
2132
+ throw new Error(`Invalid line range for ${request.path}.`);
2133
+ }
2134
+ if (stableFiles.has(request.path))
2135
+ continue;
2136
+ const file = await this.readStableFile(request.path, false);
2137
+ totalBytes += file.contents.byteLength;
2138
+ if (totalBytes > MAX_READ_BATCH_BYTES) {
2139
+ throw new Error(`readFiles exceeds the aggregate ${MAX_READ_BATCH_BYTES}-byte source limit.`);
2140
+ }
2141
+ stableFiles.set(request.path, file);
2142
+ }
2143
+ return {
2144
+ files: ordered.map((request) => this.renderReadFile(stableFiles.get(request.path), request.startLine ?? 1, request.endLine)),
2145
+ sourceBytes: totalBytes
2146
+ };
2147
+ }
1944
2148
  async inspectFile(relativePath) {
1945
2149
  const file = await this.readStableFile(relativePath);
1946
2150
  return {
@@ -1963,21 +2167,29 @@ class Workspace {
1963
2167
  const caseSensitive = options.caseSensitive ?? false;
1964
2168
  let afterPath = "";
1965
2169
  let afterLine = 0;
2170
+ let cursorIndexVersion;
1966
2171
  if (options.cursor) {
1967
2172
  const parsed = cursorDecode(options.cursor);
1968
- 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)) {
2173
+ 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") {
1969
2174
  throw new Error("The pagination cursor does not match this search request.");
1970
2175
  }
1971
2176
  afterPath = parsed.afterPath;
1972
2177
  afterLine = parsed.afterLine;
2178
+ cursorIndexVersion = parsed.indexVersion;
2179
+ }
2180
+ const index = await this.getWorkspaceIndex();
2181
+ if (cursorIndexVersion && cursorIndexVersion !== index.version) {
2182
+ throw new Error("The pagination cursor is stale because the workspace changed.");
1973
2183
  }
1974
2184
  const prefix = requestPath === "." ? "" : `${requestPath}/`;
1975
2185
  const needle = caseSensitive ? query : query.toLocaleLowerCase();
1976
2186
  const matches = [];
1977
2187
  let hasMore = false;
1978
- for (const candidate of await this.collectFiles()) {
1979
- if (requestPath !== "." && !candidate.path.startsWith(prefix) || candidate.size > MAX_FILE_BYTES || candidate.path < afterPath)
1980
- continue;
2188
+ let candidateIndex = afterPath ? firstPathAtLeast(index.files, afterPath) : requestPath === "." ? 0 : firstPathAtLeast(index.files, prefix);
2189
+ for (;candidateIndex < index.files.length; candidateIndex += 1) {
2190
+ const candidate = index.files[candidateIndex];
2191
+ if (!candidate || requestPath !== "." && !candidate.path.startsWith(prefix))
2192
+ break;
1981
2193
  let file;
1982
2194
  try {
1983
2195
  file = await this.readStableFile(candidate.path, false);
@@ -1985,11 +2197,11 @@ class Workspace {
1985
2197
  continue;
1986
2198
  }
1987
2199
  const lines = file.contents.toString("utf8").split(/\r?\n/);
1988
- for (let index = 0;index < lines.length; index += 1) {
1989
- const lineNumber = index + 1;
2200
+ for (let index2 = 0;index2 < lines.length; index2 += 1) {
2201
+ const lineNumber = index2 + 1;
1990
2202
  if (candidate.path === afterPath && lineNumber <= afterLine)
1991
2203
  continue;
1992
- const line = lines[index] ?? "";
2204
+ const line = lines[index2] ?? "";
1993
2205
  if (!(caseSensitive ? line : line.toLocaleLowerCase()).includes(needle))
1994
2206
  continue;
1995
2207
  if (matches.length >= limit) {
@@ -2006,10 +2218,88 @@ class Workspace {
2006
2218
  matches,
2007
2219
  truncated: hasMore,
2008
2220
  ...hasMore && last ? {
2009
- nextCursor: cursorEncode({ v: 1, kind: "search", path: requestPath, query, caseSensitive, limit, afterPath: last.path, afterLine: last.line })
2221
+ nextCursor: cursorEncode({
2222
+ v: 2,
2223
+ kind: "search",
2224
+ path: requestPath,
2225
+ query,
2226
+ caseSensitive,
2227
+ limit,
2228
+ afterPath: last.path,
2229
+ afterLine: last.line,
2230
+ indexVersion: index.version
2231
+ })
2010
2232
  } : {}
2011
2233
  };
2012
2234
  }
2235
+ async searchMany(queries, relativePath = ".", options = {}) {
2236
+ if (!Array.isArray(queries) || queries.length < 1 || queries.length > MAX_SEARCH_MANY_QUERIES) {
2237
+ throw new Error(`searchMany requires between 1 and ${MAX_SEARCH_MANY_QUERIES} queries.`);
2238
+ }
2239
+ const limitPerQuery = options.limitPerQuery ?? 50;
2240
+ if (!Number.isSafeInteger(limitPerQuery) || limitPerQuery < 1 || limitPerQuery > MAX_SEARCH_MANY_MATCHES || limitPerQuery * queries.length > MAX_SEARCH_MANY_MATCHES) {
2241
+ throw new Error(`searchMany allows at most ${MAX_SEARCH_MANY_MATCHES} aggregate matches.`);
2242
+ }
2243
+ const seen = new Set;
2244
+ const states = queries.map((input) => {
2245
+ if (!input.query || input.query.length > 200)
2246
+ throw new Error("Each search query must be between 1 and 200 characters.");
2247
+ const caseSensitive = input.caseSensitive ?? false;
2248
+ const key = `${caseSensitive ? "1" : "0"}\x00${input.query}`;
2249
+ if (seen.has(key))
2250
+ throw new Error("searchMany queries must be unique.");
2251
+ seen.add(key);
2252
+ return {
2253
+ query: input.query,
2254
+ caseSensitive,
2255
+ needle: caseSensitive ? input.query : input.query.toLocaleLowerCase(),
2256
+ matches: [],
2257
+ truncated: false
2258
+ };
2259
+ });
2260
+ const start = await this.safePath(relativePath);
2261
+ if (!(await lstat3(start.path)).isDirectory())
2262
+ throw new Error("searchMany requires a directory.");
2263
+ const requestPath = wirePath(path5.relative(this.root, start.path)) || ".";
2264
+ const prefix = requestPath === "." ? "" : `${requestPath}/`;
2265
+ const index = await this.getWorkspaceIndex();
2266
+ let candidateIndex = requestPath === "." ? 0 : firstPathAtLeast(index.files, prefix);
2267
+ for (;candidateIndex < index.files.length; candidateIndex += 1) {
2268
+ const candidate = index.files[candidateIndex];
2269
+ if (!candidate || requestPath !== "." && !candidate.path.startsWith(prefix))
2270
+ break;
2271
+ let file;
2272
+ try {
2273
+ file = await this.readStableFile(candidate.path, false);
2274
+ } catch {
2275
+ continue;
2276
+ }
2277
+ const lines = file.contents.toString("utf8").split(/\r?\n/);
2278
+ for (let lineIndex = 0;lineIndex < lines.length; lineIndex += 1) {
2279
+ const line = lines[lineIndex] ?? "";
2280
+ const foldedLine = line.toLocaleLowerCase();
2281
+ for (const state of states) {
2282
+ if (state.truncated || !(state.caseSensitive ? line : foldedLine).includes(state.needle))
2283
+ continue;
2284
+ if (state.matches.length >= limitPerQuery) {
2285
+ state.truncated = true;
2286
+ continue;
2287
+ }
2288
+ state.matches.push({
2289
+ path: candidate.path,
2290
+ line: lineIndex + 1,
2291
+ text: truncate(line, 500),
2292
+ digest: file.digest
2293
+ });
2294
+ }
2295
+ }
2296
+ if (states.every((state) => state.truncated))
2297
+ break;
2298
+ }
2299
+ return {
2300
+ results: states.map(({ needle: _needle, ...state }) => state)
2301
+ };
2302
+ }
2013
2303
  audit(entry) {
2014
2304
  const record = { id: randomUUID2(), timestamp: new Date().toISOString(), ...entry };
2015
2305
  this.auditEntries.push(record);
@@ -2173,6 +2463,7 @@ class Workspace {
2173
2463
  throw new AggregateError([error, ...rollbackErrors], "Patch failed and rollback was incomplete.");
2174
2464
  throw error;
2175
2465
  }
2466
+ this.invalidateWorkspaceIndex();
2176
2467
  const changes = prepared.map((item) => this.audit({
2177
2468
  operation: item.before ? "update" : "create",
2178
2469
  path: item.change.path,
@@ -2233,6 +2524,7 @@ class Workspace {
2233
2524
  await unlink(destination.path).catch(() => {});
2234
2525
  throw error;
2235
2526
  }
2527
+ this.invalidateWorkspaceIndex();
2236
2528
  const audit = this.audit({ operation: "move", path: parsed.source, destination: parsed.destination, beforeDigest: source.digest, afterDigest: source.digest });
2237
2529
  return { source: parsed.source, destination: parsed.destination, digest: source.digest, audit };
2238
2530
  }
@@ -2321,6 +2613,7 @@ class Workspace {
2321
2613
  await unlink(path5.join(directory, `${quarantineId}.json`)).catch(() => {});
2322
2614
  throw error;
2323
2615
  }
2616
+ this.invalidateWorkspaceIndex();
2324
2617
  const audit = this.audit({ operation: "quarantine", path: parsed.path, beforeDigest: source.digest, quarantineId });
2325
2618
  return { quarantineId, path: parsed.path, digest: source.digest, audit };
2326
2619
  }
@@ -2389,6 +2682,7 @@ class Workspace {
2389
2682
  }
2390
2683
  throw rollbackErrors.length ? new AggregateError([error, ...rollbackErrors], "Restore failed and rollback was incomplete.") : error;
2391
2684
  }
2685
+ this.invalidateWorkspaceIndex();
2392
2686
  const audit = this.audit({ operation: "restore", path: destinationPath, afterDigest: digest2, quarantineId: parsed.quarantineId });
2393
2687
  return { quarantineId: parsed.quarantineId, path: destinationPath, digest: digest2, audit };
2394
2688
  }
@@ -2409,7 +2703,11 @@ class Workspace {
2409
2703
  throw new Error(`package.json does not define the "${check}" script.`);
2410
2704
  if (scripts[check] !== expectedScript)
2411
2705
  throw new Error(`The "${check}" script changed or does not match expectedScript.`);
2412
- return spawnBounded(["bun", "--no-env-file", "run", check], this.root);
2706
+ try {
2707
+ return await spawnBounded(["bun", "--no-env-file", "run", check], this.root);
2708
+ } finally {
2709
+ this.invalidateWorkspaceIndex();
2710
+ }
2413
2711
  }
2414
2712
  async gitDiff() {
2415
2713
  const [statusResult, diffResult, stagedResult] = await Promise.all([
@@ -2434,7 +2732,9 @@ var TERMINAL_STATUSES2 = new Set([
2434
2732
  var BUILT_IN_TOOL_NAMES = new Set([
2435
2733
  "list_files",
2436
2734
  "read_file",
2735
+ "read_files",
2437
2736
  "search_files",
2737
+ "search_many",
2438
2738
  "propose_edits",
2439
2739
  "apply_patch",
2440
2740
  "move_file",
@@ -2559,11 +2859,26 @@ var parseRuntimeVersion = (stdout, runtime) => {
2559
2859
  var validContainerIds = (stdout) => stdout.split(/\r?\n/).map((value) => value.trim()).filter((value) => /^[a-f0-9]{12,64}$/.test(value));
2560
2860
  var validVolumeNames = (stdout) => stdout.split(/\r?\n/).map((value) => value.trim()).filter((value) => /^zhivex-harness-[A-Za-z0-9_.-]+$/.test(value));
2561
2861
  var MAX_SYNC_ENTRIES = 1e4;
2862
+ var updateSnapshotMetadata = (target, kind, relative, entry) => {
2863
+ target.update([
2864
+ kind,
2865
+ relative,
2866
+ entry.dev,
2867
+ entry.ino,
2868
+ entry.size,
2869
+ entry.mtimeMs,
2870
+ entry.ctimeMs,
2871
+ entry.mode & 511
2872
+ ].join("\x00"));
2873
+ target.update("\x00");
2874
+ };
2562
2875
  var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspaceBytes, maxFileWriteBytes) => {
2563
2876
  let entries = 0;
2564
2877
  let totalBytes = 0;
2878
+ const seal = createHash6("sha256");
2879
+ const metadata = createHash6("sha256");
2565
2880
  const visit = async (directory, relativeDirectory = "") => {
2566
- for (const name of await readdir2(directory)) {
2881
+ for (const name of (await readdir2(directory)).sort()) {
2567
2882
  const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
2568
2883
  const absolute = path6.join(directory, name);
2569
2884
  if (!isInside3(stagedRoot, absolute) || relative.length > 1024) {
@@ -2582,22 +2897,32 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
2582
2897
  throw new Error(`OCI workspace export contains a symbolic link: ${relative}.`);
2583
2898
  }
2584
2899
  if (entry.isDirectory()) {
2900
+ updateSnapshotMetadata(metadata, "directory", relative, entry);
2901
+ seal.update(`directory\x00${relative}\x00${entry.mode & 511}\x00`);
2585
2902
  await visit(absolute, relative);
2586
2903
  continue;
2587
2904
  }
2588
2905
  if (!entry.isFile() || entry.nlink !== 1) {
2589
2906
  throw new Error(`OCI workspace export contains a special or linked file: ${relative}.`);
2590
2907
  }
2908
+ const contents = await readFile2(absolute);
2909
+ const after = await lstat4(absolute);
2910
+ 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)) {
2911
+ throw new Error(`OCI workspace export changed while it was being validated: ${relative}.`);
2912
+ }
2591
2913
  totalBytes += entry.size;
2592
2914
  if (totalBytes > maxWorkspaceBytes) {
2593
2915
  throw new Error(`OCI workspace export exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
2594
2916
  }
2917
+ const contentDigest = digest2(contents);
2918
+ updateSnapshotMetadata(metadata, "file", relative, after);
2919
+ seal.update(`file\x00${relative}\x00${entry.mode & 511}\x00${entry.size}\x00${contentDigest}\x00`);
2595
2920
  if (entry.size > maxFileWriteBytes) {
2596
2921
  const previous = path6.join(previousRoot, ...relative.split("/"));
2597
2922
  let unchanged = false;
2598
2923
  try {
2599
2924
  const previousEntry = await lstat4(previous);
2600
- unchanged = previousEntry.isFile() && !previousEntry.isSymbolicLink() && previousEntry.size === entry.size && digest2(await readFile2(previous)) === digest2(await readFile2(absolute));
2925
+ unchanged = previousEntry.isFile() && !previousEntry.isSymbolicLink() && previousEntry.size === entry.size && digest2(await readFile2(previous)) === contentDigest;
2601
2926
  } catch (error) {
2602
2927
  if (error.code !== "ENOENT")
2603
2928
  throw error;
@@ -2609,6 +2934,51 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
2609
2934
  }
2610
2935
  };
2611
2936
  await visit(stagedRoot);
2937
+ return {
2938
+ entries,
2939
+ totalBytes,
2940
+ seal: `sha256:${seal.digest("hex")}`,
2941
+ metadataFingerprint: `sha256:${metadata.digest("hex")}`
2942
+ };
2943
+ };
2944
+ var snapshotMetadataFingerprint = async (root, maxWorkspaceBytes) => {
2945
+ let entries = 0;
2946
+ let totalBytes = 0;
2947
+ const metadata = createHash6("sha256");
2948
+ const visit = async (directory, relativeDirectory = "") => {
2949
+ for (const name of (await readdir2(directory)).sort()) {
2950
+ const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
2951
+ if (relative === "node_modules")
2952
+ continue;
2953
+ const absolute = path6.join(directory, name);
2954
+ if (!isInside3(root, absolute) || relative.length > 1024) {
2955
+ throw new Error(`OCI workspace snapshot contains an invalid path: ${relative}.`);
2956
+ }
2957
+ entries += 1;
2958
+ if (entries > MAX_SYNC_ENTRIES) {
2959
+ throw new Error(`OCI workspace snapshot exceeds the ${MAX_SYNC_ENTRIES}-entry limit.`);
2960
+ }
2961
+ const entry = await lstat4(absolute);
2962
+ if (entry.isSymbolicLink()) {
2963
+ throw new Error(`OCI workspace snapshot contains a symbolic link: ${relative}.`);
2964
+ }
2965
+ if (entry.isDirectory()) {
2966
+ updateSnapshotMetadata(metadata, "directory", relative, entry);
2967
+ await visit(absolute, relative);
2968
+ continue;
2969
+ }
2970
+ if (!entry.isFile() || entry.nlink !== 1) {
2971
+ throw new Error(`OCI workspace snapshot contains a special or linked file: ${relative}.`);
2972
+ }
2973
+ totalBytes += entry.size;
2974
+ if (totalBytes > maxWorkspaceBytes) {
2975
+ throw new Error(`OCI workspace snapshot exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
2976
+ }
2977
+ updateSnapshotMetadata(metadata, "file", relative, entry);
2978
+ }
2979
+ };
2980
+ await visit(root);
2981
+ return `sha256:${metadata.digest("hex")}`;
2612
2982
  };
2613
2983
  var replaceSnapshot = async (snapshotRoot, stagedRoot) => {
2614
2984
  const backupRoot = `${snapshotRoot}.${randomUUID3()}.previous`;
@@ -2623,10 +2993,42 @@ var replaceSnapshot = async (snapshotRoot, stagedRoot) => {
2623
2993
  }
2624
2994
  await rm(backupRoot, { recursive: true, force: true });
2625
2995
  };
2996
+ var OCI_WORKSPACE_SEAL_SCRIPT = [
2997
+ "import { createHash } from 'node:crypto';",
2998
+ "import { lstat, readFile, readdir, readlink, rm } from 'node:fs/promises';",
2999
+ "import path from 'node:path';",
3000
+ "const root='/workspace';",
3001
+ "const seal=createHash('sha256');",
3002
+ "const modules=path.join(root,'node_modules');",
3003
+ "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});}",
3004
+ "const digest=(value)=>`sha256:${createHash('sha256').update(value).digest('hex')}`;",
3005
+ "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);",
3006
+ "const visit=async(directory,base='')=>{",
3007
+ "for(const name of (await readdir(directory)).sort()){",
3008
+ "const relative=base?`${base}/${name}`:name;",
3009
+ "if(relative==='node_modules')continue;",
3010
+ "const absolute=path.join(directory,name);",
3011
+ "const entry=await lstat(absolute);",
3012
+ "if(entry.isSymbolicLink())throw new Error(`symbolic link: ${relative}`);",
3013
+ "if(entry.isDirectory()){seal.update(`directory\\0${relative}\\0${entry.mode&0o777}\\0`);await visit(absolute,relative);continue;}",
3014
+ "if(!entry.isFile()||entry.nlink!==1)throw new Error(`special or linked file: ${relative}`);",
3015
+ "const contents=await readFile(absolute);",
3016
+ "const after=await lstat(absolute);",
3017
+ "if(!same(entry,after))throw new Error(`changed while sealing: ${relative}`);",
3018
+ "seal.update(`file\\0${relative}\\0${entry.mode&0o777}\\0${entry.size}\\0${digest(contents)}\\0`);",
3019
+ "}",
3020
+ "};",
3021
+ "await visit(root);",
3022
+ "for(const name of await readdir('/tmp'))await rm(path.join('/tmp',name),{recursive:true,force:true});",
3023
+ "console.log(`sha256:${seal.digest('hex')}`);"
3024
+ ].join("");
3025
+ var OCI_SESSION_CONTROLLER_SCRIPT = "setInterval(()=>{},2147483647);";
2626
3026
 
2627
3027
  class CliOciRuntimeAdapter {
2628
3028
  runtime;
2629
3029
  hostEnvironment;
3030
+ sessions = new Map;
3031
+ runQueues = new Map;
2630
3032
  constructor(runtime) {
2631
3033
  this.runtime = runtime;
2632
3034
  this.hostEnvironment = Object.fromEntries(["DOCKER_CONFIG", "DOCKER_CONTEXT", "DOCKER_HOST", "HOME", "PATH", "XDG_RUNTIME_DIR"].flatMap((name) => process.env[name] ? [[name, process.env[name]]] : []));
@@ -2679,10 +3081,44 @@ ${result.stdout}`)) {
2679
3081
  throw new Error(`${this.runtime} could not remove OCI workspace volume ${name}: ${result.stderr.trim()}`);
2680
3082
  }
2681
3083
  }
2682
- async run(request) {
3084
+ requestFingerprint(request) {
3085
+ return digest2(JSON.stringify({
3086
+ runId: request.runId,
3087
+ snapshotRoot: request.snapshotRoot,
3088
+ dependencyRoot: request.dependencyRoot ?? null,
3089
+ imageId: request.imageId,
3090
+ limits: request.limits
3091
+ }));
3092
+ }
3093
+ async withRunLock(runId, operation) {
3094
+ const key = runHash(runId);
3095
+ const previous = this.runQueues.get(key) ?? Promise.resolve();
3096
+ let release;
3097
+ const gate = new Promise((resolve) => {
3098
+ release = resolve;
3099
+ });
3100
+ const tail = previous.then(() => gate, () => gate);
3101
+ this.runQueues.set(key, tail);
3102
+ await previous.catch(() => {
3103
+ return;
3104
+ });
3105
+ try {
3106
+ return await operation();
3107
+ } finally {
3108
+ release();
3109
+ if (this.runQueues.get(key) === tail)
3110
+ this.runQueues.delete(key);
3111
+ }
3112
+ }
3113
+ async destroySession(session) {
3114
+ if (this.sessions.get(session.key) === session)
3115
+ this.sessions.delete(session.key);
3116
+ await this.forceRemove(session.name);
3117
+ await this.removeVolume(session.volumeName);
3118
+ }
3119
+ async createSession(request, requestFingerprint, snapshotSeal, hostFingerprint) {
2683
3120
  const resourceName = `zhivex-harness-${runHash(request.runId)}-${randomUUID3().slice(0, 8)}`;
2684
3121
  const name = `${resourceName}-job`;
2685
- const exportName = `${resourceName}-export`;
2686
3122
  const volumeName = `${resourceName}-workspace`;
2687
3123
  const uid = process.getuid?.() ?? 65532;
2688
3124
  const gid = process.getgid?.() ?? 65532;
@@ -2738,8 +3174,6 @@ ${result.stdout}`)) {
2738
3174
  "BUN_INSTALL_CACHE_DIR=/tmp/bun-cache",
2739
3175
  "--env",
2740
3176
  "CI=1",
2741
- "--volume",
2742
- `${request.snapshotRoot}:/seed:ro`,
2743
3177
  ...request.dependencyRoot ? ["--volume", `${request.dependencyRoot}:/dependencies:ro`] : [],
2744
3178
  "--workdir",
2745
3179
  "/workspace",
@@ -2747,26 +3181,19 @@ ${result.stdout}`)) {
2747
3181
  "bun",
2748
3182
  request.imageId,
2749
3183
  "-e",
2750
- `await Bun.sleep(${request.limits.maxProcessRuntimeMs + 60000})`
3184
+ OCI_SESSION_CONTROLLER_SCRIPT
2751
3185
  ];
2752
- let forcedRemoval;
2753
- const forceRemove = () => {
2754
- forcedRemoval ??= this.forceRemove(name);
2755
- };
3186
+ let volumeCreated = false;
2756
3187
  try {
2757
3188
  await this.cli(volumeArgs);
3189
+ volumeCreated = true;
2758
3190
  await this.cli(args);
2759
3191
  await this.cli(["start", name]);
2760
3192
  await this.cli([
2761
- "exec",
2762
- "--workdir",
2763
- "/workspace",
2764
- "--user",
2765
- `${uid}:${gid}`,
2766
- name,
2767
- "bun",
2768
- "-e",
2769
- "import { cp } from 'node:fs/promises'; await cp('/seed', '/workspace', { recursive: true, force: false, errorOnExist: true })"
3193
+ "cp",
3194
+ "-a",
3195
+ `${request.snapshotRoot}${path6.sep}.`,
3196
+ `${name}:/workspace`
2770
3197
  ], 60000);
2771
3198
  if (request.dependencyRoot) {
2772
3199
  await this.cli([
@@ -2781,88 +3208,183 @@ ${result.stdout}`)) {
2781
3208
  "import { symlink } from 'node:fs/promises'; await symlink('/dependencies', '/workspace/node_modules', 'dir')"
2782
3209
  ]);
2783
3210
  }
2784
- const result = await spawnBounded2([
2785
- this.runtime,
3211
+ await this.cli(["pause", name]);
3212
+ const session = {
3213
+ key: runHash(request.runId),
3214
+ requestFingerprint,
3215
+ name,
3216
+ volumeName,
3217
+ snapshotSeal,
3218
+ hostFingerprint,
3219
+ paused: true
3220
+ };
3221
+ this.sessions.set(session.key, session);
3222
+ return session;
3223
+ } catch (error) {
3224
+ await this.forceRemove(name);
3225
+ if (volumeCreated)
3226
+ await this.removeVolume(volumeName).catch(() => {
3227
+ return;
3228
+ });
3229
+ throw error;
3230
+ }
3231
+ }
3232
+ async onlyControllerProcessRemains(session) {
3233
+ const result = await this.cli(this.runtime === "podman" ? ["top", session.name, "pid"] : ["top", session.name, "-eo", "pid"]);
3234
+ const processIds = result.stdout.split(/\r?\n/).filter((line) => /^\s*\d+\s*$/.test(line));
3235
+ return processIds.length === 1;
3236
+ }
3237
+ async containerWorkspaceSeal(session, uid, gid, hasDependencies) {
3238
+ if (session.paused) {
3239
+ await this.cli(["unpause", session.name]);
3240
+ session.paused = false;
3241
+ }
3242
+ try {
3243
+ const result = await this.cli([
2786
3244
  "exec",
2787
3245
  "--workdir",
2788
3246
  "/workspace",
2789
3247
  "--user",
2790
3248
  `${uid}:${gid}`,
2791
- name,
2792
- ...request.command
2793
- ], {
2794
- env: this.hostEnvironment,
2795
- timeoutMs: request.limits.maxProcessRuntimeMs,
2796
- maxOutputBytes: request.limits.maxProcessOutputBytes,
2797
- ...request.abortSignal ? { abortSignal: request.abortSignal } : {},
2798
- onStop: forceRemove
2799
- });
2800
- if (result.timedOut || result.cancelled || result.outputLimitExceeded) {
2801
- forceRemove();
2802
- await forcedRemoval;
2803
- } else if (result.exitCode === 0) {
2804
- await this.cli(["pause", name]);
2805
- const stagedRoot = path6.join(path6.dirname(request.snapshotRoot), `.workspace-sync-${randomUUID3()}`);
2806
- try {
2807
- await privateDirectory(stagedRoot);
2808
- try {
2809
- await this.cli([
2810
- "run",
2811
- "--rm",
2812
- "--name",
2813
- exportName,
2814
- "--label",
2815
- `${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`,
2816
- "--label",
2817
- `com.zhivex.harness.run=${runHash(request.runId)}`,
2818
- "--network",
2819
- "none",
2820
- "--read-only",
2821
- "--cap-drop",
2822
- "ALL",
2823
- "--security-opt",
2824
- "no-new-privileges",
2825
- "--pids-limit",
2826
- "32",
2827
- "--memory",
2828
- "256m",
2829
- "--cpus",
2830
- "1",
2831
- "--tmpfs",
2832
- "/tmp:rw,noexec,nosuid,nodev,size=16m",
2833
- "--user",
2834
- `${uid}:${gid}`,
2835
- "--volume",
2836
- `${volumeName}:/workspace:ro`,
2837
- "--volume",
2838
- `${stagedRoot}:/export:rw`,
2839
- "--workdir",
2840
- "/workspace",
2841
- "--entrypoint",
2842
- "bun",
2843
- request.imageId,
2844
- "-e",
2845
- "import { cp } from 'node:fs/promises'; await cp('/workspace', '/export', { recursive: true, force: false, errorOnExist: true })"
2846
- ], 60000);
2847
- } finally {
2848
- await this.forceRemove(exportName);
2849
- }
2850
- await validateSynchronizedSnapshot(stagedRoot, request.snapshotRoot, request.limits.maxWorkspaceBytes, request.limits.maxFileWriteBytes);
2851
- await replaceSnapshot(request.snapshotRoot, stagedRoot);
2852
- } finally {
2853
- await rm(stagedRoot, { recursive: true, force: true });
2854
- }
3249
+ "--env",
3250
+ `ZHIVEX_HARNESS_HAS_DEPENDENCIES=${hasDependencies ? "1" : "0"}`,
3251
+ session.name,
3252
+ "bun",
3253
+ "-e",
3254
+ OCI_WORKSPACE_SEAL_SCRIPT
3255
+ ], 60000, 20000);
3256
+ const seal = result.stdout.trim();
3257
+ if (!/^sha256:[a-f0-9]{64}$/.test(seal)) {
3258
+ throw new Error("OCI workspace seal helper returned an invalid digest.");
2855
3259
  }
2856
- return {
2857
- ...result,
2858
- command: request.command
2859
- };
3260
+ return seal;
2860
3261
  } finally {
3262
+ if (!session.paused) {
3263
+ await this.cli(["pause", session.name]);
3264
+ session.paused = true;
3265
+ }
3266
+ }
3267
+ }
3268
+ async runInSession(request, session, sessionReused) {
3269
+ const uid = process.getuid?.() ?? 65532;
3270
+ const gid = process.getgid?.() ?? 65532;
3271
+ if (session.paused) {
3272
+ await this.cli(["unpause", session.name]);
3273
+ session.paused = false;
3274
+ }
3275
+ let forcedRemoval;
3276
+ const forceRemove = () => {
3277
+ forcedRemoval ??= this.forceRemove(session.name);
3278
+ };
3279
+ const result = await spawnBounded2([
3280
+ this.runtime,
3281
+ "exec",
3282
+ "--workdir",
3283
+ "/workspace",
3284
+ "--user",
3285
+ `${uid}:${gid}`,
3286
+ session.name,
3287
+ ...request.command
3288
+ ], {
3289
+ env: this.hostEnvironment,
3290
+ timeoutMs: request.limits.maxProcessRuntimeMs,
3291
+ maxOutputBytes: request.limits.maxProcessOutputBytes,
3292
+ ...request.abortSignal ? { abortSignal: request.abortSignal } : {},
3293
+ onStop: forceRemove
3294
+ });
3295
+ if (result.exitCode !== 0 || result.timedOut || result.cancelled || result.outputLimitExceeded) {
2861
3296
  forceRemove();
2862
3297
  await forcedRemoval;
2863
- await this.removeVolume(volumeName);
3298
+ await this.destroySession(session);
3299
+ return {
3300
+ ...result,
3301
+ command: request.command,
3302
+ sessionReused,
3303
+ workspacePublished: false,
3304
+ workspaceExported: false
3305
+ };
3306
+ }
3307
+ try {
3308
+ if (!await this.onlyControllerProcessRemains(session)) {
3309
+ await this.destroySession(session);
3310
+ return {
3311
+ ...result,
3312
+ command: request.command,
3313
+ exitCode: 126,
3314
+ stderr: [
3315
+ result.stderr,
3316
+ "OCI command left background processes running; its workspace changes were discarded."
3317
+ ].filter(Boolean).join(`
3318
+ `),
3319
+ sessionReused,
3320
+ workspacePublished: false,
3321
+ workspaceExported: false
3322
+ };
3323
+ }
3324
+ const workspaceSeal = await this.containerWorkspaceSeal(session, uid, gid, Boolean(request.dependencyRoot));
3325
+ if (workspaceSeal === session.snapshotSeal) {
3326
+ return {
3327
+ ...result,
3328
+ command: request.command,
3329
+ sessionReused,
3330
+ workspacePublished: true,
3331
+ workspaceExported: false
3332
+ };
3333
+ }
3334
+ const stagedRoot = path6.join(path6.dirname(request.snapshotRoot), `.workspace-sync-${randomUUID3()}`);
3335
+ try {
3336
+ await privateDirectory(stagedRoot);
3337
+ await this.cli(["cp", `${session.name}:/workspace/.`, stagedRoot], 60000);
3338
+ const validated = await validateSynchronizedSnapshot(stagedRoot, request.snapshotRoot, request.limits.maxWorkspaceBytes, request.limits.maxFileWriteBytes);
3339
+ await replaceSnapshot(request.snapshotRoot, stagedRoot);
3340
+ session.snapshotSeal = validated.seal;
3341
+ session.hostFingerprint = validated.metadataFingerprint;
3342
+ } finally {
3343
+ await rm(stagedRoot, { recursive: true, force: true });
3344
+ }
3345
+ return {
3346
+ ...result,
3347
+ command: request.command,
3348
+ sessionReused,
3349
+ workspacePublished: true,
3350
+ workspaceExported: true
3351
+ };
3352
+ } catch (error) {
3353
+ await this.destroySession(session);
3354
+ throw error;
2864
3355
  }
2865
3356
  }
3357
+ async runLocked(request) {
3358
+ const key = runHash(request.runId);
3359
+ const requestFingerprint = this.requestFingerprint(request);
3360
+ let session = this.sessions.get(key);
3361
+ let sessionReused = Boolean(session);
3362
+ let validated;
3363
+ if (session) {
3364
+ const hostFingerprint = await snapshotMetadataFingerprint(request.snapshotRoot, request.limits.maxWorkspaceBytes);
3365
+ if (session.requestFingerprint !== requestFingerprint || session.hostFingerprint !== hostFingerprint) {
3366
+ await this.destroySession(session);
3367
+ session = undefined;
3368
+ sessionReused = false;
3369
+ }
3370
+ }
3371
+ if (!session) {
3372
+ validated = await validateSynchronizedSnapshot(request.snapshotRoot, request.snapshotRoot, request.limits.maxWorkspaceBytes, request.limits.maxFileWriteBytes);
3373
+ session = await this.createSession(request, requestFingerprint, validated.seal, validated.metadataFingerprint);
3374
+ }
3375
+ try {
3376
+ return await this.runInSession(request, session, sessionReused);
3377
+ } catch (error) {
3378
+ if (this.sessions.get(key) === session)
3379
+ await this.destroySession(session).catch(() => {
3380
+ return;
3381
+ });
3382
+ throw error;
3383
+ }
3384
+ }
3385
+ async run(request) {
3386
+ return this.withRunLock(request.runId, () => this.runLocked(request));
3387
+ }
2866
3388
  async removeByFilters(filters) {
2867
3389
  const listed = await spawnBounded2([this.runtime, "ps", "-aq", ...filters.flatMap((filter) => ["--filter", filter])], {
2868
3390
  env: this.hostEnvironment,
@@ -2908,15 +3430,23 @@ ${result.stdout}`)) {
2908
3430
  return removed;
2909
3431
  }
2910
3432
  async removeRunContainers(runId) {
3433
+ const key = runHash(runId);
3434
+ const session = this.sessions.get(key);
3435
+ let cachedResources = 0;
3436
+ if (session) {
3437
+ await this.destroySession(session);
3438
+ cachedResources = 2;
3439
+ }
2911
3440
  const filters = [
2912
3441
  `label=${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`,
2913
- `label=com.zhivex.harness.run=${runHash(runId)}`
3442
+ `label=com.zhivex.harness.run=${key}`
2914
3443
  ];
2915
3444
  const containers = await this.removeByFilters(filters);
2916
3445
  const volumes = await this.removeVolumesByFilters(filters);
2917
- return containers + volumes;
3446
+ return cachedResources + containers + volumes;
2918
3447
  }
2919
3448
  async cleanupOrphans() {
3449
+ this.sessions.clear();
2920
3450
  let removed = 0;
2921
3451
  for (const status of ["created", "exited", "dead", "paused"]) {
2922
3452
  removed += await this.removeByFilters([
@@ -2930,34 +3460,81 @@ ${result.stdout}`)) {
2930
3460
  return removed;
2931
3461
  }
2932
3462
  }
2933
- var collectSnapshotFiles = async (workspace) => {
3463
+ var SNAPSHOT_INVENTORY_PAGE_SIZE = 5000;
3464
+ var COPY_ON_WRITE_UNSUPPORTED_CODES = new Set(["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP"]);
3465
+ var collectSnapshotInventory = async (workspace, metrics) => {
2934
3466
  const files = new Map;
3467
+ if (metrics)
3468
+ metrics.inventoryPasses += 1;
2935
3469
  let cursor;
2936
3470
  do {
2937
- const page = await workspace.listFiles(".", { limit: 500, ...cursor ? { cursor } : {} });
3471
+ const page = await workspace.listFiles(".", {
3472
+ limit: SNAPSHOT_INVENTORY_PAGE_SIZE,
3473
+ ...cursor ? { cursor } : {}
3474
+ });
3475
+ if (metrics)
3476
+ metrics.inventoryPages += 1;
2938
3477
  for (const file of page.files) {
2939
3478
  const absolute = path6.join(workspace.root, ...file.path.split("/"));
2940
3479
  if (!isInside3(workspace.root, absolute))
2941
3480
  throw new Error(`Snapshot file escaped its workspace: ${file.path}.`);
2942
- const before = await lstat4(absolute);
2943
- if (before.isSymbolicLink() || !before.isFile())
3481
+ const entry = await lstat4(absolute);
3482
+ if (entry.isSymbolicLink() || !entry.isFile()) {
2944
3483
  throw new Error(`Snapshot source is not a regular file: ${file.path}.`);
2945
- const contents = await readFile2(absolute);
2946
- const after = await lstat4(absolute);
2947
- if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
2948
- throw new Error(`Snapshot source changed while reading: ${file.path}.`);
2949
3484
  }
2950
- if (digest2(contents) !== file.digest)
2951
- throw new Error(`Snapshot digest changed while reading: ${file.path}.`);
2952
- files.set(file.path, { path: file.path, digest: file.digest, contents, mode: before.mode & 511 });
3485
+ if (entry.size !== file.size) {
3486
+ throw new Error(`Snapshot source changed while inventorying: ${file.path}.`);
3487
+ }
3488
+ files.set(file.path, {
3489
+ path: file.path,
3490
+ digest: file.digest,
3491
+ mode: entry.mode & 511,
3492
+ bytes: file.size
3493
+ });
2953
3494
  }
2954
3495
  cursor = page.nextCursor;
2955
3496
  } while (cursor);
2956
3497
  return files;
2957
3498
  };
2958
- var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceBytes) => {
2959
- const files = await collectSnapshotFiles(source);
2960
- const totalBytes = [...files.values()].reduce((total, file) => total + file.contents.byteLength, 0);
3499
+ var readSnapshotFile = async (workspace, expected, metrics) => {
3500
+ const absolute = path6.join(workspace.root, ...expected.path.split("/"));
3501
+ if (!isInside3(workspace.root, absolute)) {
3502
+ throw new Error(`Snapshot file escaped its workspace: ${expected.path}.`);
3503
+ }
3504
+ const before = await lstat4(absolute);
3505
+ if (before.isSymbolicLink() || !before.isFile()) {
3506
+ throw new Error(`Snapshot source is not a regular file: ${expected.path}.`);
3507
+ }
3508
+ const contents = await readFile2(absolute);
3509
+ const after = await lstat4(absolute);
3510
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || (before.mode & 511) !== (after.mode & 511)) {
3511
+ throw new Error(`Snapshot source changed while reading: ${expected.path}.`);
3512
+ }
3513
+ const actualDigest = digest2(contents);
3514
+ if (actualDigest !== expected.digest || contents.byteLength !== expected.bytes || (before.mode & 511) !== expected.mode) {
3515
+ throw new Error(`Snapshot source changed after inventory: ${expected.path}.`);
3516
+ }
3517
+ if (metrics) {
3518
+ metrics.verifiedContentReads += 1;
3519
+ metrics.verifiedContentBytes += contents.byteLength;
3520
+ }
3521
+ return { ...expected, contents };
3522
+ };
3523
+ var copySnapshotFile = async (source, target, metrics) => {
3524
+ try {
3525
+ await copyFile(source, target, fsConstants.COPYFILE_FICLONE_FORCE);
3526
+ } catch (error) {
3527
+ const code = error.code;
3528
+ if (!code || !COPY_ON_WRITE_UNSUPPORTED_CODES.has(code))
3529
+ throw error;
3530
+ if (metrics)
3531
+ metrics.snapshotCloneFallbacks += 1;
3532
+ await copyFile(source, target);
3533
+ }
3534
+ };
3535
+ var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceBytes, metrics) => {
3536
+ const files = await collectSnapshotInventory(source, metrics);
3537
+ const totalBytes = [...files.values()].reduce((total, file) => total + file.bytes, 0);
2961
3538
  if (totalBytes > maxWorkspaceBytes) {
2962
3539
  throw new Error(`Workspace snapshot exceeds the ${maxWorkspaceBytes}-byte OCI limit.`);
2963
3540
  }
@@ -2965,13 +3542,22 @@ var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceB
2965
3542
  await rm(root, { recursive: true, force: true });
2966
3543
  await privateDirectory(root);
2967
3544
  }
2968
- for (const file of files.values()) {
2969
- for (const root of [baseRoot, snapshotRoot]) {
2970
- const target = path6.join(root, ...file.path.split("/"));
2971
- await mkdir3(path6.dirname(target), { recursive: true, mode: 448 });
2972
- await writeFile(target, file.contents, { mode: file.mode });
2973
- await chmod2(target, file.mode);
2974
- }
3545
+ for (const expected of files.values()) {
3546
+ const file = await readSnapshotFile(source, expected, metrics);
3547
+ const baseTarget = path6.join(baseRoot, ...file.path.split("/"));
3548
+ const snapshotTarget = path6.join(snapshotRoot, ...file.path.split("/"));
3549
+ await Promise.all([
3550
+ mkdir3(path6.dirname(baseTarget), { recursive: true, mode: 448 }),
3551
+ mkdir3(path6.dirname(snapshotTarget), { recursive: true, mode: 448 })
3552
+ ]);
3553
+ await writeFile(baseTarget, file.contents, { mode: file.mode });
3554
+ await chmod2(baseTarget, file.mode);
3555
+ await copySnapshotFile(baseTarget, snapshotTarget, metrics);
3556
+ await chmod2(snapshotTarget, file.mode);
3557
+ }
3558
+ if (metrics) {
3559
+ metrics.snapshotFiles += files.size;
3560
+ metrics.snapshotBytes += totalBytes;
2975
3561
  }
2976
3562
  return { files: files.size, bytes: totalBytes };
2977
3563
  };
@@ -2990,9 +3576,11 @@ var textContent = (file, maxFileWriteBytes) => {
2990
3576
  }
2991
3577
  return file.contents.toString("utf8");
2992
3578
  };
2993
- var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes) => {
2994
- const before = await collectSnapshotFiles(base);
2995
- const after = await collectSnapshotFiles(current);
3579
+ var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes, metrics) => {
3580
+ const [before, after] = await Promise.all([
3581
+ collectSnapshotInventory(base, metrics),
3582
+ collectSnapshotInventory(current, metrics)
3583
+ ]);
2996
3584
  const paths = [...new Set([...before.keys(), ...after.keys()])].sort();
2997
3585
  const entries = [];
2998
3586
  let totalBytes = 0;
@@ -3002,9 +3590,9 @@ var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes) =>
3002
3590
  if (oldFile?.digest === newFile?.digest && oldFile?.mode === newFile?.mode)
3003
3591
  continue;
3004
3592
  const operation = !oldFile ? "create" : !newFile ? "delete" : "update";
3005
- const beforeContent = oldFile ? textContent(oldFile, maxFileWriteBytes) : undefined;
3006
- const afterContent = newFile ? textContent(newFile, maxFileWriteBytes) : undefined;
3007
- const bytes = newFile?.contents.byteLength ?? 0;
3593
+ const beforeContent = oldFile ? textContent(await readSnapshotFile(base, oldFile, metrics), maxFileWriteBytes) : undefined;
3594
+ const afterContent = newFile ? textContent(await readSnapshotFile(current, newFile, metrics), maxFileWriteBytes) : undefined;
3595
+ const bytes = newFile?.bytes ?? 0;
3008
3596
  totalBytes += bytes;
3009
3597
  entries.push({
3010
3598
  path: filePath,
@@ -3044,8 +3632,8 @@ var inspectHostPrecondition = async (workspace, filePath, expectedDigest, expect
3044
3632
  throw error;
3045
3633
  }
3046
3634
  };
3047
- var importPatch = async (runId, expectedPatchId, host, base, current, maxFileWriteBytes) => {
3048
- const patch = await createEnvironmentPatch(runId, base, current, maxFileWriteBytes);
3635
+ var importPatch = async (runId, expectedPatchId, host, base, current, maxFileWriteBytes, metrics) => {
3636
+ const patch = await createEnvironmentPatch(runId, base, current, maxFileWriteBytes, metrics);
3049
3637
  if (patch.patchId !== expectedPatchId) {
3050
3638
  throw new Error("Environment patch changed after review; inspect it again before import.");
3051
3639
  }
@@ -3139,7 +3727,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
3139
3727
  version: `${HARNESS_EXECUTION_POLICY_VERSION}:${image.imageDigest}`,
3140
3728
  backend: "container",
3141
3729
  assurance: "enforced",
3142
- isolation: "per-tool-call",
3730
+ isolation: "per-run",
3143
3731
  workspace: {
3144
3732
  id: workspaceIdentity,
3145
3733
  root: "/workspace",
@@ -3176,6 +3764,8 @@ var createHarnessOciExecutionEnvironment = async (options) => {
3176
3764
  maxPids: options.config.maxPids,
3177
3765
  maxCpus: options.config.maxCpus,
3178
3766
  tmpfsMb: options.config.tmpfsMb,
3767
+ containerLifecycle: "warm-per-acquired-run",
3768
+ failedCommandRecovery: "discard-and-reseed",
3179
3769
  patchImportApproval: true,
3180
3770
  hostMutationMode: "reviewed-environment-patch"
3181
3771
  }
@@ -3187,6 +3777,19 @@ var createHarnessOciExecutionEnvironment = async (options) => {
3187
3777
  image,
3188
3778
  runtime,
3189
3779
  async acquire(request) {
3780
+ const ioMetrics = {
3781
+ inventoryPasses: 0,
3782
+ inventoryPages: 0,
3783
+ verifiedContentReads: 0,
3784
+ verifiedContentBytes: 0,
3785
+ snapshotFiles: 0,
3786
+ snapshotBytes: 0,
3787
+ snapshotCloneFallbacks: 0,
3788
+ containerStarts: 0,
3789
+ containerReuses: 0,
3790
+ workspacePublishes: 0,
3791
+ workspaceExports: 0
3792
+ };
3190
3793
  await privateDirectory(environmentRoot);
3191
3794
  const directory = path6.join(environmentRoot, runHash(request.runId));
3192
3795
  await privateDirectory(directory);
@@ -3205,7 +3808,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
3205
3808
  } catch (error) {
3206
3809
  if (error.code !== "ENOENT")
3207
3810
  throw error;
3208
- const snapshot = await copyWorkspaceSnapshot(options.workspace, baseRoot, snapshotRoot, options.config.maxWorkspaceBytes);
3811
+ const snapshot = await copyWorkspaceSnapshot(options.workspace, baseRoot, snapshotRoot, options.config.maxWorkspaceBytes, ioMetrics);
3209
3812
  const now = new Date().toISOString();
3210
3813
  metadata = {
3211
3814
  schemaVersion: HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION,
@@ -3262,6 +3865,14 @@ var createHarnessOciExecutionEnvironment = async (options) => {
3262
3865
  },
3263
3866
  ...context?.abortSignal ?? request.abortSignal ? { abortSignal: context?.abortSignal ?? request.abortSignal } : {}
3264
3867
  });
3868
+ if (result.sessionReused === false)
3869
+ ioMetrics.containerStarts += 1;
3870
+ if (result.sessionReused === true)
3871
+ ioMetrics.containerReuses += 1;
3872
+ if (result.workspacePublished === true)
3873
+ ioMetrics.workspacePublishes += 1;
3874
+ if (result.workspaceExported === true)
3875
+ ioMetrics.workspaceExports += 1;
3265
3876
  const forcedExitCode = result.cancelled ? 130 : result.timedOut ? 124 : result.outputLimitExceeded ? 125 : result.exitCode;
3266
3877
  return {
3267
3878
  command: result.command,
@@ -3308,7 +3919,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
3308
3919
  return await operation();
3309
3920
  },
3310
3921
  async status() {
3311
- const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes);
3922
+ const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes, ioMetrics);
3312
3923
  return {
3313
3924
  schemaVersion: HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION,
3314
3925
  kind: "environment-status",
@@ -3322,7 +3933,8 @@ var createHarnessOciExecutionEnvironment = async (options) => {
3322
3933
  rootFilesystem: "read-only",
3323
3934
  workspace: "ephemeral-snapshot",
3324
3935
  patchId: patch.patchId,
3325
- changedFiles: patch.entries.length
3936
+ changedFiles: patch.entries.length,
3937
+ io: { ...ioMetrics }
3326
3938
  };
3327
3939
  },
3328
3940
  runCommand,
@@ -3347,7 +3959,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
3347
3959
  return runCommand("bun", ["--no-env-file", "run", check], context);
3348
3960
  },
3349
3961
  async inspectPatch() {
3350
- const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes);
3962
+ const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes, ioMetrics);
3351
3963
  const payload = patchPayload(request.runId, patch.entries);
3352
3964
  return {
3353
3965
  ...payload,
@@ -3356,7 +3968,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
3356
3968
  };
3357
3969
  },
3358
3970
  importPatch(host, patchId) {
3359
- return importPatch(request.runId, patchId, host, base, workspace, options.config.maxFileWriteBytes);
3971
+ return importPatch(request.runId, patchId, host, base, workspace, options.config.maxFileWriteBytes, ioMetrics);
3360
3972
  },
3361
3973
  async release(result) {
3362
3974
  await runtime.removeRunContainers(request.runId);
@@ -3872,7 +4484,7 @@ var HARNESS_SUBAGENT_PROFILE_DESCRIPTORS = [
3872
4484
  toolName: "delegate_explorer",
3873
4485
  description: "Delegate bounded repository discovery to a read-only explorer.",
3874
4486
  instructions: "Inspect the repository with read-only tools. Return evidence with paths and concrete findings. Never propose or perform mutations.",
3875
- toolNames: ["list_files", "read_file", "search_files", "git_diff"]
4487
+ toolNames: ["list_files", "read_file", "read_files", "search_files", "search_many", "git_diff"]
3876
4488
  },
3877
4489
  {
3878
4490
  id: "implementer",
@@ -3886,14 +4498,14 @@ var HARNESS_SUBAGENT_PROFILE_DESCRIPTORS = [
3886
4498
  toolName: "delegate_tester",
3887
4499
  description: "Delegate focused verification to a tester with read-only inspection and approved Bun checks.",
3888
4500
  instructions: "Verify the delegated behavior with repository evidence and declared Bun checks. Do not edit files. Report exact failures and limits.",
3889
- toolNames: ["list_files", "read_file", "search_files", "run_check", "git_diff"]
4501
+ toolNames: ["list_files", "read_file", "read_files", "search_files", "search_many", "run_check", "git_diff"]
3890
4502
  },
3891
4503
  {
3892
4504
  id: "reviewer",
3893
4505
  toolName: "delegate_reviewer",
3894
4506
  description: "Delegate an independent read-only review of correctness, safety, and regressions.",
3895
4507
  instructions: "Review the requested scope independently. Inspect concrete code and diffs, prioritize actionable defects, and do not mutate the workspace.",
3896
- toolNames: ["list_files", "read_file", "search_files", "mutation_audit", "git_diff"]
4508
+ toolNames: ["list_files", "read_file", "read_files", "search_files", "search_many", "mutation_audit", "git_diff"]
3897
4509
  }
3898
4510
  ];
3899
4511
  var pickTools = (tools, names) => {
@@ -4028,8 +4640,513 @@ var runHarnessReviewGroup = async (runtime, input, profiles = ["explorer", "revi
4028
4640
  outputs: result.outputs
4029
4641
  };
4030
4642
  };
4031
- // src/harness.ts
4643
+ // src/change-envelope.ts
4032
4644
  import { createHash as createHash8 } from "crypto";
4645
+ import { z as z3 } from "zod";
4646
+ var CHANGE_ENVELOPE_SCHEMA_VERSION = 1;
4647
+ var CHANGE_ENVELOPE_DIGEST_ALGORITHM = "sha256";
4648
+ var MAX_CHANGE_ENVELOPE_CHECKS = 100;
4649
+ var MAX_CHANGE_ENVELOPE_APPROVALS = 100;
4650
+ var MAX_CHANGE_ENVELOPE_APPROVAL_SCOPES = 32;
4651
+ var MAX_CHANGE_ENVELOPE_ATTESTATIONS = 32;
4652
+ var timestampSchema = z3.iso.datetime({ precision: 3 });
4653
+ var boundedIdentifierSchema = z3.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/, "Identifier must contain only bounded non-secret token characters.");
4654
+ var approvalScopeSchema = z3.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._:/*-]*$/, "Approval scope must contain only bounded token characters.");
4655
+ var attestationFormatSchema = z3.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9.+_-]*$/);
4656
+ var mediaTypeSchema = z3.string().min(3).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/);
4657
+ var addDuplicateIssues = (values, key, context, path8) => {
4658
+ const seen = new Set;
4659
+ for (let index = 0;index < values.length; index += 1) {
4660
+ const value = values[index];
4661
+ if (value === undefined)
4662
+ continue;
4663
+ const identifier2 = key(value);
4664
+ if (seen.has(identifier2)) {
4665
+ context.addIssue({
4666
+ code: "custom",
4667
+ path: [index, path8],
4668
+ message: `Duplicate ${path8}: ${identifier2}.`
4669
+ });
4670
+ }
4671
+ seen.add(identifier2);
4672
+ }
4673
+ };
4674
+ var addCanonicalOrderIssue = (values, key, context, path8) => {
4675
+ for (let index = 1;index < values.length; index += 1) {
4676
+ const previous = values[index - 1];
4677
+ const current = values[index];
4678
+ if (previous !== undefined && current !== undefined && key(previous) >= key(current)) {
4679
+ context.addIssue({
4680
+ code: "custom",
4681
+ path: [index, path8],
4682
+ message: `${path8} entries must be unique and in canonical ascending order.`
4683
+ });
4684
+ }
4685
+ }
4686
+ };
4687
+ var approvalScopesInputSchema = z3.array(approvalScopeSchema).min(1).max(MAX_CHANGE_ENVELOPE_APPROVAL_SCOPES).superRefine((scopes, context) => addDuplicateIssues(scopes, (scope) => scope, context, "scope"));
4688
+ var canonicalApprovalScopesSchema = z3.array(approvalScopeSchema).min(1).max(MAX_CHANGE_ENVELOPE_APPROVAL_SCOPES).superRefine((scopes, context) => addCanonicalOrderIssue(scopes, (scope) => scope, context, "scope"));
4689
+ var changeEnvelopeCheckSchema = z3.strictObject({
4690
+ checkId: boundedIdentifierSchema,
4691
+ status: z3.enum(["passed", "failed", "skipped"]),
4692
+ redacted: z3.literal(true),
4693
+ startedAt: timestampSchema,
4694
+ completedAt: timestampSchema,
4695
+ durationMs: z3.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
4696
+ exitCode: z3.number().int().min(0).max(255).optional(),
4697
+ redactedOutputDigest: fileDigestSchema.optional(),
4698
+ runnerFingerprint: fileDigestSchema.optional()
4699
+ }).superRefine((check, context) => {
4700
+ const startedAt = Date.parse(check.startedAt);
4701
+ const completedAt = Date.parse(check.completedAt);
4702
+ if (completedAt < startedAt) {
4703
+ context.addIssue({
4704
+ code: "custom",
4705
+ path: ["completedAt"],
4706
+ message: "Check completion cannot precede its start."
4707
+ });
4708
+ }
4709
+ if (check.status === "passed" && check.exitCode !== undefined && check.exitCode !== 0) {
4710
+ context.addIssue({
4711
+ code: "custom",
4712
+ path: ["exitCode"],
4713
+ message: "A passed check cannot have a non-zero exit code."
4714
+ });
4715
+ }
4716
+ if (check.status === "failed" && check.exitCode === 0) {
4717
+ context.addIssue({
4718
+ code: "custom",
4719
+ path: ["exitCode"],
4720
+ message: "A failed check cannot have a zero exit code."
4721
+ });
4722
+ }
4723
+ if (check.status === "skipped" && check.exitCode !== undefined) {
4724
+ context.addIssue({
4725
+ code: "custom",
4726
+ path: ["exitCode"],
4727
+ message: "A skipped check cannot have an exit code."
4728
+ });
4729
+ }
4730
+ });
4731
+ var checkInputListSchema = z3.array(changeEnvelopeCheckSchema).min(1).max(MAX_CHANGE_ENVELOPE_CHECKS).superRefine((checks, context) => addDuplicateIssues(checks, (check) => check.checkId, context, "checkId"));
4732
+ var canonicalCheckListSchema = z3.array(changeEnvelopeCheckSchema).min(1).max(MAX_CHANGE_ENVELOPE_CHECKS).superRefine((checks, context) => addCanonicalOrderIssue(checks, (check) => check.checkId, context, "checkId"));
4733
+ var changeEnvelopeApprovalSchema = z3.strictObject({
4734
+ kind: z3.literal("approval-reference"),
4735
+ approvalId: boundedIdentifierSchema,
4736
+ approvalDigest: fileDigestSchema,
4737
+ approverDigest: fileDigestSchema,
4738
+ scopes: canonicalApprovalScopesSchema,
4739
+ approvedAt: timestampSchema,
4740
+ expiresAt: timestampSchema.optional()
4741
+ }).superRefine((approval, context) => {
4742
+ if (approval.expiresAt && Date.parse(approval.expiresAt) <= Date.parse(approval.approvedAt)) {
4743
+ context.addIssue({
4744
+ code: "custom",
4745
+ path: ["expiresAt"],
4746
+ message: "Approval expiry must be later than approval time."
4747
+ });
4748
+ }
4749
+ });
4750
+ var changeEnvelopeApprovalInputSchema = z3.strictObject({
4751
+ kind: z3.literal("approval-reference"),
4752
+ approvalId: boundedIdentifierSchema,
4753
+ approvalDigest: fileDigestSchema,
4754
+ approverDigest: fileDigestSchema,
4755
+ scopes: approvalScopesInputSchema,
4756
+ approvedAt: timestampSchema,
4757
+ expiresAt: timestampSchema.optional()
4758
+ }).superRefine((approval, context) => {
4759
+ if (approval.expiresAt && Date.parse(approval.expiresAt) <= Date.parse(approval.approvedAt)) {
4760
+ context.addIssue({
4761
+ code: "custom",
4762
+ path: ["expiresAt"],
4763
+ message: "Approval expiry must be later than approval time."
4764
+ });
4765
+ }
4766
+ });
4767
+ var approvalInputListSchema = z3.array(changeEnvelopeApprovalInputSchema).min(1).max(MAX_CHANGE_ENVELOPE_APPROVALS).superRefine((approvals, context) => addDuplicateIssues(approvals, (approval) => approval.approvalId, context, "approvalId"));
4768
+ var canonicalApprovalListSchema = z3.array(changeEnvelopeApprovalSchema).min(1).max(MAX_CHANGE_ENVELOPE_APPROVALS).superRefine((approvals, context) => addCanonicalOrderIssue(approvals, (approval) => approval.approvalId, context, "approvalId"));
4769
+ var externalAttestationReferenceSchema = z3.strictObject({
4770
+ kind: z3.literal("external-attestation-reference"),
4771
+ attestationId: boundedIdentifierSchema,
4772
+ format: attestationFormatSchema,
4773
+ mediaType: mediaTypeSchema,
4774
+ bundleDigest: fileDigestSchema
4775
+ });
4776
+ var externalAttestationInputListSchema = z3.array(externalAttestationReferenceSchema).min(1).max(MAX_CHANGE_ENVELOPE_ATTESTATIONS).superRefine((attestations, context) => addDuplicateIssues(attestations, (attestation) => attestation.attestationId, context, "attestationId"));
4777
+ var canonicalExternalAttestationListSchema = z3.array(externalAttestationReferenceSchema).min(1).max(MAX_CHANGE_ENVELOPE_ATTESTATIONS).superRefine((attestations, context) => addCanonicalOrderIssue(attestations, (attestation) => attestation.attestationId, context, "attestationId"));
4778
+ var changeEnvelopeBaseSchema = z3.strictObject({
4779
+ workspaceDigest: fileDigestSchema,
4780
+ treeDigest: fileDigestSchema
4781
+ });
4782
+ var changeEnvelopePatchSchema = z3.strictObject({
4783
+ patchId: boundedIdentifierSchema,
4784
+ patchDigest: fileDigestSchema
4785
+ });
4786
+ var changeEnvelopeFingerprintsSchema = z3.strictObject({
4787
+ harness: fileDigestSchema,
4788
+ policy: fileDigestSchema,
4789
+ environment: fileDigestSchema
4790
+ });
4791
+ var changeEnvelopeEvidenceSchema = z3.strictObject({
4792
+ schemaVersion: z3.literal(CHANGE_ENVELOPE_SCHEMA_VERSION),
4793
+ kind: z3.literal("check-evidence-summary"),
4794
+ digestAlgorithm: z3.literal(CHANGE_ENVELOPE_DIGEST_ALGORITHM),
4795
+ evidenceDigest: fileDigestSchema,
4796
+ checks: canonicalCheckListSchema
4797
+ });
4798
+ var changeEnvelopeInputSchema = z3.strictObject({
4799
+ createdAt: timestampSchema,
4800
+ expiresAt: timestampSchema.optional(),
4801
+ base: changeEnvelopeBaseSchema,
4802
+ patch: changeEnvelopePatchSchema,
4803
+ fingerprints: changeEnvelopeFingerprintsSchema,
4804
+ checks: checkInputListSchema,
4805
+ approvals: approvalInputListSchema.optional(),
4806
+ externalAttestations: externalAttestationInputListSchema.optional()
4807
+ });
4808
+ var changeEnvelopeSchema = z3.strictObject({
4809
+ schemaVersion: z3.literal(CHANGE_ENVELOPE_SCHEMA_VERSION),
4810
+ kind: z3.literal("change-envelope"),
4811
+ digestAlgorithm: z3.literal(CHANGE_ENVELOPE_DIGEST_ALGORITHM),
4812
+ envelopeId: fileDigestSchema,
4813
+ createdAt: timestampSchema,
4814
+ expiresAt: timestampSchema.optional(),
4815
+ base: changeEnvelopeBaseSchema,
4816
+ patch: changeEnvelopePatchSchema,
4817
+ fingerprints: changeEnvelopeFingerprintsSchema,
4818
+ evidence: changeEnvelopeEvidenceSchema,
4819
+ approvals: canonicalApprovalListSchema.optional(),
4820
+ externalAttestations: canonicalExternalAttestationListSchema.optional()
4821
+ }).superRefine((envelope, context) => {
4822
+ if (envelope.expiresAt && Date.parse(envelope.expiresAt) <= Date.parse(envelope.createdAt)) {
4823
+ context.addIssue({
4824
+ code: "custom",
4825
+ path: ["expiresAt"],
4826
+ message: "Envelope expiry must be later than creation time."
4827
+ });
4828
+ }
4829
+ for (let index = 0;index < envelope.evidence.checks.length; index += 1) {
4830
+ const check = envelope.evidence.checks[index];
4831
+ if (check && Date.parse(check.completedAt) > Date.parse(envelope.createdAt)) {
4832
+ context.addIssue({
4833
+ code: "custom",
4834
+ path: ["evidence", "checks", index, "completedAt"],
4835
+ message: "Check completion cannot be later than envelope creation time."
4836
+ });
4837
+ }
4838
+ }
4839
+ for (let index = 0;index < (envelope.approvals?.length ?? 0); index += 1) {
4840
+ const approval = envelope.approvals?.[index];
4841
+ if (approval && Date.parse(approval.approvedAt) > Date.parse(envelope.createdAt)) {
4842
+ context.addIssue({
4843
+ code: "custom",
4844
+ path: ["approvals", index, "approvedAt"],
4845
+ message: "Approval time cannot be later than envelope creation time."
4846
+ });
4847
+ }
4848
+ }
4849
+ });
4850
+ var canonicalJson = (value) => {
4851
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
4852
+ return JSON.stringify(value);
4853
+ }
4854
+ if (typeof value === "number") {
4855
+ if (!Number.isFinite(value))
4856
+ throw new TypeError("Canonical JSON cannot encode a non-finite number.");
4857
+ return JSON.stringify(Object.is(value, -0) ? 0 : value);
4858
+ }
4859
+ if (Array.isArray(value)) {
4860
+ return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
4861
+ }
4862
+ if (typeof value === "object") {
4863
+ const record = value;
4864
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
4865
+ }
4866
+ throw new TypeError(`Canonical JSON cannot encode ${typeof value}.`);
4867
+ };
4868
+ var digestCanonical = (value) => `sha256:${createHash8("sha256").update(canonicalJson(value)).digest("hex")}`;
4869
+ var digestChangeEnvelopeArtifact = (value) => `sha256:${createHash8("sha256").update(value).digest("hex")}`;
4870
+ var evidencePayload = (checks) => ({
4871
+ schemaVersion: CHANGE_ENVELOPE_SCHEMA_VERSION,
4872
+ kind: "check-evidence-summary",
4873
+ digestAlgorithm: CHANGE_ENVELOPE_DIGEST_ALGORITHM,
4874
+ checks: [...checks]
4875
+ });
4876
+ var envelopePayload = (envelope) => {
4877
+ const { envelopeId: _envelopeId, ...payload } = envelope;
4878
+ return payload;
4879
+ };
4880
+ var sortBy = (values, key) => [...values].sort((left, right) => {
4881
+ const leftKey = key(left);
4882
+ const rightKey = key(right);
4883
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
4884
+ });
4885
+ var createChangeEnvelope = (input) => {
4886
+ const parsed = changeEnvelopeInputSchema.parse(input);
4887
+ const checks = sortBy(parsed.checks, (check) => check.checkId);
4888
+ const evidenceWithoutDigest = evidencePayload(checks);
4889
+ const evidence = {
4890
+ ...evidenceWithoutDigest,
4891
+ evidenceDigest: digestCanonical(evidenceWithoutDigest)
4892
+ };
4893
+ const approvals = parsed.approvals ? sortBy(parsed.approvals, (approval) => approval.approvalId).map((approval) => ({
4894
+ ...approval,
4895
+ scopes: [...approval.scopes].sort()
4896
+ })) : undefined;
4897
+ const externalAttestations = parsed.externalAttestations ? sortBy(parsed.externalAttestations, (attestation) => attestation.attestationId) : undefined;
4898
+ const payload = {
4899
+ schemaVersion: CHANGE_ENVELOPE_SCHEMA_VERSION,
4900
+ kind: "change-envelope",
4901
+ digestAlgorithm: CHANGE_ENVELOPE_DIGEST_ALGORITHM,
4902
+ createdAt: parsed.createdAt,
4903
+ ...parsed.expiresAt ? { expiresAt: parsed.expiresAt } : {},
4904
+ base: parsed.base,
4905
+ patch: parsed.patch,
4906
+ fingerprints: parsed.fingerprints,
4907
+ evidence,
4908
+ ...approvals ? { approvals } : {},
4909
+ ...externalAttestations ? { externalAttestations } : {}
4910
+ };
4911
+ return changeEnvelopeSchema.parse({
4912
+ ...payload,
4913
+ envelopeId: digestCanonical(payload)
4914
+ });
4915
+ };
4916
+ var canonicalizeChangeEnvelope = (input) => canonicalJson(changeEnvelopeSchema.parse(input));
4917
+ var computeChangeEnvelopeDigest = (input) => {
4918
+ const parsed = changeEnvelopeSchema.parse(input);
4919
+ return digestCanonical(envelopePayload(parsed));
4920
+ };
4921
+ var computeChangeEnvelopeEvidenceDigest = (input) => {
4922
+ const parsed = changeEnvelopeEvidenceSchema.parse(input);
4923
+ return digestCanonical(evidencePayload(parsed.checks));
4924
+ };
4925
+ var changeEnvelopePreconditionsSchema = z3.strictObject({
4926
+ envelopeId: fileDigestSchema.optional(),
4927
+ baseWorkspaceDigest: fileDigestSchema.optional(),
4928
+ baseTreeDigest: fileDigestSchema.optional(),
4929
+ patchId: boundedIdentifierSchema.optional(),
4930
+ patchDigest: fileDigestSchema.optional(),
4931
+ harnessFingerprint: fileDigestSchema.optional(),
4932
+ policyFingerprint: fileDigestSchema.optional(),
4933
+ environmentFingerprint: fileDigestSchema.optional(),
4934
+ evidenceDigest: fileDigestSchema.optional(),
4935
+ requiredPassedChecks: z3.array(boundedIdentifierSchema).max(MAX_CHANGE_ENVELOPE_CHECKS).optional(),
4936
+ requiredApprovalDigests: z3.array(fileDigestSchema).max(MAX_CHANGE_ENVELOPE_APPROVALS).optional(),
4937
+ requiredApprovalScopes: z3.array(approvalScopeSchema).max(MAX_CHANGE_ENVELOPE_APPROVAL_SCOPES).optional(),
4938
+ requiredAttestationDigests: z3.array(fileDigestSchema).max(MAX_CHANGE_ENVELOPE_ATTESTATIONS).optional()
4939
+ }).superRefine((preconditions, context) => {
4940
+ if (preconditions.requiredPassedChecks) {
4941
+ addDuplicateIssues(preconditions.requiredPassedChecks, (checkId) => checkId, context, "requiredPassedCheck");
4942
+ }
4943
+ if (preconditions.requiredApprovalDigests) {
4944
+ addDuplicateIssues(preconditions.requiredApprovalDigests, (digest3) => digest3, context, "requiredApprovalDigest");
4945
+ }
4946
+ if (preconditions.requiredApprovalScopes) {
4947
+ addDuplicateIssues(preconditions.requiredApprovalScopes, (scope) => scope, context, "requiredApprovalScope");
4948
+ }
4949
+ if (preconditions.requiredAttestationDigests) {
4950
+ addDuplicateIssues(preconditions.requiredAttestationDigests, (digest3) => digest3, context, "requiredAttestationDigest");
4951
+ }
4952
+ });
4953
+ var verificationOptionsSchema = z3.strictObject({
4954
+ now: z3.union([z3.date(), z3.number().int(), timestampSchema]).optional(),
4955
+ preconditions: changeEnvelopePreconditionsSchema.optional()
4956
+ });
4957
+ var verificationTime = (value) => {
4958
+ const date = value === undefined ? new Date : value instanceof Date ? value : new Date(value);
4959
+ if (!Number.isFinite(date.getTime()))
4960
+ throw new TypeError("Verification time must be a valid date.");
4961
+ return date;
4962
+ };
4963
+ var verifyChangeEnvelope = (input, options = {}) => {
4964
+ const parsedOptions = verificationOptionsSchema.parse(options);
4965
+ const now = verificationTime(parsedOptions.now);
4966
+ const checkedAt = now.toISOString();
4967
+ const parsedEnvelope = changeEnvelopeSchema.safeParse(input);
4968
+ if (!parsedEnvelope.success) {
4969
+ return {
4970
+ valid: false,
4971
+ verificationScope: "integrity-expiration-and-preconditions-only",
4972
+ issues: parsedEnvelope.error.issues.map((issue) => ({
4973
+ code: "invalid-schema",
4974
+ path: issue.path.map((segment) => typeof segment === "symbol" ? segment.description ?? "symbol" : segment),
4975
+ message: issue.message
4976
+ })),
4977
+ integrity: { envelopeDigestValid: false, evidenceDigestValid: false },
4978
+ expiration: { checkedAt, envelopeNotYetValid: false, envelopeExpired: false, expiredApprovalIds: [] },
4979
+ checks: {
4980
+ recorded: 0,
4981
+ passed: [],
4982
+ failed: [],
4983
+ skipped: [],
4984
+ requiredPassed: [...parsedOptions.preconditions?.requiredPassedChecks ?? []].sort(),
4985
+ missingPassed: [...parsedOptions.preconditions?.requiredPassedChecks ?? []].sort()
4986
+ },
4987
+ approvals: {
4988
+ recorded: 0,
4989
+ requiredScopes: [...parsedOptions.preconditions?.requiredApprovalScopes ?? []].sort(),
4990
+ missingScopes: [...parsedOptions.preconditions?.requiredApprovalScopes ?? []].sort(),
4991
+ authenticity: "not-verified"
4992
+ },
4993
+ externalAttestations: {
4994
+ referenced: 0,
4995
+ requiredDigests: [...parsedOptions.preconditions?.requiredAttestationDigests ?? []].sort(),
4996
+ missingDigests: [...parsedOptions.preconditions?.requiredAttestationDigests ?? []].sort(),
4997
+ authenticity: "not-verified"
4998
+ }
4999
+ };
5000
+ }
5001
+ const envelope = parsedEnvelope.data;
5002
+ const issues = [];
5003
+ const computedEvidenceDigest = digestCanonical(evidencePayload(envelope.evidence.checks));
5004
+ const evidenceDigestValid = computedEvidenceDigest === envelope.evidence.evidenceDigest;
5005
+ if (!evidenceDigestValid) {
5006
+ issues.push({
5007
+ code: "evidence-digest-mismatch",
5008
+ path: ["evidence", "evidenceDigest"],
5009
+ message: "Evidence digest does not match the canonical redacted check summary.",
5010
+ expected: computedEvidenceDigest,
5011
+ actual: envelope.evidence.evidenceDigest
5012
+ });
5013
+ }
5014
+ const computedEnvelopeDigest = digestCanonical(envelopePayload(envelope));
5015
+ const envelopeDigestValid = computedEnvelopeDigest === envelope.envelopeId;
5016
+ if (!envelopeDigestValid) {
5017
+ issues.push({
5018
+ code: "envelope-digest-mismatch",
5019
+ path: ["envelopeId"],
5020
+ message: "Envelope digest does not match its canonical payload.",
5021
+ expected: computedEnvelopeDigest,
5022
+ actual: envelope.envelopeId
5023
+ });
5024
+ }
5025
+ const envelopeNotYetValid = now.getTime() < Date.parse(envelope.createdAt);
5026
+ if (envelopeNotYetValid) {
5027
+ issues.push({
5028
+ code: "envelope-not-yet-valid",
5029
+ path: ["createdAt"],
5030
+ message: "Change envelope creation time is in the future."
5031
+ });
5032
+ }
5033
+ const envelopeExpired = envelope.expiresAt !== undefined && now.getTime() >= Date.parse(envelope.expiresAt);
5034
+ if (envelopeExpired) {
5035
+ issues.push({
5036
+ code: "envelope-expired",
5037
+ path: ["expiresAt"],
5038
+ message: "Change envelope has expired."
5039
+ });
5040
+ }
5041
+ const expiredApprovalIds = (envelope.approvals ?? []).filter((approval) => approval.expiresAt !== undefined && now.getTime() >= Date.parse(approval.expiresAt)).map((approval) => approval.approvalId);
5042
+ for (const approvalId of expiredApprovalIds) {
5043
+ issues.push({
5044
+ code: "approval-expired",
5045
+ path: ["approvals", envelope.approvals?.findIndex((approval) => approval.approvalId === approvalId) ?? 0, "expiresAt"],
5046
+ message: `Recorded approval has expired: ${approvalId}.`
5047
+ });
5048
+ }
5049
+ const preconditions = parsedOptions.preconditions;
5050
+ const comparePrecondition = (field, expected, actual, path8) => {
5051
+ if (expected !== undefined && expected !== actual) {
5052
+ issues.push({
5053
+ code: "precondition-mismatch",
5054
+ path: path8,
5055
+ message: `Change envelope precondition does not match: ${field}.`,
5056
+ expected,
5057
+ actual
5058
+ });
5059
+ }
5060
+ };
5061
+ comparePrecondition("envelopeId", preconditions?.envelopeId, envelope.envelopeId, ["envelopeId"]);
5062
+ comparePrecondition("baseWorkspaceDigest", preconditions?.baseWorkspaceDigest, envelope.base.workspaceDigest, ["base", "workspaceDigest"]);
5063
+ comparePrecondition("baseTreeDigest", preconditions?.baseTreeDigest, envelope.base.treeDigest, ["base", "treeDigest"]);
5064
+ comparePrecondition("patchId", preconditions?.patchId, envelope.patch.patchId, ["patch", "patchId"]);
5065
+ comparePrecondition("patchDigest", preconditions?.patchDigest, envelope.patch.patchDigest, ["patch", "patchDigest"]);
5066
+ comparePrecondition("harnessFingerprint", preconditions?.harnessFingerprint, envelope.fingerprints.harness, ["fingerprints", "harness"]);
5067
+ comparePrecondition("policyFingerprint", preconditions?.policyFingerprint, envelope.fingerprints.policy, ["fingerprints", "policy"]);
5068
+ comparePrecondition("environmentFingerprint", preconditions?.environmentFingerprint, envelope.fingerprints.environment, ["fingerprints", "environment"]);
5069
+ comparePrecondition("evidenceDigest", preconditions?.evidenceDigest, envelope.evidence.evidenceDigest, ["evidence", "evidenceDigest"]);
5070
+ const passedChecks = envelope.evidence.checks.filter((check) => check.status === "passed").map((check) => check.checkId);
5071
+ const failedChecks = envelope.evidence.checks.filter((check) => check.status === "failed").map((check) => check.checkId);
5072
+ const skippedChecks = envelope.evidence.checks.filter((check) => check.status === "skipped").map((check) => check.checkId);
5073
+ const passedCheckSet = new Set(passedChecks);
5074
+ const requiredPassed = [...preconditions?.requiredPassedChecks ?? []].sort();
5075
+ const missingPassed = requiredPassed.filter((checkId) => !passedCheckSet.has(checkId));
5076
+ for (const checkId of missingPassed) {
5077
+ issues.push({
5078
+ code: "missing-passed-check",
5079
+ path: ["evidence", "checks"],
5080
+ message: `Required passed check is not recorded: ${checkId}.`,
5081
+ expected: checkId
5082
+ });
5083
+ }
5084
+ const expiredApprovals = new Set(expiredApprovalIds);
5085
+ const activeApprovals = (envelope.approvals ?? []).filter((approval) => !expiredApprovals.has(approval.approvalId));
5086
+ const approvalDigests = new Set(activeApprovals.map((approval) => approval.approvalDigest));
5087
+ for (const requiredDigest of preconditions?.requiredApprovalDigests ?? []) {
5088
+ if (!approvalDigests.has(requiredDigest)) {
5089
+ issues.push({
5090
+ code: "missing-approval-digest",
5091
+ path: ["approvals"],
5092
+ message: `Required active approval digest is not recorded: ${requiredDigest}.`,
5093
+ expected: requiredDigest
5094
+ });
5095
+ }
5096
+ }
5097
+ const approvedScopes = new Set(activeApprovals.flatMap((approval) => approval.scopes));
5098
+ const requiredScopes = [...preconditions?.requiredApprovalScopes ?? []].sort();
5099
+ const missingScopes = requiredScopes.filter((scope) => !approvedScopes.has(scope));
5100
+ for (const scope of missingScopes) {
5101
+ issues.push({
5102
+ code: "missing-approval-scope",
5103
+ path: ["approvals"],
5104
+ message: `Required active approval scope is not recorded: ${scope}.`,
5105
+ expected: scope
5106
+ });
5107
+ }
5108
+ const attestationDigests = new Set((envelope.externalAttestations ?? []).map((attestation) => attestation.bundleDigest));
5109
+ const requiredAttestationDigests = [...preconditions?.requiredAttestationDigests ?? []].sort();
5110
+ const missingAttestationDigests = requiredAttestationDigests.filter((digest3) => !attestationDigests.has(digest3));
5111
+ for (const requiredDigest of missingAttestationDigests) {
5112
+ issues.push({
5113
+ code: "missing-attestation-digest",
5114
+ path: ["externalAttestations"],
5115
+ message: `Required external attestation digest is not referenced: ${requiredDigest}.`,
5116
+ expected: requiredDigest
5117
+ });
5118
+ }
5119
+ return {
5120
+ valid: issues.length === 0,
5121
+ verificationScope: "integrity-expiration-and-preconditions-only",
5122
+ envelope,
5123
+ issues,
5124
+ integrity: { envelopeDigestValid, evidenceDigestValid },
5125
+ expiration: { checkedAt, envelopeNotYetValid, envelopeExpired, expiredApprovalIds },
5126
+ checks: {
5127
+ recorded: envelope.evidence.checks.length,
5128
+ passed: passedChecks,
5129
+ failed: failedChecks,
5130
+ skipped: skippedChecks,
5131
+ requiredPassed,
5132
+ missingPassed
5133
+ },
5134
+ approvals: {
5135
+ recorded: envelope.approvals?.length ?? 0,
5136
+ requiredScopes,
5137
+ missingScopes,
5138
+ authenticity: "not-verified"
5139
+ },
5140
+ externalAttestations: {
5141
+ referenced: envelope.externalAttestations?.length ?? 0,
5142
+ requiredDigests: requiredAttestationDigests,
5143
+ missingDigests: missingAttestationDigests,
5144
+ authenticity: "not-verified"
5145
+ }
5146
+ };
5147
+ };
5148
+ // src/harness.ts
5149
+ import { createHash as createHash9 } from "crypto";
4033
5150
  import {
4034
5151
  Agent,
4035
5152
  applySafetyPolicyToAgent as applySafetyPolicyToAgent2,
@@ -4044,7 +5161,7 @@ import {
4044
5161
  estimateTokenCost
4045
5162
  } from "@zhivex-ai/agents/ops";
4046
5163
  import { serializeJsonValue as serializeJsonValue2 } from "@zhivex-ai/core";
4047
- import { z as z3 } from "zod";
5164
+ import { z as z4 } from "zod";
4048
5165
 
4049
5166
  // src/operations.ts
4050
5167
  import { chmod as chmod3, lstat as lstat6, mkdir as mkdir4, open as open3 } from "fs/promises";
@@ -4357,12 +5474,12 @@ var openHarnessPersistence = async (config, options = {}) => {
4357
5474
 
4358
5475
  // src/harness.ts
4359
5476
  var APPROVAL_VERSION = "2026-08-17-v5";
4360
- var TOOL_CONTRACT_VERSION = "enforced-execution-v1";
5477
+ var TOOL_CONTRACT_VERSION = "workspace-batch-v2";
4361
5478
  var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, providerTransportFingerprint, executionEnvironment) => ({
4362
5479
  schemaVersion: 1,
4363
5480
  id: "zhivex-harness",
4364
5481
  version: HARNESS_VERSION,
4365
- fingerprint: `sha256:${createHash8("sha256").update(JSON.stringify({
5482
+ fingerprint: `sha256:${createHash9("sha256").update(JSON.stringify({
4366
5483
  configSchemaVersion: HARNESS_CONFIG_SCHEMA_VERSION,
4367
5484
  approvalVersion: APPROVAL_VERSION,
4368
5485
  toolContractVersion: TOOL_CONTRACT_VERSION,
@@ -4387,7 +5504,7 @@ var HARNESS_INSTRUCTIONS = `You are Zhivex Harness, a provider-portable coding a
4387
5504
 
4388
5505
  Rules:
4389
5506
  - Match the user's language.
4390
- - Inspect the repository before proposing changes. Prefer search_files and read_file over assumptions.
5507
+ - 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.
4391
5508
  - Use only workspace-relative paths. Never request or expose secrets.
4392
5509
  - Make the smallest coherent change that fully addresses the task.
4393
5510
  - For every file edit, first read its digest and call propose_edits. Apply exactly that reviewed proposal with apply_patch.
@@ -4437,10 +5554,10 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
4437
5554
  list_files: tool({
4438
5555
  name: "list_files",
4439
5556
  description: "List regular files with content digests using a stable cursor. Build artifacts, dependencies, Git internals, and harness state are ignored.",
4440
- schema: z3.object({
4441
- path: z3.string().min(1).default("."),
4442
- limit: z3.number().int().min(1).max(500).default(200),
4443
- cursor: z3.string().min(1).max(2000).optional()
5557
+ schema: z4.object({
5558
+ path: z4.string().min(1).default("."),
5559
+ limit: z4.number().int().min(1).max(500).default(200),
5560
+ cursor: z4.string().min(1).max(2000).optional()
4444
5561
  }),
4445
5562
  metadata: readOnlyMetadata,
4446
5563
  execute: async ({ path: path9, limit, cursor }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).listFiles(path9, {
@@ -4451,23 +5568,40 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
4451
5568
  read_file: tool({
4452
5569
  name: "read_file",
4453
5570
  description: "Read a bounded, line-numbered slice and SHA-256 digest of one UTF-8 text file using a workspace-relative path.",
4454
- schema: z3.object({
4455
- path: z3.string().min(1),
4456
- startLine: z3.number().int().min(1).default(1),
4457
- endLine: z3.number().int().min(1).optional()
5571
+ schema: z4.object({
5572
+ path: z4.string().min(1),
5573
+ startLine: z4.number().int().min(1).default(1),
5574
+ endLine: z4.number().int().min(1).optional()
4458
5575
  }),
4459
5576
  metadata: readOnlyMetadata,
4460
5577
  execute: async ({ path: path9, startLine, endLine }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).readFile(path9, startLine, endLine))
4461
5578
  }),
5579
+ read_files: tool({
5580
+ name: "read_files",
5581
+ 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.",
5582
+ schema: z4.object({
5583
+ files: z4.array(z4.object({
5584
+ path: z4.string().min(1),
5585
+ startLine: z4.number().int().min(1).default(1),
5586
+ endLine: z4.number().int().min(1).optional()
5587
+ })).min(1).max(20)
5588
+ }),
5589
+ metadata: readOnlyMetadata,
5590
+ execute: async ({ files }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).readFiles(files.map(({ path: path9, startLine, endLine }) => ({
5591
+ path: path9,
5592
+ startLine,
5593
+ ...endLine !== undefined ? { endLine } : {}
5594
+ }))))
5595
+ }),
4462
5596
  search_files: tool({
4463
5597
  name: "search_files",
4464
5598
  description: "Search for a literal string in text files using a stable cursor.",
4465
- schema: z3.object({
4466
- query: z3.string().min(1).max(200),
4467
- path: z3.string().min(1).default("."),
4468
- caseSensitive: z3.boolean().default(false),
4469
- limit: z3.number().int().min(1).max(500).default(100),
4470
- cursor: z3.string().min(1).max(2000).optional()
5599
+ schema: z4.object({
5600
+ query: z4.string().min(1).max(200),
5601
+ path: z4.string().min(1).default("."),
5602
+ caseSensitive: z4.boolean().default(false),
5603
+ limit: z4.number().int().min(1).max(500).default(100),
5604
+ cursor: z4.string().min(1).max(2000).optional()
4471
5605
  }),
4472
5606
  metadata: readOnlyMetadata,
4473
5607
  execute: async ({ query, path: path9, caseSensitive, limit, cursor }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).searchFiles(query, path9, {
@@ -4476,6 +5610,28 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
4476
5610
  ...cursor ? { cursor } : {}
4477
5611
  }))
4478
5612
  }),
5613
+ search_many: tool({
5614
+ name: "search_many",
5615
+ description: "Search up to 10 independent literal queries in one workspace pass, with at most 500 aggregate matches.",
5616
+ schema: z4.object({
5617
+ queries: z4.array(z4.object({
5618
+ query: z4.string().min(1).max(200),
5619
+ caseSensitive: z4.boolean().default(false)
5620
+ })).min(1).max(10),
5621
+ path: z4.string().min(1).default("."),
5622
+ limitPerQuery: z4.number().int().min(1).max(500).default(50)
5623
+ }).superRefine((input, context) => {
5624
+ if (input.queries.length * input.limitPerQuery > 500) {
5625
+ context.addIssue({
5626
+ code: "custom",
5627
+ path: ["limitPerQuery"],
5628
+ message: "search_many allows at most 500 aggregate matches."
5629
+ });
5630
+ }
5631
+ }),
5632
+ metadata: readOnlyMetadata,
5633
+ execute: async ({ queries, path: path9, limitPerQuery }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).searchMany(queries, path9, { limitPerQuery }))
5634
+ }),
4479
5635
  propose_edits: tool({
4480
5636
  name: "propose_edits",
4481
5637
  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.",
@@ -4520,9 +5676,9 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
4520
5676
  run_check: tool({
4521
5677
  name: "run_check",
4522
5678
  description: `Run one explicitly allowed package.json script through Bun (${allowedChecks.join(", ")}). Read package.json first and pass its exact script text as expectedScript so the operator can review the command. No arbitrary shell or .env loading is exposed.`,
4523
- schema: z3.object({
4524
- check: z3.string().min(1).max(100).regex(/^[A-Za-z0-9:_-]+$/),
4525
- expectedScript: z3.string().min(1).max(2000)
5679
+ schema: z4.object({
5680
+ check: z4.string().min(1).max(100).regex(/^[A-Za-z0-9:_-]+$/),
5681
+ expectedScript: z4.string().min(1).max(2000)
4526
5682
  }),
4527
5683
  requiresApproval: true,
4528
5684
  approvalMode: "interrupt",
@@ -4536,14 +5692,14 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
4536
5692
  mutation_audit: tool({
4537
5693
  name: "mutation_audit",
4538
5694
  description: "Inspect the immutable in-memory audit journal for file mutations made by this harness instance.",
4539
- schema: z3.object({}),
5695
+ schema: z4.object({}),
4540
5696
  metadata: readOnlyMetadata,
4541
5697
  execute: async (_input, context) => serializeJsonValue2(editContractDocument("mutation-audit", (harnessExecutionSession(context)?.workspace ?? workspace).mutationAudit()))
4542
5698
  }),
4543
5699
  git_diff: tool({
4544
5700
  name: "git_diff",
4545
5701
  description: "Inspect final Git status, unstaged diff, staged diff, and this harness instance's mutation audit. This tool is read-only and does not commit, stage, reset, or push.",
4546
- schema: z3.object({}),
5702
+ schema: z4.object({}),
4547
5703
  metadata: readOnlyMetadata,
4548
5704
  execute: async () => serializeJsonValue2(editContractDocument("workspace-diff", {
4549
5705
  ...await workspace.gitDiff(),
@@ -4561,9 +5717,9 @@ var createExecutionEnvironmentTools = (workspace) => ({
4561
5717
  run_environment_command: tool({
4562
5718
  name: "run_environment_command",
4563
5719
  description: "Run one allowlisted argv command inside the enforced OCI snapshot. This never invokes a host shell, inherits no host environment variables, and has no network by default.",
4564
- schema: z3.strictObject({
4565
- command: z3.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._+-]*$/),
4566
- args: z3.array(z3.string().max(8192)).max(256).default([])
5720
+ schema: z4.strictObject({
5721
+ command: z4.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._+-]*$/),
5722
+ args: z4.array(z4.string().max(8192)).max(256).default([])
4567
5723
  }),
4568
5724
  requiresApproval: true,
4569
5725
  approvalMode: "interrupt",
@@ -4574,21 +5730,21 @@ var createExecutionEnvironmentTools = (workspace) => ({
4574
5730
  environment_status: tool({
4575
5731
  name: "environment_status",
4576
5732
  description: "Inspect the immutable image binding and enforced policy for the active run without exposing host paths or environment variables.",
4577
- schema: z3.object({}),
5733
+ schema: z4.object({}),
4578
5734
  metadata: readOnlyMetadata,
4579
5735
  execute: async (_input, context) => serializeJsonValue2(await requireExecutionSession(context).status())
4580
5736
  }),
4581
5737
  inspect_environment_patch: tool({
4582
5738
  name: "inspect_environment_patch",
4583
5739
  description: "Inspect a content-bound summary of changes made in the ephemeral OCI snapshot. Content remains in harness-owned state until a separately approved import.",
4584
- schema: z3.object({}),
5740
+ schema: z4.object({}),
4585
5741
  metadata: readOnlyMetadata,
4586
5742
  execute: async (_input, context) => serializeJsonValue2(await requireExecutionSession(context).inspectPatch())
4587
5743
  }),
4588
5744
  apply_environment_patch: tool({
4589
5745
  name: "apply_environment_patch",
4590
5746
  description: "Import an unchanged reviewed OCI snapshot patch into the host workspace. Host digests are rechecked and deletions use recoverable quarantine.",
4591
- schema: z3.strictObject({ patchId: fileDigestSchema }),
5747
+ schema: z4.strictObject({ patchId: fileDigestSchema }),
4592
5748
  ...mutationApproval,
4593
5749
  execute: async ({ patchId }, context) => serializeJsonValue2(await requireExecutionSession(context).importPatch(workspace, patchId))
4594
5750
  })
@@ -4879,6 +6035,7 @@ var appendUserMessage = (messages, text) => [
4879
6035
  ];
4880
6036
  export {
4881
6037
  workspaceFilePathSchema,
6038
+ verifyChangeEnvelope,
4882
6039
  validateStateDirectory,
4883
6040
  validateEditProposal,
4884
6041
  streamResultDocument,
@@ -4911,6 +6068,7 @@ export {
4911
6068
  inspectHarnessModelCapabilities,
4912
6069
  harnessExecutionSession,
4913
6070
  fileDigestSchema,
6071
+ externalAttestationReferenceSchema,
4914
6072
  executionFingerprintInput,
4915
6073
  estimateMessageTokens,
4916
6074
  editProposalSchema,
@@ -4919,6 +6077,7 @@ export {
4919
6077
  editContractDocument,
4920
6078
  editChangesSchema,
4921
6079
  editChangeSchema,
6080
+ digestChangeEnvelopeArtifact,
4922
6081
  describeOciCommand,
4923
6082
  defaultHarnessNamespace,
4924
6083
  createProviderRegistry,
@@ -4931,9 +6090,22 @@ export {
4931
6090
  createHarness,
4932
6091
  createExecutionEnvironmentTools,
4933
6092
  createEditProposal,
6093
+ createChangeEnvelope,
6094
+ computeChangeEnvelopeEvidenceDigest,
6095
+ computeChangeEnvelopeDigest,
4934
6096
  compactHarnessMessages,
4935
6097
  cleanupHarnessRuns,
4936
6098
  cleanupHarnessExecutionArtifacts,
6099
+ changeEnvelopeSchema,
6100
+ changeEnvelopePreconditionsSchema,
6101
+ changeEnvelopePatchSchema,
6102
+ changeEnvelopeInputSchema,
6103
+ changeEnvelopeFingerprintsSchema,
6104
+ changeEnvelopeEvidenceSchema,
6105
+ changeEnvelopeCheckSchema,
6106
+ changeEnvelopeBaseSchema,
6107
+ changeEnvelopeApprovalSchema,
6108
+ canonicalizeChangeEnvelope,
4937
6109
  cancelHarnessRun,
4938
6110
  assertHarnessModelCapabilities,
4939
6111
  applyEditProposalInputSchema,
@@ -4947,6 +6119,10 @@ export {
4947
6119
  MAX_EDIT_PROPOSAL_BYTES,
4948
6120
  MAX_EDIT_FILE_BYTES,
4949
6121
  MAX_EDIT_CHANGES,
6122
+ MAX_CHANGE_ENVELOPE_CHECKS,
6123
+ MAX_CHANGE_ENVELOPE_ATTESTATIONS,
6124
+ MAX_CHANGE_ENVELOPE_APPROVAL_SCOPES,
6125
+ MAX_CHANGE_ENVELOPE_APPROVALS,
4950
6126
  HARNESS_VERSION,
4951
6127
  HARNESS_SUBAGENT_PROFILE_DESCRIPTORS,
4952
6128
  HARNESS_SUBAGENT_PROFILES,
@@ -4974,6 +6150,8 @@ export {
4974
6150
  CliOciRuntimeAdapter,
4975
6151
  CLI_JSON_SCHEMA_VERSION,
4976
6152
  CLI_EVENT_SCHEMA_VERSION,
6153
+ CHANGE_ENVELOPE_SCHEMA_VERSION,
6154
+ CHANGE_ENVELOPE_DIGEST_ALGORITHM,
4977
6155
  BUN_ENGINE_RANGE,
4978
6156
  BUILTIN_PROVIDER_REGISTRATIONS
4979
6157
  };