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