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