@zhivex-ai/harness 0.7.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/CHANGELOG.md +45 -0
- package/README.md +31 -7
- package/ROADMAP.md +54 -9
- package/SECURITY.md +7 -1
- package/SUPPORT.md +2 -2
- package/dist/change-envelope.d.ts +271 -0
- package/dist/cli-stream.d.ts +14 -14
- package/dist/cli.d.ts +7 -1
- package/dist/cli.js +1457 -164
- package/dist/config.d.ts +1 -1
- package/dist/execution-environment.d.ts +49 -3
- package/dist/harness.d.ts +16 -1
- package/dist/hostile-repository-demo.js +812 -160
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1367 -188
- package/dist/workspace.d.ts +53 -0
- package/dist/zhx.js +1457 -164
- package/docs/CHANGE_ENVELOPES.md +116 -0
- package/docs/CLI.md +14 -4
- package/docs/DURABLE_OPERATIONS.md +14 -2
- package/docs/EXECUTION_ENVIRONMENTS.md +10 -4
- package/docs/EXTENSIBILITY.md +7 -3
- package/docs/LIVE_CERTIFICATION.md +17 -1
- package/docs/RELEASE.md +4 -4
- package/docs/REPOSITORY_EDITING.md +4 -0
- package/examples/change-envelope-input.json +26 -0
- package/examples/change.patch +6 -0
- package/package.json +14 -8
package/dist/index.js
CHANGED
|
@@ -317,7 +317,7 @@ var BUILTIN_PROVIDER_REGISTRATIONS = Object.freeze([
|
|
|
317
317
|
descriptor: {
|
|
318
318
|
id: "openai",
|
|
319
319
|
name: "OpenAI",
|
|
320
|
-
defaultModel: "gpt-5.
|
|
320
|
+
defaultModel: "gpt-5.6-luna",
|
|
321
321
|
credentialNames: ["OPENAI_API_KEY"],
|
|
322
322
|
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
323
323
|
support: "certified"
|
|
@@ -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-
|
|
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
|
-
|
|
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
|
|
1862
|
-
if (
|
|
1863
|
-
|
|
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
|
|
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
|
|
1954
|
+
files.push({ path: relative });
|
|
1886
1955
|
}
|
|
1887
1956
|
};
|
|
1888
1957
|
await walk(this.root, "", []);
|
|
1889
|
-
|
|
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 !==
|
|
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
|
-
|
|
1910
|
-
const selected =
|
|
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 ? {
|
|
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
|
-
|
|
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 !==
|
|
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
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
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
|
|
1989
|
-
const lineNumber =
|
|
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[
|
|
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({
|
|
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
|
-
|
|
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)) ===
|
|
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
|
-
|
|
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
|
-
|
|
3184
|
+
OCI_SESSION_CONTROLLER_SCRIPT
|
|
2751
3185
|
];
|
|
2752
|
-
let
|
|
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
|
-
"
|
|
2762
|
-
"
|
|
2763
|
-
|
|
2764
|
-
|
|
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
|
-
|
|
2785
|
-
|
|
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
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
})
|
|
2800
|
-
|
|
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.
|
|
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=${
|
|
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
|
|
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(".", {
|
|
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
|
|
2943
|
-
if (
|
|
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 (
|
|
2951
|
-
throw new Error(`Snapshot
|
|
2952
|
-
|
|
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
|
|
2959
|
-
const
|
|
2960
|
-
|
|
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
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
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
|
|
2995
|
-
|
|
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?.
|
|
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-
|
|
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/
|
|
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
|
|
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 = "
|
|
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:${
|
|
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,11 +5504,12 @@ 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
|
|
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.
|
|
4394
5511
|
- apply_patch, move_file, quarantine_file, restore_file, and run_check require explicit approval from the operator.
|
|
5512
|
+
- Calling an approval-gated tool is how you request that approval: call the tool with its complete reviewed arguments, then let the runtime pause before execution. Never replace the tool call with a textual approval request.
|
|
4395
5513
|
- With enforced OCI execution enabled, workspace tools operate on an ephemeral snapshot. Use inspect_environment_patch and obtain a separate approval through apply_environment_patch before changing the host workspace.
|
|
4396
5514
|
- run_environment_command executes one allowlisted argv command without a host shell. Network, privileges, resources, environment variables, and output are bounded by the OCI policy.
|
|
4397
5515
|
- Never overwrite stale content. If an expected digest no longer matches, inspect the file again and create a new proposal.
|
|
@@ -4436,10 +5554,10 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
4436
5554
|
list_files: tool({
|
|
4437
5555
|
name: "list_files",
|
|
4438
5556
|
description: "List regular files with content digests using a stable cursor. Build artifacts, dependencies, Git internals, and harness state are ignored.",
|
|
4439
|
-
schema:
|
|
4440
|
-
path:
|
|
4441
|
-
limit:
|
|
4442
|
-
cursor:
|
|
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()
|
|
4443
5561
|
}),
|
|
4444
5562
|
metadata: readOnlyMetadata,
|
|
4445
5563
|
execute: async ({ path: path9, limit, cursor }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).listFiles(path9, {
|
|
@@ -4450,23 +5568,40 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
4450
5568
|
read_file: tool({
|
|
4451
5569
|
name: "read_file",
|
|
4452
5570
|
description: "Read a bounded, line-numbered slice and SHA-256 digest of one UTF-8 text file using a workspace-relative path.",
|
|
4453
|
-
schema:
|
|
4454
|
-
path:
|
|
4455
|
-
startLine:
|
|
4456
|
-
endLine:
|
|
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()
|
|
4457
5575
|
}),
|
|
4458
5576
|
metadata: readOnlyMetadata,
|
|
4459
5577
|
execute: async ({ path: path9, startLine, endLine }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).readFile(path9, startLine, endLine))
|
|
4460
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
|
+
}),
|
|
4461
5596
|
search_files: tool({
|
|
4462
5597
|
name: "search_files",
|
|
4463
5598
|
description: "Search for a literal string in text files using a stable cursor.",
|
|
4464
|
-
schema:
|
|
4465
|
-
query:
|
|
4466
|
-
path:
|
|
4467
|
-
caseSensitive:
|
|
4468
|
-
limit:
|
|
4469
|
-
cursor:
|
|
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()
|
|
4470
5605
|
}),
|
|
4471
5606
|
metadata: readOnlyMetadata,
|
|
4472
5607
|
execute: async ({ query, path: path9, caseSensitive, limit, cursor }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).searchFiles(query, path9, {
|
|
@@ -4475,6 +5610,28 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
4475
5610
|
...cursor ? { cursor } : {}
|
|
4476
5611
|
}))
|
|
4477
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
|
+
}),
|
|
4478
5635
|
propose_edits: tool({
|
|
4479
5636
|
name: "propose_edits",
|
|
4480
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.",
|
|
@@ -4487,7 +5644,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
4487
5644
|
}),
|
|
4488
5645
|
apply_patch: tool({
|
|
4489
5646
|
name: "apply_patch",
|
|
4490
|
-
description: "
|
|
5647
|
+
description: "Request approval to atomically apply one reviewed multi-file proposal; the runtime pauses before execution. Every existing file requires its exact expected digest; expectedDigest=null is create-only. Call this tool instead of asking for approval in text.",
|
|
4491
5648
|
schema: applyEditProposalInputSchema,
|
|
4492
5649
|
...mutationApproval,
|
|
4493
5650
|
execute: async (input, context) => {
|
|
@@ -4519,9 +5676,9 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
4519
5676
|
run_check: tool({
|
|
4520
5677
|
name: "run_check",
|
|
4521
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.`,
|
|
4522
|
-
schema:
|
|
4523
|
-
check:
|
|
4524
|
-
expectedScript:
|
|
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)
|
|
4525
5682
|
}),
|
|
4526
5683
|
requiresApproval: true,
|
|
4527
5684
|
approvalMode: "interrupt",
|
|
@@ -4535,14 +5692,14 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
4535
5692
|
mutation_audit: tool({
|
|
4536
5693
|
name: "mutation_audit",
|
|
4537
5694
|
description: "Inspect the immutable in-memory audit journal for file mutations made by this harness instance.",
|
|
4538
|
-
schema:
|
|
5695
|
+
schema: z4.object({}),
|
|
4539
5696
|
metadata: readOnlyMetadata,
|
|
4540
5697
|
execute: async (_input, context) => serializeJsonValue2(editContractDocument("mutation-audit", (harnessExecutionSession(context)?.workspace ?? workspace).mutationAudit()))
|
|
4541
5698
|
}),
|
|
4542
5699
|
git_diff: tool({
|
|
4543
5700
|
name: "git_diff",
|
|
4544
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.",
|
|
4545
|
-
schema:
|
|
5702
|
+
schema: z4.object({}),
|
|
4546
5703
|
metadata: readOnlyMetadata,
|
|
4547
5704
|
execute: async () => serializeJsonValue2(editContractDocument("workspace-diff", {
|
|
4548
5705
|
...await workspace.gitDiff(),
|
|
@@ -4560,9 +5717,9 @@ var createExecutionEnvironmentTools = (workspace) => ({
|
|
|
4560
5717
|
run_environment_command: tool({
|
|
4561
5718
|
name: "run_environment_command",
|
|
4562
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.",
|
|
4563
|
-
schema:
|
|
4564
|
-
command:
|
|
4565
|
-
args:
|
|
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([])
|
|
4566
5723
|
}),
|
|
4567
5724
|
requiresApproval: true,
|
|
4568
5725
|
approvalMode: "interrupt",
|
|
@@ -4573,21 +5730,21 @@ var createExecutionEnvironmentTools = (workspace) => ({
|
|
|
4573
5730
|
environment_status: tool({
|
|
4574
5731
|
name: "environment_status",
|
|
4575
5732
|
description: "Inspect the immutable image binding and enforced policy for the active run without exposing host paths or environment variables.",
|
|
4576
|
-
schema:
|
|
5733
|
+
schema: z4.object({}),
|
|
4577
5734
|
metadata: readOnlyMetadata,
|
|
4578
5735
|
execute: async (_input, context) => serializeJsonValue2(await requireExecutionSession(context).status())
|
|
4579
5736
|
}),
|
|
4580
5737
|
inspect_environment_patch: tool({
|
|
4581
5738
|
name: "inspect_environment_patch",
|
|
4582
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.",
|
|
4583
|
-
schema:
|
|
5740
|
+
schema: z4.object({}),
|
|
4584
5741
|
metadata: readOnlyMetadata,
|
|
4585
5742
|
execute: async (_input, context) => serializeJsonValue2(await requireExecutionSession(context).inspectPatch())
|
|
4586
5743
|
}),
|
|
4587
5744
|
apply_environment_patch: tool({
|
|
4588
5745
|
name: "apply_environment_patch",
|
|
4589
5746
|
description: "Import an unchanged reviewed OCI snapshot patch into the host workspace. Host digests are rechecked and deletions use recoverable quarantine.",
|
|
4590
|
-
schema:
|
|
5747
|
+
schema: z4.strictObject({ patchId: fileDigestSchema }),
|
|
4591
5748
|
...mutationApproval,
|
|
4592
5749
|
execute: async ({ patchId }, context) => serializeJsonValue2(await requireExecutionSession(context).importPatch(workspace, patchId))
|
|
4593
5750
|
})
|
|
@@ -4878,6 +6035,7 @@ var appendUserMessage = (messages, text) => [
|
|
|
4878
6035
|
];
|
|
4879
6036
|
export {
|
|
4880
6037
|
workspaceFilePathSchema,
|
|
6038
|
+
verifyChangeEnvelope,
|
|
4881
6039
|
validateStateDirectory,
|
|
4882
6040
|
validateEditProposal,
|
|
4883
6041
|
streamResultDocument,
|
|
@@ -4910,6 +6068,7 @@ export {
|
|
|
4910
6068
|
inspectHarnessModelCapabilities,
|
|
4911
6069
|
harnessExecutionSession,
|
|
4912
6070
|
fileDigestSchema,
|
|
6071
|
+
externalAttestationReferenceSchema,
|
|
4913
6072
|
executionFingerprintInput,
|
|
4914
6073
|
estimateMessageTokens,
|
|
4915
6074
|
editProposalSchema,
|
|
@@ -4918,6 +6077,7 @@ export {
|
|
|
4918
6077
|
editContractDocument,
|
|
4919
6078
|
editChangesSchema,
|
|
4920
6079
|
editChangeSchema,
|
|
6080
|
+
digestChangeEnvelopeArtifact,
|
|
4921
6081
|
describeOciCommand,
|
|
4922
6082
|
defaultHarnessNamespace,
|
|
4923
6083
|
createProviderRegistry,
|
|
@@ -4930,9 +6090,22 @@ export {
|
|
|
4930
6090
|
createHarness,
|
|
4931
6091
|
createExecutionEnvironmentTools,
|
|
4932
6092
|
createEditProposal,
|
|
6093
|
+
createChangeEnvelope,
|
|
6094
|
+
computeChangeEnvelopeEvidenceDigest,
|
|
6095
|
+
computeChangeEnvelopeDigest,
|
|
4933
6096
|
compactHarnessMessages,
|
|
4934
6097
|
cleanupHarnessRuns,
|
|
4935
6098
|
cleanupHarnessExecutionArtifacts,
|
|
6099
|
+
changeEnvelopeSchema,
|
|
6100
|
+
changeEnvelopePreconditionsSchema,
|
|
6101
|
+
changeEnvelopePatchSchema,
|
|
6102
|
+
changeEnvelopeInputSchema,
|
|
6103
|
+
changeEnvelopeFingerprintsSchema,
|
|
6104
|
+
changeEnvelopeEvidenceSchema,
|
|
6105
|
+
changeEnvelopeCheckSchema,
|
|
6106
|
+
changeEnvelopeBaseSchema,
|
|
6107
|
+
changeEnvelopeApprovalSchema,
|
|
6108
|
+
canonicalizeChangeEnvelope,
|
|
4936
6109
|
cancelHarnessRun,
|
|
4937
6110
|
assertHarnessModelCapabilities,
|
|
4938
6111
|
applyEditProposalInputSchema,
|
|
@@ -4946,6 +6119,10 @@ export {
|
|
|
4946
6119
|
MAX_EDIT_PROPOSAL_BYTES,
|
|
4947
6120
|
MAX_EDIT_FILE_BYTES,
|
|
4948
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,
|
|
4949
6126
|
HARNESS_VERSION,
|
|
4950
6127
|
HARNESS_SUBAGENT_PROFILE_DESCRIPTORS,
|
|
4951
6128
|
HARNESS_SUBAGENT_PROFILES,
|
|
@@ -4973,6 +6150,8 @@ export {
|
|
|
4973
6150
|
CliOciRuntimeAdapter,
|
|
4974
6151
|
CLI_JSON_SCHEMA_VERSION,
|
|
4975
6152
|
CLI_EVENT_SCHEMA_VERSION,
|
|
6153
|
+
CHANGE_ENVELOPE_SCHEMA_VERSION,
|
|
6154
|
+
CHANGE_ENVELOPE_DIGEST_ALGORITHM,
|
|
4976
6155
|
BUN_ENGINE_RANGE,
|
|
4977
6156
|
BUILTIN_PROVIDER_REGISTRATIONS
|
|
4978
6157
|
};
|