@zhivex-ai/harness 0.9.0 → 0.10.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 +29 -0
- package/README.md +42 -16
- package/ROADMAP.md +35 -12
- package/SUPPORT.md +3 -2
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +1291 -417
- package/dist/config.d.ts +3 -3
- package/dist/execution-environment.d.ts +33 -5
- package/dist/harness.d.ts +57 -19
- package/dist/hostile-repository-demo.js +1217 -341
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1797 -443
- package/dist/package-manager.d.ts +17 -0
- package/dist/process-runtime.d.ts +16 -0
- package/dist/sqlite-database.d.ts +23 -0
- package/dist/time-to-safe-fix.d.ts +232 -0
- package/dist/version.d.ts +3 -1
- package/dist/workspace.d.ts +20 -5
- package/dist/zhx.js +1291 -417
- package/docs/CLI.md +4 -4
- package/docs/DURABLE_OPERATIONS.md +11 -3
- package/docs/EXECUTION_ENVIRONMENTS.md +22 -10
- package/docs/EXTENSIBILITY.md +3 -3
- package/docs/HOSTILE_REPOSITORY_DEMO.md +5 -4
- package/docs/RELEASE.md +6 -6
- package/docs/REPOSITORY_EDITING.md +2 -2
- package/docs/TIME_TO_SAFE_FIX.md +172 -0
- package/evaluations/time-to-safe-fix-expanded.jsonl +12 -0
- package/evaluations/time-to-safe-fix-smoke.jsonl +2 -0
- package/package.json +18 -7
package/dist/zhx.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
// @bun
|
|
1
|
+
#!/usr/bin/env node
|
|
3
2
|
|
|
4
3
|
// src/cli.ts
|
|
5
|
-
import { createInterface } from "readline/promises";
|
|
6
|
-
import { randomUUID as randomUUID5 } from "crypto";
|
|
7
|
-
import { constants as fsConstants2 } from "fs";
|
|
8
|
-
import { access, lstat as
|
|
9
|
-
import
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
6
|
+
import { constants as fsConstants2, realpathSync as realpathSync2 } from "node:fs";
|
|
7
|
+
import { access, lstat as lstat8, readFile as readFile4, stat as stat4 } from "node:fs/promises";
|
|
8
|
+
import path10 from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { z as z5 } from "zod";
|
|
11
11
|
import {
|
|
12
12
|
getAgentBudgetStatus as getAgentBudgetStatus3
|
|
13
13
|
} from "@zhivex-ai/agents";
|
|
14
14
|
|
|
15
15
|
// src/config.ts
|
|
16
|
-
import { createHash as createHash2 } from "crypto";
|
|
17
|
-
import { realpathSync } from "fs";
|
|
18
|
-
import path from "path";
|
|
16
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
17
|
+
import { realpathSync } from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
19
|
|
|
20
20
|
// src/providers.ts
|
|
21
|
-
import { createHash } from "crypto";
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
22
|
import { createGemini } from "@zhivex-ai/gemini";
|
|
23
23
|
import { createMeta } from "@zhivex-ai/meta";
|
|
24
24
|
import { createOpenAI } from "@zhivex-ai/openai";
|
|
@@ -372,11 +372,11 @@ var HARNESS_CONFIG_SCHEMA_VERSION = 4;
|
|
|
372
372
|
var HARNESS_STORE_BACKENDS = ["sqlite", "file"];
|
|
373
373
|
var HARNESS_EXECUTION_BACKENDS = ["none", "oci"];
|
|
374
374
|
var HARNESS_OCI_RUNTIMES = ["docker", "podman"];
|
|
375
|
-
var HARNESS_EXECUTION_POLICY_VERSION = "2026-08-
|
|
375
|
+
var HARNESS_EXECUTION_POLICY_VERSION = "2026-08-21-v3";
|
|
376
376
|
var DEFAULT_OCI_EXECUTION = {
|
|
377
377
|
runtime: "docker",
|
|
378
|
-
image: "
|
|
379
|
-
allowedCommands: ["
|
|
378
|
+
image: "node:24-bookworm-slim",
|
|
379
|
+
allowedCommands: ["node", "npm"],
|
|
380
380
|
maxProcessRuntimeMs: 120000,
|
|
381
381
|
maxProcessOutputBytes: 20000,
|
|
382
382
|
maxMemoryMb: 1024,
|
|
@@ -501,8 +501,8 @@ var resolveOciAllowedCommands = (configured) => {
|
|
|
501
501
|
if (invalid) {
|
|
502
502
|
throw new Error(`Invalid OCI command name: ${invalid}. Use a bare executable name without a path.`);
|
|
503
503
|
}
|
|
504
|
-
if (!commands.
|
|
505
|
-
throw new Error("ociAllowedCommands must include bun so declared package checks remain available.");
|
|
504
|
+
if (!commands.some((command) => ["npm", "pnpm", "yarn", "bun"].includes(command))) {
|
|
505
|
+
throw new Error("ociAllowedCommands must include npm, pnpm, yarn, or bun so declared package checks remain available.");
|
|
506
506
|
}
|
|
507
507
|
return commands;
|
|
508
508
|
};
|
|
@@ -657,7 +657,7 @@ var createProviderModel = (config, env = process.env, registry = DEFAULT_PROVIDE
|
|
|
657
657
|
var providerAvailability = (env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => registry.availability(env);
|
|
658
658
|
|
|
659
659
|
// src/harness.ts
|
|
660
|
-
import { createHash as createHash7 } from "crypto";
|
|
660
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
661
661
|
import {
|
|
662
662
|
Agent,
|
|
663
663
|
applySafetyPolicyToAgent as applySafetyPolicyToAgent2,
|
|
@@ -727,8 +727,8 @@ var selectHarnessModel = (candidates, required) => {
|
|
|
727
727
|
};
|
|
728
728
|
|
|
729
729
|
// src/edit-contracts.ts
|
|
730
|
-
import { createHash as createHash3 } from "crypto";
|
|
731
|
-
import path2 from "path";
|
|
730
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
731
|
+
import path2 from "node:path";
|
|
732
732
|
import { z } from "zod";
|
|
733
733
|
var EDIT_CONTRACT_SCHEMA_VERSION = 1;
|
|
734
734
|
var EDIT_DIGEST_ALGORITHM = "sha256";
|
|
@@ -862,9 +862,256 @@ var editContractDocument = (kind, result) => ({
|
|
|
862
862
|
});
|
|
863
863
|
|
|
864
864
|
// src/workspace.ts
|
|
865
|
-
import { createHash as createHash4, randomUUID } from "crypto";
|
|
866
|
-
import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "fs/promises";
|
|
867
|
-
import
|
|
865
|
+
import { createHash as createHash4, randomUUID } from "node:crypto";
|
|
866
|
+
import { link, lstat as lstat2, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "node:fs/promises";
|
|
867
|
+
import path4 from "node:path";
|
|
868
|
+
|
|
869
|
+
// src/package-manager.ts
|
|
870
|
+
import { lstat } from "node:fs/promises";
|
|
871
|
+
import path3 from "node:path";
|
|
872
|
+
var HARNESS_PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
873
|
+
var LOCKFILES = {
|
|
874
|
+
npm: ["package-lock.json", "npm-shrinkwrap.json"],
|
|
875
|
+
pnpm: ["pnpm-lock.yaml"],
|
|
876
|
+
yarn: ["yarn.lock"],
|
|
877
|
+
bun: ["bun.lock", "bun.lockb"]
|
|
878
|
+
};
|
|
879
|
+
var isPackageManager = (value) => HARNESS_PACKAGE_MANAGERS.includes(value);
|
|
880
|
+
var parsePackageManager = (value) => {
|
|
881
|
+
if (value === undefined)
|
|
882
|
+
return;
|
|
883
|
+
if (typeof value !== "string") {
|
|
884
|
+
throw new Error("package.json packageManager must be a string when present.");
|
|
885
|
+
}
|
|
886
|
+
const match = /^(npm|pnpm|yarn|bun)@([^\s]+)$/.exec(value.trim());
|
|
887
|
+
if (!match?.[1] || !match[2]) {
|
|
888
|
+
throw new Error("package.json packageManager must pin npm, pnpm, yarn, or bun using <manager>@<version>.");
|
|
889
|
+
}
|
|
890
|
+
if (!isPackageManager(match[1])) {
|
|
891
|
+
throw new Error(`Unsupported package manager: ${match[1]}.`);
|
|
892
|
+
}
|
|
893
|
+
return match[1];
|
|
894
|
+
};
|
|
895
|
+
var existingLockfiles = async (root) => {
|
|
896
|
+
const matches = [];
|
|
897
|
+
for (const manager of HARNESS_PACKAGE_MANAGERS) {
|
|
898
|
+
for (const file of LOCKFILES[manager]) {
|
|
899
|
+
try {
|
|
900
|
+
const entry = await lstat(path3.join(root, file));
|
|
901
|
+
if (entry.isSymbolicLink()) {
|
|
902
|
+
throw new Error(`Package-manager lockfile must not be a symbolic link: ${file}.`);
|
|
903
|
+
}
|
|
904
|
+
if (entry.isFile())
|
|
905
|
+
matches.push({ manager, file });
|
|
906
|
+
} catch (error) {
|
|
907
|
+
if (error.code !== "ENOENT")
|
|
908
|
+
throw error;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return matches;
|
|
913
|
+
};
|
|
914
|
+
var resolvePackageManager = async (root, manifest) => {
|
|
915
|
+
const declared = parsePackageManager(manifest.packageManager);
|
|
916
|
+
if (declared) {
|
|
917
|
+
return {
|
|
918
|
+
manager: declared,
|
|
919
|
+
source: "packageManager",
|
|
920
|
+
evidence: manifest.packageManager
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
const lockfiles = await existingLockfiles(root);
|
|
924
|
+
const managers = [...new Set(lockfiles.map((entry) => entry.manager))];
|
|
925
|
+
if (managers.length > 1) {
|
|
926
|
+
throw new Error(`Ambiguous package manager: found lockfiles for ${managers.join(", ")}; declare packageManager explicitly.`);
|
|
927
|
+
}
|
|
928
|
+
const detected = managers[0];
|
|
929
|
+
if (detected) {
|
|
930
|
+
return {
|
|
931
|
+
manager: detected,
|
|
932
|
+
source: "lockfile",
|
|
933
|
+
evidence: lockfiles.filter((entry) => entry.manager === detected).map((entry) => entry.file).join(",")
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
return { manager: "npm", source: "default" };
|
|
937
|
+
};
|
|
938
|
+
var runCommand = (manager, check) => {
|
|
939
|
+
switch (manager) {
|
|
940
|
+
case "bun":
|
|
941
|
+
return ["bun", "--no-env-file", "run", check];
|
|
942
|
+
case "npm":
|
|
943
|
+
return ["npm", "--ignore-scripts", "run", check];
|
|
944
|
+
case "pnpm":
|
|
945
|
+
return ["pnpm", "--ignore-scripts", "run", check];
|
|
946
|
+
case "yarn":
|
|
947
|
+
return ["yarn", "run", check];
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
var resolvePackageCheckCommand = async (root, manifest, check, expectedScript, allowedChecks) => {
|
|
951
|
+
if (!/^[A-Za-z0-9:_-]+$/.test(check) || !allowedChecks.includes(check)) {
|
|
952
|
+
throw new Error(`The check "${check}" is not in the explicit allowlist.`);
|
|
953
|
+
}
|
|
954
|
+
const scripts = manifest.scripts && typeof manifest.scripts === "object" ? manifest.scripts : {};
|
|
955
|
+
const actual = scripts[check];
|
|
956
|
+
if (typeof actual !== "string") {
|
|
957
|
+
throw new Error(`package.json does not define the "${check}" script.`);
|
|
958
|
+
}
|
|
959
|
+
if (actual !== expectedScript) {
|
|
960
|
+
throw new Error(`The "${check}" script changed or does not match expectedScript.`);
|
|
961
|
+
}
|
|
962
|
+
for (const lifecycleName of [`pre${check}`, `post${check}`]) {
|
|
963
|
+
if (typeof scripts[lifecycleName] === "string") {
|
|
964
|
+
throw new Error(`The approved "${check}" check has an implicit ${lifecycleName} lifecycle script; approve a standalone script instead.`);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
const resolution = await resolvePackageManager(root, manifest);
|
|
968
|
+
return { ...resolution, command: runCommand(resolution.manager, check) };
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
// src/process-runtime.ts
|
|
972
|
+
import { spawn } from "node:child_process";
|
|
973
|
+
var DEFAULT_MAX_OUTPUT_CHARACTERS = 20000;
|
|
974
|
+
var DEFAULT_TERMINATION_GRACE_MS = 1000;
|
|
975
|
+
var PORTABLE_ENVIRONMENT_KEYS = [
|
|
976
|
+
"CI",
|
|
977
|
+
"FORCE_COLOR",
|
|
978
|
+
"LANG",
|
|
979
|
+
"LC_ALL",
|
|
980
|
+
"NO_COLOR",
|
|
981
|
+
"PATH",
|
|
982
|
+
"TEMP",
|
|
983
|
+
"TERM",
|
|
984
|
+
"TMP",
|
|
985
|
+
"TMPDIR"
|
|
986
|
+
];
|
|
987
|
+
|
|
988
|
+
class BoundedTextCapture {
|
|
989
|
+
decoder = new TextDecoder;
|
|
990
|
+
maxCharacters;
|
|
991
|
+
output = "";
|
|
992
|
+
totalCharacters = 0;
|
|
993
|
+
constructor(maxCharacters) {
|
|
994
|
+
this.maxCharacters = maxCharacters;
|
|
995
|
+
}
|
|
996
|
+
write(chunk) {
|
|
997
|
+
this.append(this.decoder.decode(chunk, { stream: true }));
|
|
998
|
+
}
|
|
999
|
+
finish() {
|
|
1000
|
+
this.append(this.decoder.decode());
|
|
1001
|
+
return this.totalCharacters <= this.maxCharacters ? this.output : `${this.output}
|
|
1002
|
+
… output truncated (${this.totalCharacters - this.maxCharacters} characters omitted)`;
|
|
1003
|
+
}
|
|
1004
|
+
append(value) {
|
|
1005
|
+
this.totalCharacters += value.length;
|
|
1006
|
+
if (this.output.length < this.maxCharacters) {
|
|
1007
|
+
this.output += value.slice(0, this.maxCharacters - this.output.length);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
var processEnvironment = (additional) => {
|
|
1012
|
+
const env = {};
|
|
1013
|
+
for (const key of PORTABLE_ENVIRONMENT_KEYS) {
|
|
1014
|
+
const value = process.env[key];
|
|
1015
|
+
if (value !== undefined)
|
|
1016
|
+
env[key] = value;
|
|
1017
|
+
}
|
|
1018
|
+
for (const [key, value] of Object.entries(additional ?? {})) {
|
|
1019
|
+
if (value === undefined)
|
|
1020
|
+
delete env[key];
|
|
1021
|
+
else
|
|
1022
|
+
env[key] = value;
|
|
1023
|
+
}
|
|
1024
|
+
return env;
|
|
1025
|
+
};
|
|
1026
|
+
var exitCodeForSignal = (signal) => {
|
|
1027
|
+
if (signal === "SIGKILL")
|
|
1028
|
+
return 137;
|
|
1029
|
+
if (signal === "SIGTERM")
|
|
1030
|
+
return 143;
|
|
1031
|
+
return 1;
|
|
1032
|
+
};
|
|
1033
|
+
var abortReason = (signal) => signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted.", "AbortError");
|
|
1034
|
+
var runPortableProcess = async (command, options = {}) => {
|
|
1035
|
+
if (command.length === 0 || !command[0])
|
|
1036
|
+
throw new Error("The process command must not be empty.");
|
|
1037
|
+
if (options.signal?.aborted)
|
|
1038
|
+
throw abortReason(options.signal);
|
|
1039
|
+
const maxOutputCharacters = options.maxOutputCharacters ?? DEFAULT_MAX_OUTPUT_CHARACTERS;
|
|
1040
|
+
if (!Number.isSafeInteger(maxOutputCharacters) || maxOutputCharacters < 0) {
|
|
1041
|
+
throw new Error("maxOutputCharacters must be a non-negative safe integer.");
|
|
1042
|
+
}
|
|
1043
|
+
if (options.timeoutMs !== undefined && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0)) {
|
|
1044
|
+
throw new Error("timeoutMs must be a positive safe integer.");
|
|
1045
|
+
}
|
|
1046
|
+
const argv = [...command];
|
|
1047
|
+
const stdout = new BoundedTextCapture(maxOutputCharacters);
|
|
1048
|
+
const stderr = new BoundedTextCapture(maxOutputCharacters);
|
|
1049
|
+
const child = spawn(argv[0], argv.slice(1), {
|
|
1050
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
1051
|
+
env: processEnvironment(options.env),
|
|
1052
|
+
shell: false,
|
|
1053
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1054
|
+
windowsHide: true
|
|
1055
|
+
});
|
|
1056
|
+
let timedOut = false;
|
|
1057
|
+
let aborted = false;
|
|
1058
|
+
let terminationTimer;
|
|
1059
|
+
child.stdout.on("data", (chunk) => stdout.write(chunk));
|
|
1060
|
+
child.stderr?.on("data", (chunk) => stderr.write(chunk));
|
|
1061
|
+
const terminate = () => {
|
|
1062
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
1063
|
+
return;
|
|
1064
|
+
child.kill("SIGTERM");
|
|
1065
|
+
terminationTimer = setTimeout(() => {
|
|
1066
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
1067
|
+
child.kill("SIGKILL");
|
|
1068
|
+
}, DEFAULT_TERMINATION_GRACE_MS);
|
|
1069
|
+
terminationTimer.unref?.();
|
|
1070
|
+
};
|
|
1071
|
+
const handleAbort = () => {
|
|
1072
|
+
aborted = true;
|
|
1073
|
+
terminate();
|
|
1074
|
+
};
|
|
1075
|
+
options.signal?.addEventListener("abort", handleAbort, { once: true });
|
|
1076
|
+
const timeout = options.timeoutMs === undefined ? undefined : setTimeout(() => {
|
|
1077
|
+
timedOut = true;
|
|
1078
|
+
terminate();
|
|
1079
|
+
}, options.timeoutMs);
|
|
1080
|
+
timeout?.unref?.();
|
|
1081
|
+
try {
|
|
1082
|
+
return await new Promise((resolve, reject) => {
|
|
1083
|
+
let processError;
|
|
1084
|
+
child.once("error", (error) => {
|
|
1085
|
+
processError = error;
|
|
1086
|
+
});
|
|
1087
|
+
child.once("close", (exitCode, signal) => {
|
|
1088
|
+
if (processError) {
|
|
1089
|
+
reject(processError);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (aborted && options.signal) {
|
|
1093
|
+
reject(abortReason(options.signal));
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
resolve({
|
|
1097
|
+
command: argv,
|
|
1098
|
+
exitCode: exitCode ?? exitCodeForSignal(signal),
|
|
1099
|
+
stdout: stdout.finish(),
|
|
1100
|
+
stderr: options.stderr === "ignore" ? "" : stderr.finish(),
|
|
1101
|
+
timedOut
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
1104
|
+
});
|
|
1105
|
+
} finally {
|
|
1106
|
+
if (timeout)
|
|
1107
|
+
clearTimeout(timeout);
|
|
1108
|
+
if (terminationTimer)
|
|
1109
|
+
clearTimeout(terminationTimer);
|
|
1110
|
+
options.signal?.removeEventListener("abort", handleAbort);
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
// src/workspace.ts
|
|
868
1115
|
var HARD_IGNORES = new Set([".git", ".next", ".turbo", ".zhivex-harness", "coverage", "dist", "node_modules"]);
|
|
869
1116
|
var IGNORE_FILES = [".gitignore", ".zhivex-harnessignore"];
|
|
870
1117
|
var DEFAULT_CHECKS = ["test", "typecheck", "lint", "build"];
|
|
@@ -880,13 +1127,13 @@ var isSensitiveName = (name) => {
|
|
|
880
1127
|
};
|
|
881
1128
|
var isHardIgnored = (relativePath) => relativePath.split("/").some((segment) => HARD_IGNORES.has(segment.toLocaleLowerCase()) || isSensitiveName(segment));
|
|
882
1129
|
var isInside = (root, candidate) => {
|
|
883
|
-
const relative =
|
|
884
|
-
return relative === "" || !relative.startsWith(`..${
|
|
1130
|
+
const relative = path4.relative(root, candidate);
|
|
1131
|
+
return relative === "" || !relative.startsWith(`..${path4.sep}`) && relative !== ".." && !path4.isAbsolute(relative);
|
|
885
1132
|
};
|
|
886
|
-
var wirePath = (value) => value.split(
|
|
1133
|
+
var wirePath = (value) => value.split(path4.sep).join("/");
|
|
887
1134
|
var digestBytes = (value) => `sha256:${createHash4("sha256").update(value).digest("hex")}`;
|
|
888
1135
|
var truncate = (value, maxLength = MAX_TOOL_OUTPUT) => value.length <= maxLength ? value : `${value.slice(0, maxLength)}
|
|
889
|
-
|
|
1136
|
+
… output truncated (${value.length - maxLength} characters omitted)`;
|
|
890
1137
|
var cursorEncode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
891
1138
|
var cursorDecode = (value) => {
|
|
892
1139
|
try {
|
|
@@ -987,24 +1234,7 @@ var isIgnoredByRules = (relativePath, isDirectory, rules) => {
|
|
|
987
1234
|
return ignored;
|
|
988
1235
|
};
|
|
989
1236
|
var spawnBounded = async (command, cwd, timeoutMs = 120000) => {
|
|
990
|
-
|
|
991
|
-
const env = Object.fromEntries(allowed.flatMap((key) => process.env[key] ? [[key, process.env[key]]] : []));
|
|
992
|
-
const child = Bun.spawn(command, { cwd, env, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
993
|
-
let timedOut = false;
|
|
994
|
-
const timeout = setTimeout(() => {
|
|
995
|
-
timedOut = true;
|
|
996
|
-
child.kill();
|
|
997
|
-
}, timeoutMs);
|
|
998
|
-
try {
|
|
999
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
1000
|
-
new Response(child.stdout).text(),
|
|
1001
|
-
new Response(child.stderr).text(),
|
|
1002
|
-
child.exited
|
|
1003
|
-
]);
|
|
1004
|
-
return { command, exitCode, stdout: truncate(stdout), stderr: truncate(stderr), timedOut };
|
|
1005
|
-
} finally {
|
|
1006
|
-
clearTimeout(timeout);
|
|
1007
|
-
}
|
|
1237
|
+
return await runPortableProcess(command, { cwd, timeoutMs });
|
|
1008
1238
|
};
|
|
1009
1239
|
|
|
1010
1240
|
class Workspace {
|
|
@@ -1019,7 +1249,7 @@ class Workspace {
|
|
|
1019
1249
|
this.root = root;
|
|
1020
1250
|
}
|
|
1021
1251
|
static async open(root) {
|
|
1022
|
-
const resolved = await realpath(
|
|
1252
|
+
const resolved = await realpath(path4.resolve(root));
|
|
1023
1253
|
if (!(await stat(resolved)).isDirectory())
|
|
1024
1254
|
throw new Error(`The workspace is not a directory: ${root}`);
|
|
1025
1255
|
return new Workspace(resolved);
|
|
@@ -1027,10 +1257,10 @@ class Workspace {
|
|
|
1027
1257
|
lexicalPath(relativePath) {
|
|
1028
1258
|
if (!relativePath || relativePath.includes("\x00"))
|
|
1029
1259
|
throw new Error("The path must be a valid relative path.");
|
|
1030
|
-
const candidate =
|
|
1260
|
+
const candidate = path4.resolve(this.root, relativePath);
|
|
1031
1261
|
if (!isInside(this.root, candidate))
|
|
1032
1262
|
throw new Error(`The path escapes the workspace: ${relativePath}`);
|
|
1033
|
-
const relative = wirePath(
|
|
1263
|
+
const relative = wirePath(path4.relative(this.root, candidate));
|
|
1034
1264
|
if (relative && isHardIgnored(relative)) {
|
|
1035
1265
|
const blocked = relative.split("/").find((segment) => HARD_IGNORES.has(segment.toLocaleLowerCase()) || isSensitiveName(segment));
|
|
1036
1266
|
throw new Error(`The path is protected by the harness policy: ${blocked}`);
|
|
@@ -1041,13 +1271,13 @@ class Workspace {
|
|
|
1041
1271
|
const candidate = this.lexicalPath(relativePath);
|
|
1042
1272
|
if (candidate === this.root && options.requireFile)
|
|
1043
1273
|
throw new Error("The workspace root is not a regular file.");
|
|
1044
|
-
const segments =
|
|
1274
|
+
const segments = path4.relative(this.root, candidate).split(path4.sep).filter(Boolean);
|
|
1045
1275
|
let current = this.root;
|
|
1046
1276
|
for (let index = 0;index < segments.length; index += 1) {
|
|
1047
|
-
current =
|
|
1277
|
+
current = path4.join(current, segments[index]);
|
|
1048
1278
|
let entry;
|
|
1049
1279
|
try {
|
|
1050
|
-
entry = await
|
|
1280
|
+
entry = await lstat2(current);
|
|
1051
1281
|
} catch (error) {
|
|
1052
1282
|
if (options.allowMissing && error.code === "ENOENT") {
|
|
1053
1283
|
return { path: candidate, exists: false };
|
|
@@ -1068,18 +1298,18 @@ class Workspace {
|
|
|
1068
1298
|
async readStableFile(relativePath, allowBinary = true) {
|
|
1069
1299
|
this.indexMetrics.stableFileReads += 1;
|
|
1070
1300
|
const safe = await this.safePath(relativePath, { requireFile: true });
|
|
1071
|
-
const before = await
|
|
1301
|
+
const before = await lstat2(safe.path);
|
|
1072
1302
|
if (before.size > MAX_FILE_BYTES)
|
|
1073
1303
|
throw new Error(`The file exceeds the ${MAX_FILE_BYTES}-byte limit.`);
|
|
1074
1304
|
const contents = await readFile(safe.path);
|
|
1075
|
-
const after = await
|
|
1305
|
+
const after = await lstat2(safe.path);
|
|
1076
1306
|
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
|
|
1077
1307
|
throw new Error(`The file changed while it was being read: ${relativePath}`);
|
|
1078
1308
|
}
|
|
1079
1309
|
if (!allowBinary && contents.includes(0))
|
|
1080
1310
|
throw new Error("The file appears to be binary and cannot be read as text.");
|
|
1081
1311
|
return {
|
|
1082
|
-
path: wirePath(
|
|
1312
|
+
path: wirePath(path4.relative(this.root, safe.path)),
|
|
1083
1313
|
absolutePath: safe.path,
|
|
1084
1314
|
contents,
|
|
1085
1315
|
digest: digestBytes(contents),
|
|
@@ -1105,21 +1335,21 @@ class Workspace {
|
|
|
1105
1335
|
const rules = [...inherited];
|
|
1106
1336
|
for (const filename of IGNORE_FILES) {
|
|
1107
1337
|
try {
|
|
1108
|
-
const ignorePath =
|
|
1109
|
-
const before = await
|
|
1338
|
+
const ignorePath = path4.join(directory, filename);
|
|
1339
|
+
const before = await lstat2(ignorePath);
|
|
1110
1340
|
if (before.isSymbolicLink() || !before.isFile())
|
|
1111
1341
|
continue;
|
|
1112
1342
|
if (before.size <= MAX_FILE_BYTES) {
|
|
1113
1343
|
const contents = await readFile(ignorePath);
|
|
1114
|
-
const after = await
|
|
1344
|
+
const after = await lstat2(ignorePath);
|
|
1115
1345
|
if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, this.fingerprint("", "ignore-file", before))) {
|
|
1116
|
-
throw new Error(`The ignore file changed while the workspace was being indexed: ${wirePath(
|
|
1346
|
+
throw new Error(`The ignore file changed while the workspace was being indexed: ${wirePath(path4.relative(this.root, ignorePath))}`);
|
|
1117
1347
|
}
|
|
1118
1348
|
const digest2 = digestBytes(contents);
|
|
1119
|
-
ignoreFiles.push(this.fingerprint(wirePath(
|
|
1349
|
+
ignoreFiles.push(this.fingerprint(wirePath(path4.relative(this.root, ignorePath)), "ignore-file", after, digest2));
|
|
1120
1350
|
rules.push(...parseIgnoreRules(contents.toString("utf8"), base));
|
|
1121
1351
|
} else {
|
|
1122
|
-
ignoreFiles.push(this.fingerprint(wirePath(
|
|
1352
|
+
ignoreFiles.push(this.fingerprint(wirePath(path4.relative(this.root, ignorePath)), "ignore-file", before));
|
|
1123
1353
|
}
|
|
1124
1354
|
} catch (error) {
|
|
1125
1355
|
if (error.code !== "ENOENT")
|
|
@@ -1133,7 +1363,7 @@ class Workspace {
|
|
|
1133
1363
|
const directories = [];
|
|
1134
1364
|
const ignoreFiles = [];
|
|
1135
1365
|
const walk = async (directory, base, inherited) => {
|
|
1136
|
-
const directoryEntry = await
|
|
1366
|
+
const directoryEntry = await lstat2(directory);
|
|
1137
1367
|
if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
|
|
1138
1368
|
throw new Error(`A workspace directory changed while it was being indexed: ${base || "."}`);
|
|
1139
1369
|
}
|
|
@@ -1144,7 +1374,7 @@ class Workspace {
|
|
|
1144
1374
|
const relative = base ? `${base}/${entry.name}` : entry.name;
|
|
1145
1375
|
if (isHardIgnored(relative) || entry.isSymbolicLink() || isIgnoredByRules(relative, entry.isDirectory(), rules))
|
|
1146
1376
|
continue;
|
|
1147
|
-
const absolute =
|
|
1377
|
+
const absolute = path4.join(directory, entry.name);
|
|
1148
1378
|
if (entry.isDirectory())
|
|
1149
1379
|
await walk(absolute, relative, rules);
|
|
1150
1380
|
else if (entry.isFile())
|
|
@@ -1158,19 +1388,19 @@ class Workspace {
|
|
|
1158
1388
|
async isWorkspaceIndexFresh(index) {
|
|
1159
1389
|
try {
|
|
1160
1390
|
for (const fingerprint of index.directories) {
|
|
1161
|
-
const absolute = fingerprint.path === "." ? this.root :
|
|
1162
|
-
const entry = await
|
|
1391
|
+
const absolute = fingerprint.path === "." ? this.root : path4.join(this.root, fingerprint.path);
|
|
1392
|
+
const entry = await lstat2(absolute);
|
|
1163
1393
|
if (entry.isSymbolicLink() || !entry.isDirectory() || !this.sameFingerprint(entry, fingerprint))
|
|
1164
1394
|
return false;
|
|
1165
1395
|
}
|
|
1166
1396
|
for (const fingerprint of index.ignoreFiles) {
|
|
1167
|
-
const absolute =
|
|
1168
|
-
const before = await
|
|
1397
|
+
const absolute = path4.join(this.root, fingerprint.path);
|
|
1398
|
+
const before = await lstat2(absolute);
|
|
1169
1399
|
if (before.isSymbolicLink() || !before.isFile() || !this.sameFingerprint(before, fingerprint))
|
|
1170
1400
|
return false;
|
|
1171
1401
|
if (fingerprint.digest) {
|
|
1172
1402
|
const contents = await readFile(absolute);
|
|
1173
|
-
const after = await
|
|
1403
|
+
const after = await lstat2(absolute);
|
|
1174
1404
|
if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, fingerprint) || digestBytes(contents) !== fingerprint.digest)
|
|
1175
1405
|
return false;
|
|
1176
1406
|
}
|
|
@@ -1244,18 +1474,19 @@ class Workspace {
|
|
|
1244
1474
|
}
|
|
1245
1475
|
async listFiles(relativePath = ".", options = {}) {
|
|
1246
1476
|
const start = await this.safePath(relativePath);
|
|
1247
|
-
if (!(await
|
|
1477
|
+
if (!(await lstat2(start.path)).isDirectory())
|
|
1248
1478
|
throw new Error("list_files requires a directory.");
|
|
1249
|
-
const requestPath = wirePath(
|
|
1479
|
+
const requestPath = wirePath(path4.relative(this.root, start.path)) || ".";
|
|
1250
1480
|
const input = typeof options === "number" ? { limit: options } : options;
|
|
1251
1481
|
const limit = input.limit ?? 200;
|
|
1482
|
+
const includeDigests = input.includeDigests ?? true;
|
|
1252
1483
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 5000)
|
|
1253
1484
|
throw new Error("limit must be between 1 and 5000.");
|
|
1254
1485
|
let after = "";
|
|
1255
1486
|
let cursorIndexVersion;
|
|
1256
1487
|
if (input.cursor) {
|
|
1257
1488
|
const parsed = cursorDecode(input.cursor);
|
|
1258
|
-
if (parsed.v !== 2 || parsed.kind !== "list" || parsed.path !== requestPath || parsed.limit !== limit || typeof parsed.indexVersion !== "string" || typeof parsed.after !== "string") {
|
|
1489
|
+
if (parsed.v !== 2 || parsed.kind !== "list" || parsed.path !== requestPath || parsed.limit !== limit || parsed.includeDigests !== includeDigests || typeof parsed.indexVersion !== "string" || typeof parsed.after !== "string") {
|
|
1259
1490
|
throw new Error("The pagination cursor does not match this list request.");
|
|
1260
1491
|
}
|
|
1261
1492
|
after = parsed.after;
|
|
@@ -1279,16 +1510,28 @@ class Workspace {
|
|
|
1279
1510
|
if (truncated)
|
|
1280
1511
|
selected.pop();
|
|
1281
1512
|
const files = [];
|
|
1282
|
-
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1513
|
+
if (includeDigests) {
|
|
1514
|
+
for (const candidate of selected) {
|
|
1515
|
+
const file = await this.readStableFile(candidate.path);
|
|
1516
|
+
files.push({ path: file.path, size: file.contents.byteLength, digest: file.digest });
|
|
1517
|
+
}
|
|
1518
|
+
} else {
|
|
1519
|
+
files.push(...selected.map(({ path: candidatePath }) => ({ path: candidatePath })));
|
|
1285
1520
|
}
|
|
1286
1521
|
const last = selected.at(-1);
|
|
1287
1522
|
return {
|
|
1288
1523
|
files,
|
|
1289
1524
|
truncated,
|
|
1290
1525
|
...truncated && last ? {
|
|
1291
|
-
nextCursor: cursorEncode({
|
|
1526
|
+
nextCursor: cursorEncode({
|
|
1527
|
+
v: 2,
|
|
1528
|
+
kind: "list",
|
|
1529
|
+
path: requestPath,
|
|
1530
|
+
limit,
|
|
1531
|
+
includeDigests,
|
|
1532
|
+
after: last.path,
|
|
1533
|
+
indexVersion: index.version
|
|
1534
|
+
})
|
|
1292
1535
|
} : {}
|
|
1293
1536
|
};
|
|
1294
1537
|
}
|
|
@@ -1320,7 +1563,7 @@ class Workspace {
|
|
|
1320
1563
|
}
|
|
1321
1564
|
const ordered = requests.map((request) => ({
|
|
1322
1565
|
...request,
|
|
1323
|
-
path: wirePath(
|
|
1566
|
+
path: wirePath(path4.relative(this.root, this.lexicalPath(request.path)))
|
|
1324
1567
|
})).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));
|
|
1325
1568
|
const stableFiles = new Map;
|
|
1326
1569
|
let totalBytes = 0;
|
|
@@ -1355,9 +1598,9 @@ class Workspace {
|
|
|
1355
1598
|
if (!query || query.length > 200)
|
|
1356
1599
|
throw new Error("The search query must be between 1 and 200 characters.");
|
|
1357
1600
|
const start = await this.safePath(relativePath);
|
|
1358
|
-
if (!(await
|
|
1601
|
+
if (!(await lstat2(start.path)).isDirectory())
|
|
1359
1602
|
throw new Error("search_files requires a directory.");
|
|
1360
|
-
const requestPath = wirePath(
|
|
1603
|
+
const requestPath = wirePath(path4.relative(this.root, start.path)) || ".";
|
|
1361
1604
|
const limit = options.limit ?? options.maxMatches ?? 100;
|
|
1362
1605
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)
|
|
1363
1606
|
throw new Error("limit must be between 1 and 500.");
|
|
@@ -1455,9 +1698,9 @@ class Workspace {
|
|
|
1455
1698
|
};
|
|
1456
1699
|
});
|
|
1457
1700
|
const start = await this.safePath(relativePath);
|
|
1458
|
-
if (!(await
|
|
1701
|
+
if (!(await lstat2(start.path)).isDirectory())
|
|
1459
1702
|
throw new Error("searchMany requires a directory.");
|
|
1460
|
-
const requestPath = wirePath(
|
|
1703
|
+
const requestPath = wirePath(path4.relative(this.root, start.path)) || ".";
|
|
1461
1704
|
const prefix = requestPath === "." ? "" : `${requestPath}/`;
|
|
1462
1705
|
const index = await this.getWorkspaceIndex();
|
|
1463
1706
|
let candidateIndex = requestPath === "." ? 0 : firstPathAtLeast(index.files, prefix);
|
|
@@ -1506,16 +1749,16 @@ class Workspace {
|
|
|
1506
1749
|
return this.auditEntries.map((entry) => ({ ...entry }));
|
|
1507
1750
|
}
|
|
1508
1751
|
async secureWorkspaceDirectory(directory, mode = 493) {
|
|
1509
|
-
const resolvedInput =
|
|
1752
|
+
const resolvedInput = path4.resolve(directory);
|
|
1510
1753
|
if (!isInside(this.root, resolvedInput)) {
|
|
1511
1754
|
throw new Error("A mutation directory escapes the workspace.");
|
|
1512
1755
|
}
|
|
1513
|
-
const segments =
|
|
1756
|
+
const segments = path4.relative(this.root, resolvedInput).split(path4.sep).filter(Boolean);
|
|
1514
1757
|
let current = this.root;
|
|
1515
1758
|
for (const segment of segments) {
|
|
1516
|
-
current =
|
|
1759
|
+
current = path4.join(current, segment);
|
|
1517
1760
|
try {
|
|
1518
|
-
const entry = await
|
|
1761
|
+
const entry = await lstat2(current);
|
|
1519
1762
|
if (entry.isSymbolicLink())
|
|
1520
1763
|
throw new Error("A mutation directory must not contain a symbolic link.");
|
|
1521
1764
|
if (!entry.isDirectory())
|
|
@@ -1528,7 +1771,7 @@ class Workspace {
|
|
|
1528
1771
|
} catch (mkdirError) {
|
|
1529
1772
|
if (mkdirError.code !== "EEXIST")
|
|
1530
1773
|
throw mkdirError;
|
|
1531
|
-
const raced = await
|
|
1774
|
+
const raced = await lstat2(current);
|
|
1532
1775
|
if (raced.isSymbolicLink() || !raced.isDirectory()) {
|
|
1533
1776
|
throw new Error("A mutation directory changed during creation.");
|
|
1534
1777
|
}
|
|
@@ -1541,8 +1784,8 @@ class Workspace {
|
|
|
1541
1784
|
return canonical;
|
|
1542
1785
|
}
|
|
1543
1786
|
async stageFile(target, contents, mode) {
|
|
1544
|
-
const directory = await this.secureWorkspaceDirectory(
|
|
1545
|
-
const temporary =
|
|
1787
|
+
const directory = await this.secureWorkspaceDirectory(path4.dirname(target));
|
|
1788
|
+
const temporary = path4.join(directory, `.${path4.basename(target)}.zhivex-${randomUUID()}.tmp`);
|
|
1546
1789
|
const handle = await open(temporary, "wx", mode);
|
|
1547
1790
|
try {
|
|
1548
1791
|
await handle.writeFile(contents);
|
|
@@ -1701,7 +1944,7 @@ class Workspace {
|
|
|
1701
1944
|
const destination = await this.safePath(parsed.destination, { allowMissing: true });
|
|
1702
1945
|
if (destination.exists)
|
|
1703
1946
|
throw new Error(`Move destination already exists: ${parsed.destination}.`);
|
|
1704
|
-
await this.secureWorkspaceDirectory(
|
|
1947
|
+
await this.secureWorkspaceDirectory(path4.dirname(destination.path));
|
|
1705
1948
|
const destinationRecheck = await this.safePath(parsed.destination, { allowMissing: true });
|
|
1706
1949
|
if (destinationRecheck.exists)
|
|
1707
1950
|
throw new Error(`Move destination already exists: ${parsed.destination}.`);
|
|
@@ -1729,9 +1972,9 @@ class Workspace {
|
|
|
1729
1972
|
const segments = [".zhivex-harness", "quarantine"];
|
|
1730
1973
|
let current = this.root;
|
|
1731
1974
|
for (const segment of segments) {
|
|
1732
|
-
current =
|
|
1975
|
+
current = path4.join(current, segment);
|
|
1733
1976
|
try {
|
|
1734
|
-
const entry = await
|
|
1977
|
+
const entry = await lstat2(current);
|
|
1735
1978
|
if (entry.isSymbolicLink()) {
|
|
1736
1979
|
throw new Error("Harness quarantine must not resolve through a symbolic link.");
|
|
1737
1980
|
}
|
|
@@ -1746,7 +1989,7 @@ class Workspace {
|
|
|
1746
1989
|
} catch (mkdirError) {
|
|
1747
1990
|
if (mkdirError.code !== "EEXIST")
|
|
1748
1991
|
throw mkdirError;
|
|
1749
|
-
const racedEntry = await
|
|
1992
|
+
const racedEntry = await lstat2(current);
|
|
1750
1993
|
if (racedEntry.isSymbolicLink() || !racedEntry.isDirectory()) {
|
|
1751
1994
|
throw new Error("Harness quarantine path changed during creation.");
|
|
1752
1995
|
}
|
|
@@ -1760,19 +2003,19 @@ class Workspace {
|
|
|
1760
2003
|
}
|
|
1761
2004
|
async writeManifest(manifest) {
|
|
1762
2005
|
const directory = await this.secureQuarantineDirectory(true);
|
|
1763
|
-
const target =
|
|
2006
|
+
const target = path4.join(directory, `${manifest.quarantineId}.json`);
|
|
1764
2007
|
const temporary = await this.stageFile(target, `${JSON.stringify(manifest, null, 2)}
|
|
1765
2008
|
`, 384);
|
|
1766
2009
|
await rename(temporary, target);
|
|
1767
2010
|
}
|
|
1768
2011
|
async readInternalRegularFile(target, label, maxBytes) {
|
|
1769
|
-
const before = await
|
|
2012
|
+
const before = await lstat2(target);
|
|
1770
2013
|
if (before.isSymbolicLink() || !before.isFile())
|
|
1771
2014
|
throw new Error(`${label} must be a regular non-symlink file.`);
|
|
1772
2015
|
if (before.size > maxBytes)
|
|
1773
2016
|
throw new Error(`${label} exceeds its size limit.`);
|
|
1774
2017
|
const contents = await readFile(target);
|
|
1775
|
-
const after = await
|
|
2018
|
+
const after = await lstat2(target);
|
|
1776
2019
|
if (after.isSymbolicLink() || !after.isFile() || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
|
|
1777
2020
|
throw new Error(`${label} changed while it was being read.`);
|
|
1778
2021
|
}
|
|
@@ -1785,7 +2028,7 @@ class Workspace {
|
|
|
1785
2028
|
throw new Error(`Stale quarantine rejected for ${parsed.path}.`);
|
|
1786
2029
|
const quarantineId = `${Date.now()}-${randomUUID()}`;
|
|
1787
2030
|
const directory = await this.secureQuarantineDirectory(true);
|
|
1788
|
-
const dataPath =
|
|
2031
|
+
const dataPath = path4.join(directory, `${quarantineId}.data`);
|
|
1789
2032
|
if ((await this.readStableFile(parsed.path)).digest !== parsed.expectedDigest)
|
|
1790
2033
|
throw new Error(`Stale quarantine rejected for ${parsed.path}.`);
|
|
1791
2034
|
await link(source.absolutePath, dataPath);
|
|
@@ -1807,7 +2050,7 @@ class Workspace {
|
|
|
1807
2050
|
await unlink(source.absolutePath);
|
|
1808
2051
|
} catch (error) {
|
|
1809
2052
|
await unlink(dataPath).catch(() => {});
|
|
1810
|
-
await unlink(
|
|
2053
|
+
await unlink(path4.join(directory, `${quarantineId}.json`)).catch(() => {});
|
|
1811
2054
|
throw error;
|
|
1812
2055
|
}
|
|
1813
2056
|
this.invalidateWorkspaceIndex();
|
|
@@ -1819,7 +2062,7 @@ class Workspace {
|
|
|
1819
2062
|
const directory = await this.secureQuarantineDirectory(false);
|
|
1820
2063
|
let manifest;
|
|
1821
2064
|
try {
|
|
1822
|
-
const manifestFile = await this.readInternalRegularFile(
|
|
2065
|
+
const manifestFile = await this.readInternalRegularFile(path4.join(directory, `${parsed.quarantineId}.json`), "Quarantine manifest", 64 * 1024);
|
|
1823
2066
|
manifest = JSON.parse(manifestFile.contents.toString("utf8"));
|
|
1824
2067
|
} catch {
|
|
1825
2068
|
throw new Error(`Quarantine entry was not found: ${parsed.quarantineId}.`);
|
|
@@ -1827,7 +2070,7 @@ class Workspace {
|
|
|
1827
2070
|
if (manifest.schemaVersion !== 1 || manifest.quarantineId !== parsed.quarantineId || manifest.status !== "quarantined") {
|
|
1828
2071
|
throw new Error(`Quarantine entry is not restorable: ${parsed.quarantineId}.`);
|
|
1829
2072
|
}
|
|
1830
|
-
const dataPath =
|
|
2073
|
+
const dataPath = path4.join(directory, `${parsed.quarantineId}.data`);
|
|
1831
2074
|
const dataFile = await this.readInternalRegularFile(dataPath, "Quarantine payload", MAX_FILE_BYTES);
|
|
1832
2075
|
const digest2 = digestBytes(dataFile.contents);
|
|
1833
2076
|
if (digest2 !== manifest.digest || parsed.expectedDigest && parsed.expectedDigest !== digest2) {
|
|
@@ -1837,16 +2080,16 @@ class Workspace {
|
|
|
1837
2080
|
const destination = await this.safePath(destinationPath, { allowMissing: true });
|
|
1838
2081
|
if (destination.exists)
|
|
1839
2082
|
throw new Error(`Restore destination already exists: ${destinationPath}.`);
|
|
1840
|
-
await this.secureWorkspaceDirectory(
|
|
2083
|
+
await this.secureWorkspaceDirectory(path4.dirname(destination.path));
|
|
1841
2084
|
const destinationRecheck = await this.safePath(destinationPath, { allowMissing: true });
|
|
1842
2085
|
if (destinationRecheck.exists)
|
|
1843
2086
|
throw new Error(`Restore destination already exists: ${destinationPath}.`);
|
|
1844
|
-
const dataRecheck = await
|
|
2087
|
+
const dataRecheck = await lstat2(dataPath);
|
|
1845
2088
|
if (dataRecheck.isSymbolicLink() || !dataRecheck.isFile() || dataRecheck.dev !== dataFile.stat.dev || dataRecheck.ino !== dataFile.stat.ino || dataRecheck.size !== dataFile.stat.size || dataRecheck.mtimeMs !== dataFile.stat.mtimeMs) {
|
|
1846
2089
|
throw new Error("Quarantine payload changed before restore commit.");
|
|
1847
2090
|
}
|
|
1848
2091
|
await link(dataPath, destination.path);
|
|
1849
|
-
const linkedDestination = await
|
|
2092
|
+
const linkedDestination = await lstat2(destination.path);
|
|
1850
2093
|
if (linkedDestination.isSymbolicLink() || !linkedDestination.isFile() || linkedDestination.dev !== dataFile.stat.dev || linkedDestination.ino !== dataFile.stat.ino || digestBytes(await readFile(destination.path)) !== digest2) {
|
|
1851
2094
|
await unlink(destination.path).catch(() => {});
|
|
1852
2095
|
throw new Error("Restore destination did not match the quarantined payload.");
|
|
@@ -1884,24 +2127,17 @@ class Workspace {
|
|
|
1884
2127
|
return { quarantineId: parsed.quarantineId, path: destinationPath, digest: digest2, audit };
|
|
1885
2128
|
}
|
|
1886
2129
|
async runCheck(check, expectedScript, allowedChecks = DEFAULT_CHECKS) {
|
|
1887
|
-
|
|
1888
|
-
throw new Error(`The check "${check}" is not in the explicit allowlist.`);
|
|
1889
|
-
}
|
|
1890
|
-
let scripts = {};
|
|
2130
|
+
let manifest;
|
|
1891
2131
|
try {
|
|
1892
|
-
|
|
1893
|
-
scripts = packageJson.scripts ?? {};
|
|
2132
|
+
manifest = JSON.parse((await this.readStableFile("package.json", false)).contents.toString("utf8"));
|
|
1894
2133
|
} catch (error) {
|
|
1895
2134
|
if (error.code === "ENOENT")
|
|
1896
2135
|
throw new Error("The workspace does not contain a package.json file.");
|
|
1897
2136
|
throw error;
|
|
1898
2137
|
}
|
|
1899
|
-
|
|
1900
|
-
throw new Error(`package.json does not define the "${check}" script.`);
|
|
1901
|
-
if (scripts[check] !== expectedScript)
|
|
1902
|
-
throw new Error(`The "${check}" script changed or does not match expectedScript.`);
|
|
2138
|
+
const resolved = await resolvePackageCheckCommand(this.root, manifest, check, expectedScript, allowedChecks);
|
|
1903
2139
|
try {
|
|
1904
|
-
return await spawnBounded(
|
|
2140
|
+
return await spawnBounded(resolved.command, this.root);
|
|
1905
2141
|
} finally {
|
|
1906
2142
|
this.invalidateWorkspaceIndex();
|
|
1907
2143
|
}
|
|
@@ -1917,8 +2153,8 @@ class Workspace {
|
|
|
1917
2153
|
}
|
|
1918
2154
|
|
|
1919
2155
|
// src/operations.ts
|
|
1920
|
-
import { chmod, lstat as
|
|
1921
|
-
import
|
|
2156
|
+
import { chmod, lstat as lstat4, mkdir as mkdir2, open as open2 } from "node:fs/promises";
|
|
2157
|
+
import path6 from "node:path";
|
|
1922
2158
|
import {
|
|
1923
2159
|
cancelAgentRun,
|
|
1924
2160
|
cancelAgentRunTree,
|
|
@@ -1935,29 +2171,95 @@ import {
|
|
|
1935
2171
|
createSqliteAgentRunStore
|
|
1936
2172
|
} from "@zhivex-ai/agents/ops";
|
|
1937
2173
|
import { createAgentRunLedger } from "@zhivex-ai/agents/control-plane";
|
|
1938
|
-
|
|
2174
|
+
|
|
2175
|
+
// src/sqlite-database.ts
|
|
2176
|
+
import { createRequire } from "node:module";
|
|
2177
|
+
var require2 = createRequire(import.meta.url);
|
|
2178
|
+
var DatabaseSyncConstructor;
|
|
2179
|
+
var loadDatabaseSync = () => {
|
|
2180
|
+
DatabaseSyncConstructor ??= require2("node:sqlite").DatabaseSync;
|
|
2181
|
+
return DatabaseSyncConstructor;
|
|
2182
|
+
};
|
|
2183
|
+
var normalizeNumberedParameters = (sql) => {
|
|
2184
|
+
const indexes = new Set;
|
|
2185
|
+
const normalizedSql = sql.replace(/\?([1-9]\d*)/g, (_match, rawIndex) => {
|
|
2186
|
+
const index = Number(rawIndex);
|
|
2187
|
+
indexes.add(index);
|
|
2188
|
+
return `$__zhivex_${index}`;
|
|
2189
|
+
});
|
|
2190
|
+
return {
|
|
2191
|
+
sql: normalizedSql,
|
|
2192
|
+
indexes: [...indexes].sort((left, right) => left - right)
|
|
2193
|
+
};
|
|
2194
|
+
};
|
|
2195
|
+
var numberedBindings = (indexes, parameters) => {
|
|
2196
|
+
if (indexes.length === 0)
|
|
2197
|
+
return;
|
|
2198
|
+
const maximumIndex = indexes.at(-1);
|
|
2199
|
+
if (parameters.length !== maximumIndex) {
|
|
2200
|
+
throw new Error(`SQLite numbered parameters require exactly ${maximumIndex} binding${maximumIndex === 1 ? "" : "s"}; received ${parameters.length}.`);
|
|
2201
|
+
}
|
|
2202
|
+
return Object.fromEntries(indexes.map((index) => [`$__zhivex_${index}`, parameters[index - 1]]));
|
|
2203
|
+
};
|
|
2204
|
+
|
|
2205
|
+
class SqliteDatabase {
|
|
2206
|
+
#database;
|
|
2207
|
+
constructor(databasePath, options = {}) {
|
|
2208
|
+
options.create;
|
|
2209
|
+
options.strict;
|
|
2210
|
+
const DatabaseSync = loadDatabaseSync();
|
|
2211
|
+
this.#database = new DatabaseSync(databasePath, {
|
|
2212
|
+
...options.readonly === undefined ? {} : { readOnly: options.readonly }
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
exec(sql) {
|
|
2216
|
+
this.#database.exec(sql);
|
|
2217
|
+
}
|
|
2218
|
+
query(sql) {
|
|
2219
|
+
const normalized = normalizeNumberedParameters(sql);
|
|
2220
|
+
const statement = this.#database.prepare(normalized.sql);
|
|
2221
|
+
const invoke = (method, parameters) => {
|
|
2222
|
+
const bindings = numberedBindings(normalized.indexes, parameters);
|
|
2223
|
+
return bindings ? method(bindings) : method(...parameters);
|
|
2224
|
+
};
|
|
2225
|
+
return {
|
|
2226
|
+
run: (...parameters) => invoke(statement.run.bind(statement), parameters),
|
|
2227
|
+
get: (...parameters) => invoke(statement.get.bind(statement), parameters),
|
|
2228
|
+
all: (...parameters) => invoke(statement.all.bind(statement), parameters)
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
close(throwOnError = true) {
|
|
2232
|
+
try {
|
|
2233
|
+
this.#database.close();
|
|
2234
|
+
} catch (error) {
|
|
2235
|
+
if (throwOnError) {
|
|
2236
|
+
throw error;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
1939
2241
|
|
|
1940
2242
|
// src/state-directory.ts
|
|
1941
|
-
import { lstat as
|
|
1942
|
-
import
|
|
2243
|
+
import { lstat as lstat3 } from "node:fs/promises";
|
|
2244
|
+
import path5 from "node:path";
|
|
1943
2245
|
var SENSITIVE_STATE_SEGMENTS = new Set([".git", ".env", ".npmrc", "dist", "node_modules", "src"]);
|
|
1944
2246
|
var isInside2 = (root, candidate) => {
|
|
1945
|
-
const relative =
|
|
1946
|
-
return relative === "" || !relative.startsWith(`..${
|
|
2247
|
+
const relative = path5.relative(root, candidate);
|
|
2248
|
+
return relative === "" || !relative.startsWith(`..${path5.sep}`) && relative !== ".." && !path5.isAbsolute(relative);
|
|
1947
2249
|
};
|
|
1948
2250
|
var validateStateDirectory = async (workspace, stateDirectory) => {
|
|
1949
|
-
if (stateDirectory === workspace || stateDirectory ===
|
|
2251
|
+
if (stateDirectory === workspace || stateDirectory === path5.parse(stateDirectory).root) {
|
|
1950
2252
|
throw new Error("The state directory cannot be the workspace or filesystem root.");
|
|
1951
2253
|
}
|
|
1952
2254
|
const insideWorkspace = isInside2(workspace, stateDirectory);
|
|
1953
|
-
const relativeSegments = insideWorkspace ?
|
|
2255
|
+
const relativeSegments = insideWorkspace ? path5.relative(workspace, stateDirectory).split(path5.sep).filter(Boolean) : stateDirectory.slice(path5.parse(stateDirectory).root.length).split(path5.sep).filter(Boolean);
|
|
1954
2256
|
const sensitiveSegment = insideWorkspace ? relativeSegments.find((segment) => SENSITIVE_STATE_SEGMENTS.has(segment)) : undefined;
|
|
1955
2257
|
if (sensitiveSegment) {
|
|
1956
2258
|
throw new Error(`The state directory is inside the protected workspace path: ${sensitiveSegment}.`);
|
|
1957
2259
|
}
|
|
1958
2260
|
if (!insideWorkspace) {
|
|
1959
2261
|
try {
|
|
1960
|
-
const externalEntry = await
|
|
2262
|
+
const externalEntry = await lstat3(stateDirectory);
|
|
1961
2263
|
if (externalEntry.isSymbolicLink()) {
|
|
1962
2264
|
throw new Error(`The state directory must not be a symbolic link: ${stateDirectory}.`);
|
|
1963
2265
|
}
|
|
@@ -1973,9 +2275,9 @@ var validateStateDirectory = async (workspace, stateDirectory) => {
|
|
|
1973
2275
|
}
|
|
1974
2276
|
let current = workspace;
|
|
1975
2277
|
for (const segment of relativeSegments) {
|
|
1976
|
-
current =
|
|
2278
|
+
current = path5.join(current, segment);
|
|
1977
2279
|
try {
|
|
1978
|
-
const entry = await
|
|
2280
|
+
const entry = await lstat3(current);
|
|
1979
2281
|
if (entry.isSymbolicLink()) {
|
|
1980
2282
|
throw new Error(`The state directory must not resolve through a symbolic link: ${current}.`);
|
|
1981
2283
|
}
|
|
@@ -2162,7 +2464,7 @@ var sqliteAdapter = (database) => ({
|
|
|
2162
2464
|
});
|
|
2163
2465
|
var ensurePrivateStateDirectory = async (directory) => {
|
|
2164
2466
|
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
2165
|
-
const entry = await
|
|
2467
|
+
const entry = await lstat4(directory);
|
|
2166
2468
|
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
2167
2469
|
throw new Error(`The state directory must be a real directory: ${directory}.`);
|
|
2168
2470
|
}
|
|
@@ -2218,17 +2520,17 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
2218
2520
|
backend: "file",
|
|
2219
2521
|
store,
|
|
2220
2522
|
memory: createFileAgentMemoryStore({
|
|
2221
|
-
directory:
|
|
2523
|
+
directory: path6.join(config.stateDirectory, "memory"),
|
|
2222
2524
|
scope: config.scope
|
|
2223
2525
|
}),
|
|
2224
2526
|
migration,
|
|
2225
2527
|
close() {}
|
|
2226
2528
|
};
|
|
2227
2529
|
}
|
|
2228
|
-
const databasePath =
|
|
2530
|
+
const databasePath = path6.join(config.stateDirectory, HARNESS_SQLITE_FILE);
|
|
2229
2531
|
let databaseEntry;
|
|
2230
2532
|
try {
|
|
2231
|
-
databaseEntry = await
|
|
2533
|
+
databaseEntry = await lstat4(databasePath);
|
|
2232
2534
|
} catch (error) {
|
|
2233
2535
|
if (error.code !== "ENOENT") {
|
|
2234
2536
|
throw error;
|
|
@@ -2241,12 +2543,12 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
2241
2543
|
throw creationError;
|
|
2242
2544
|
}
|
|
2243
2545
|
}
|
|
2244
|
-
databaseEntry = await
|
|
2546
|
+
databaseEntry = await lstat4(databasePath);
|
|
2245
2547
|
}
|
|
2246
2548
|
if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
|
|
2247
2549
|
throw new Error(`The SQLite state path must be a real file: ${databasePath}.`);
|
|
2248
2550
|
}
|
|
2249
|
-
const database = new
|
|
2551
|
+
const database = new SqliteDatabase(databasePath, { create: false, strict: true });
|
|
2250
2552
|
let closed = false;
|
|
2251
2553
|
const closeDatabase = () => {
|
|
2252
2554
|
if (!closed) {
|
|
@@ -2255,7 +2557,7 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
2255
2557
|
}
|
|
2256
2558
|
};
|
|
2257
2559
|
try {
|
|
2258
|
-
const openedEntry = await
|
|
2560
|
+
const openedEntry = await lstat4(databasePath);
|
|
2259
2561
|
if (openedEntry.isSymbolicLink() || !openedEntry.isFile() || openedEntry.dev !== databaseEntry.dev || openedEntry.ino !== databaseEntry.ino) {
|
|
2260
2562
|
throw new Error(`The SQLite state path changed while it was being opened: ${databasePath}.`);
|
|
2261
2563
|
}
|
|
@@ -2282,8 +2584,8 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
2282
2584
|
};
|
|
2283
2585
|
|
|
2284
2586
|
// src/mcp.ts
|
|
2285
|
-
import { lstat as
|
|
2286
|
-
import
|
|
2587
|
+
import { lstat as lstat5, readFile as readFile2, realpath as realpath2 } from "node:fs/promises";
|
|
2588
|
+
import path7 from "node:path";
|
|
2287
2589
|
import {
|
|
2288
2590
|
createMcpToolSet,
|
|
2289
2591
|
serializeJsonValue
|
|
@@ -2291,15 +2593,19 @@ import {
|
|
|
2291
2593
|
import { z as z2 } from "zod";
|
|
2292
2594
|
|
|
2293
2595
|
// src/version.ts
|
|
2294
|
-
import { createRequire } from "module";
|
|
2295
|
-
var packageMetadata =
|
|
2596
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
2597
|
+
var packageMetadata = createRequire2(import.meta.url)("../package.json");
|
|
2296
2598
|
if (typeof packageMetadata.version !== "string" || packageMetadata.version.length === 0) {
|
|
2297
2599
|
throw new Error("package.json must contain a non-empty version.");
|
|
2298
2600
|
}
|
|
2601
|
+
if (typeof packageMetadata.engines?.node !== "string" || packageMetadata.engines.node.length === 0) {
|
|
2602
|
+
throw new Error("package.json must contain a non-empty engines.node requirement.");
|
|
2603
|
+
}
|
|
2299
2604
|
if (typeof packageMetadata.engines?.bun !== "string" || packageMetadata.engines.bun.length === 0) {
|
|
2300
2605
|
throw new Error("package.json must contain a non-empty engines.bun requirement.");
|
|
2301
2606
|
}
|
|
2302
2607
|
var HARNESS_VERSION = packageMetadata.version;
|
|
2608
|
+
var NODE_ENGINE_RANGE = packageMetadata.engines.node;
|
|
2303
2609
|
var BUN_ENGINE_RANGE = packageMetadata.engines.bun;
|
|
2304
2610
|
|
|
2305
2611
|
// src/mcp.ts
|
|
@@ -2333,8 +2639,8 @@ var configurationSchema = z2.object({
|
|
|
2333
2639
|
servers: z2.array(serverSchema).max(20)
|
|
2334
2640
|
}).strict();
|
|
2335
2641
|
var isInsidePath = (root, candidate) => {
|
|
2336
|
-
const relative =
|
|
2337
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
2642
|
+
const relative = path7.relative(root, candidate);
|
|
2643
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path7.sep}`) && !path7.isAbsolute(relative);
|
|
2338
2644
|
};
|
|
2339
2645
|
var validatedHttpUrl = (value, serverName) => {
|
|
2340
2646
|
let url;
|
|
@@ -2418,7 +2724,7 @@ var normalizeHarnessMcpConfiguration = (value) => {
|
|
|
2418
2724
|
return { schemaVersion: HARNESS_MCP_CONFIG_SCHEMA_VERSION, servers };
|
|
2419
2725
|
};
|
|
2420
2726
|
var loadHarnessMcpConfiguration = async (workspace, configPath) => {
|
|
2421
|
-
const entry = await
|
|
2727
|
+
const entry = await lstat5(configPath);
|
|
2422
2728
|
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
2423
2729
|
throw new Error(`MCP configuration must be a regular non-symlink file: ${configPath}.`);
|
|
2424
2730
|
}
|
|
@@ -2673,7 +2979,7 @@ var createHarnessMcpTools = async (configuration, options = {}) => {
|
|
|
2673
2979
|
var mcpConfigurationFingerprintInput = (configuration) => serializeJsonValue(configuration);
|
|
2674
2980
|
|
|
2675
2981
|
// src/orchestration.ts
|
|
2676
|
-
import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
|
|
2982
|
+
import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
|
|
2677
2983
|
import {
|
|
2678
2984
|
applySafetyPolicyToAgent,
|
|
2679
2985
|
createBudgetGuard,
|
|
@@ -2698,8 +3004,8 @@ var HARNESS_SUBAGENT_PROFILE_DESCRIPTORS = [
|
|
|
2698
3004
|
{
|
|
2699
3005
|
id: "tester",
|
|
2700
3006
|
toolName: "delegate_tester",
|
|
2701
|
-
description: "Delegate focused verification to a tester with read-only inspection and approved
|
|
2702
|
-
instructions: "Verify the delegated behavior with repository evidence and declared
|
|
3007
|
+
description: "Delegate focused verification to a tester with read-only inspection and approved package-manager checks.",
|
|
3008
|
+
instructions: "Verify the delegated behavior with repository evidence and declared package-manager checks. Do not edit files. Report exact failures and limits.",
|
|
2703
3009
|
toolNames: ["list_files", "read_file", "read_files", "search_files", "search_many", "run_check", "git_diff"]
|
|
2704
3010
|
},
|
|
2705
3011
|
{
|
|
@@ -2844,12 +3150,13 @@ var runHarnessReviewGroup = async (runtime, input, profiles = ["explorer", "revi
|
|
|
2844
3150
|
};
|
|
2845
3151
|
|
|
2846
3152
|
// src/execution-environment.ts
|
|
2847
|
-
import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
|
|
2848
|
-
import {
|
|
3153
|
+
import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypto";
|
|
3154
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3155
|
+
import { constants as fsConstants } from "node:fs";
|
|
2849
3156
|
import {
|
|
2850
3157
|
chmod as chmod2,
|
|
2851
3158
|
copyFile,
|
|
2852
|
-
lstat as
|
|
3159
|
+
lstat as lstat6,
|
|
2853
3160
|
mkdir as mkdir3,
|
|
2854
3161
|
readFile as readFile3,
|
|
2855
3162
|
readdir as readdir2,
|
|
@@ -2857,12 +3164,13 @@ import {
|
|
|
2857
3164
|
rename as rename2,
|
|
2858
3165
|
rm,
|
|
2859
3166
|
writeFile
|
|
2860
|
-
} from "fs/promises";
|
|
2861
|
-
import
|
|
3167
|
+
} from "node:fs/promises";
|
|
3168
|
+
import path8 from "node:path";
|
|
2862
3169
|
import { createAgentExecutionEnvironmentBinding } from "@zhivex-ai/agents/beta";
|
|
2863
3170
|
var HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION = 1;
|
|
2864
3171
|
var HARNESS_OCI_LABEL = "com.zhivex.harness.execution";
|
|
2865
3172
|
var HARNESS_OCI_LABEL_VALUE = "v1";
|
|
3173
|
+
var HARNESS_OCI_OWNER_PID_LABEL = "com.zhivex.harness.owner-pid";
|
|
2866
3174
|
var TERMINAL_STATUSES = new Set([
|
|
2867
3175
|
"completed",
|
|
2868
3176
|
"failed",
|
|
@@ -2877,6 +3185,7 @@ var BUILT_IN_TOOL_NAMES = new Set([
|
|
|
2877
3185
|
"search_many",
|
|
2878
3186
|
"propose_edits",
|
|
2879
3187
|
"apply_patch",
|
|
3188
|
+
"apply_reviewed_edits",
|
|
2880
3189
|
"move_file",
|
|
2881
3190
|
"quarantine_file",
|
|
2882
3191
|
"restore_file",
|
|
@@ -2884,19 +3193,22 @@ var BUILT_IN_TOOL_NAMES = new Set([
|
|
|
2884
3193
|
"mutation_audit",
|
|
2885
3194
|
"git_diff",
|
|
2886
3195
|
"run_environment_command",
|
|
3196
|
+
"run_environment_batch",
|
|
2887
3197
|
"inspect_environment_patch",
|
|
2888
3198
|
"apply_environment_patch",
|
|
3199
|
+
"verify_and_apply_environment_patch",
|
|
3200
|
+
"verify_and_apply_reviewed_edits",
|
|
2889
3201
|
"environment_status"
|
|
2890
3202
|
]);
|
|
2891
3203
|
var digest2 = (value) => `sha256:${createHash6("sha256").update(value).digest("hex")}`;
|
|
2892
3204
|
var runHash = (runId) => createHash6("sha256").update(runId).digest("hex").slice(0, 24);
|
|
2893
3205
|
var isInside3 = (root, candidate) => {
|
|
2894
|
-
const relative =
|
|
2895
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
3206
|
+
const relative = path8.relative(root, candidate);
|
|
3207
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relative);
|
|
2896
3208
|
};
|
|
2897
3209
|
var privateDirectory = async (directory) => {
|
|
2898
3210
|
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
2899
|
-
const entry = await
|
|
3211
|
+
const entry = await lstat6(directory);
|
|
2900
3212
|
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
2901
3213
|
throw new Error(`Execution artifact directory must be a real directory: ${directory}.`);
|
|
2902
3214
|
}
|
|
@@ -2909,14 +3221,19 @@ var atomicJson = async (target, value) => {
|
|
|
2909
3221
|
await rename2(temporary, target);
|
|
2910
3222
|
};
|
|
2911
3223
|
var commandDisplay = (command) => command.map((value) => /^[A-Za-z0-9_./:=+-]+$/.test(value) ? value : JSON.stringify(value)).join(" ");
|
|
3224
|
+
var elapsedMilliseconds = (startedAt) => Number(process.hrtime.bigint() - startedAt) / 1e6;
|
|
2912
3225
|
var spawnBounded2 = async (command, options) => {
|
|
2913
|
-
const child =
|
|
3226
|
+
const child = spawn2(command[0], command.slice(1), {
|
|
2914
3227
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
2915
3228
|
env: options.env ?? {},
|
|
2916
|
-
|
|
2917
|
-
stdout: "pipe",
|
|
2918
|
-
stderr: "pipe"
|
|
3229
|
+
stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"]
|
|
2919
3230
|
});
|
|
3231
|
+
if (options.input !== undefined) {
|
|
3232
|
+
child.stdin?.on("error", () => {
|
|
3233
|
+
return;
|
|
3234
|
+
});
|
|
3235
|
+
child.stdin?.end(options.input);
|
|
3236
|
+
}
|
|
2920
3237
|
let timedOut = false;
|
|
2921
3238
|
let cancelled = false;
|
|
2922
3239
|
let outputLimitExceeded = false;
|
|
@@ -2944,22 +3261,19 @@ var spawnBounded2 = async (command, options) => {
|
|
|
2944
3261
|
options.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
2945
3262
|
}
|
|
2946
3263
|
const consume = async (stream) => {
|
|
2947
|
-
const reader = stream.getReader();
|
|
2948
3264
|
const chunks = [];
|
|
2949
3265
|
let omittedBytes = 0;
|
|
2950
|
-
for (
|
|
2951
|
-
const
|
|
2952
|
-
|
|
2953
|
-
break;
|
|
2954
|
-
totalOutputBytes += next.value.byteLength;
|
|
3266
|
+
for await (const value of stream) {
|
|
3267
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
3268
|
+
totalOutputBytes += chunk.byteLength;
|
|
2955
3269
|
if (retainedOutputBytes < options.maxOutputBytes) {
|
|
2956
3270
|
const remaining = options.maxOutputBytes - retainedOutputBytes;
|
|
2957
|
-
const kept =
|
|
3271
|
+
const kept = chunk.byteLength <= remaining ? chunk : chunk.subarray(0, remaining);
|
|
2958
3272
|
chunks.push(kept);
|
|
2959
3273
|
retainedOutputBytes += kept.byteLength;
|
|
2960
|
-
omittedBytes +=
|
|
3274
|
+
omittedBytes += chunk.byteLength - kept.byteLength;
|
|
2961
3275
|
} else {
|
|
2962
|
-
omittedBytes +=
|
|
3276
|
+
omittedBytes += chunk.byteLength;
|
|
2963
3277
|
}
|
|
2964
3278
|
if (totalOutputBytes > options.maxOutputBytes) {
|
|
2965
3279
|
outputLimitExceeded = true;
|
|
@@ -2968,13 +3282,17 @@ var spawnBounded2 = async (command, options) => {
|
|
|
2968
3282
|
}
|
|
2969
3283
|
const text = new TextDecoder().decode(Buffer.concat(chunks));
|
|
2970
3284
|
return omittedBytes > 0 ? `${text}
|
|
2971
|
-
|
|
3285
|
+
… output limit exceeded (${omittedBytes} bytes omitted)` : text;
|
|
2972
3286
|
};
|
|
3287
|
+
const exited = new Promise((resolve, reject) => {
|
|
3288
|
+
child.once("error", reject);
|
|
3289
|
+
child.once("exit", (exitCode) => resolve(exitCode ?? 137));
|
|
3290
|
+
});
|
|
2973
3291
|
try {
|
|
2974
3292
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2975
3293
|
consume(child.stdout),
|
|
2976
3294
|
consume(child.stderr),
|
|
2977
|
-
|
|
3295
|
+
exited
|
|
2978
3296
|
]);
|
|
2979
3297
|
return {
|
|
2980
3298
|
command,
|
|
@@ -2998,6 +3316,16 @@ var parseRuntimeVersion = (stdout, runtime) => {
|
|
|
2998
3316
|
};
|
|
2999
3317
|
var validContainerIds = (stdout) => stdout.split(/\r?\n/).map((value) => value.trim()).filter((value) => /^[a-f0-9]{12,64}$/.test(value));
|
|
3000
3318
|
var validVolumeNames = (stdout) => stdout.split(/\r?\n/).map((value) => value.trim()).filter((value) => /^zhivex-harness-[A-Za-z0-9_.-]+$/.test(value));
|
|
3319
|
+
var hostProcessIsAlive = (pid) => {
|
|
3320
|
+
if (!Number.isSafeInteger(pid) || pid < 1)
|
|
3321
|
+
return false;
|
|
3322
|
+
try {
|
|
3323
|
+
process.kill(pid, 0);
|
|
3324
|
+
return true;
|
|
3325
|
+
} catch (error) {
|
|
3326
|
+
return error.code === "EPERM";
|
|
3327
|
+
}
|
|
3328
|
+
};
|
|
3001
3329
|
var MAX_SYNC_ENTRIES = 1e4;
|
|
3002
3330
|
var updateSnapshotMetadata = (target, kind, relative, entry) => {
|
|
3003
3331
|
target.update([
|
|
@@ -3020,7 +3348,7 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
3020
3348
|
const visit = async (directory, relativeDirectory = "") => {
|
|
3021
3349
|
for (const name of (await readdir2(directory)).sort()) {
|
|
3022
3350
|
const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
|
3023
|
-
const absolute =
|
|
3351
|
+
const absolute = path8.join(directory, name);
|
|
3024
3352
|
if (!isInside3(stagedRoot, absolute) || relative.length > 1024) {
|
|
3025
3353
|
throw new Error(`OCI workspace export contains an invalid path: ${relative}.`);
|
|
3026
3354
|
}
|
|
@@ -3032,7 +3360,7 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
3032
3360
|
await rm(absolute, { recursive: true, force: true });
|
|
3033
3361
|
continue;
|
|
3034
3362
|
}
|
|
3035
|
-
const entry = await
|
|
3363
|
+
const entry = await lstat6(absolute);
|
|
3036
3364
|
if (entry.isSymbolicLink()) {
|
|
3037
3365
|
throw new Error(`OCI workspace export contains a symbolic link: ${relative}.`);
|
|
3038
3366
|
}
|
|
@@ -3046,7 +3374,7 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
3046
3374
|
throw new Error(`OCI workspace export contains a special or linked file: ${relative}.`);
|
|
3047
3375
|
}
|
|
3048
3376
|
const contents = await readFile3(absolute);
|
|
3049
|
-
const after = await
|
|
3377
|
+
const after = await lstat6(absolute);
|
|
3050
3378
|
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)) {
|
|
3051
3379
|
throw new Error(`OCI workspace export changed while it was being validated: ${relative}.`);
|
|
3052
3380
|
}
|
|
@@ -3058,10 +3386,10 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
3058
3386
|
updateSnapshotMetadata(metadata, "file", relative, after);
|
|
3059
3387
|
seal.update(`file\x00${relative}\x00${entry.mode & 511}\x00${entry.size}\x00${contentDigest}\x00`);
|
|
3060
3388
|
if (entry.size > maxFileWriteBytes) {
|
|
3061
|
-
const previous =
|
|
3389
|
+
const previous = path8.join(previousRoot, ...relative.split("/"));
|
|
3062
3390
|
let unchanged = false;
|
|
3063
3391
|
try {
|
|
3064
|
-
const previousEntry = await
|
|
3392
|
+
const previousEntry = await lstat6(previous);
|
|
3065
3393
|
unchanged = previousEntry.isFile() && !previousEntry.isSymbolicLink() && previousEntry.size === entry.size && digest2(await readFile3(previous)) === contentDigest;
|
|
3066
3394
|
} catch (error) {
|
|
3067
3395
|
if (error.code !== "ENOENT")
|
|
@@ -3090,7 +3418,7 @@ var snapshotMetadataFingerprint = async (root, maxWorkspaceBytes) => {
|
|
|
3090
3418
|
const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
|
3091
3419
|
if (relative === "node_modules")
|
|
3092
3420
|
continue;
|
|
3093
|
-
const absolute =
|
|
3421
|
+
const absolute = path8.join(directory, name);
|
|
3094
3422
|
if (!isInside3(root, absolute) || relative.length > 1024) {
|
|
3095
3423
|
throw new Error(`OCI workspace snapshot contains an invalid path: ${relative}.`);
|
|
3096
3424
|
}
|
|
@@ -3098,7 +3426,7 @@ var snapshotMetadataFingerprint = async (root, maxWorkspaceBytes) => {
|
|
|
3098
3426
|
if (entries > MAX_SYNC_ENTRIES) {
|
|
3099
3427
|
throw new Error(`OCI workspace snapshot exceeds the ${MAX_SYNC_ENTRIES}-entry limit.`);
|
|
3100
3428
|
}
|
|
3101
|
-
const entry = await
|
|
3429
|
+
const entry = await lstat6(absolute);
|
|
3102
3430
|
if (entry.isSymbolicLink()) {
|
|
3103
3431
|
throw new Error(`OCI workspace snapshot contains a symbolic link: ${relative}.`);
|
|
3104
3432
|
}
|
|
@@ -3133,14 +3461,37 @@ var replaceSnapshot = async (snapshotRoot, stagedRoot) => {
|
|
|
3133
3461
|
}
|
|
3134
3462
|
await rm(backupRoot, { recursive: true, force: true });
|
|
3135
3463
|
};
|
|
3136
|
-
var
|
|
3464
|
+
var OCI_BACKGROUND_PROCESS_MARKER = "ZHIVEX_HARNESS_BACKGROUND_PROCESS";
|
|
3465
|
+
var OCI_COMMAND_TIMEOUT_MARKER = "ZHIVEX_HARNESS_COMMAND_TIMEOUT";
|
|
3466
|
+
var OCI_ATTESTATION_TIMEOUT_MS = 60000;
|
|
3467
|
+
var OCI_ATTESTATION_PROTOCOL_BYTES = 160;
|
|
3468
|
+
var OCI_ATTESTATION_PATTERN = /\nZHIVEX_HARNESS_ATTESTATION:(sha256:[a-f0-9]{64})\n$/;
|
|
3469
|
+
var OCI_COMMAND_RUNNER_SCRIPT = [
|
|
3137
3470
|
"import { createHash } from 'node:crypto';",
|
|
3471
|
+
"import { spawn } from 'node:child_process';",
|
|
3472
|
+
"import { readFileSync } from 'node:fs';",
|
|
3138
3473
|
"import { lstat, readFile, readdir, readlink, rm } from 'node:fs/promises';",
|
|
3139
3474
|
"import path from 'node:path';",
|
|
3475
|
+
"const request=JSON.parse(readFileSync(0,'utf8'));",
|
|
3476
|
+
"if(!Array.isArray(request.commands)||request.commands.length===0||!request.commands.every((command)=>Array.isArray(command)&&command.length>0&&command.every((value)=>typeof value==='string')))throw new Error('invalid OCI command batch');",
|
|
3477
|
+
"if(!Number.isSafeInteger(request.maxProcessRuntimeMs)||request.maxProcessRuntimeMs<1)throw new Error('invalid OCI command timeout');",
|
|
3478
|
+
"const deadline=Date.now()+request.maxProcessRuntimeMs;",
|
|
3479
|
+
"const run=async(command,timeoutMs)=>await new Promise((resolve,reject)=>{",
|
|
3480
|
+
"const child=spawn(command[0],command.slice(1),{stdio:['ignore','inherit','inherit'],detached:true});",
|
|
3481
|
+
"let settled=false;let timedOut=false;",
|
|
3482
|
+
"const finish=(value,error)=>{if(settled)return;settled=true;clearTimeout(timer);error?reject(error):resolve(value);};",
|
|
3483
|
+
"const timer=setTimeout(()=>{timedOut=true;try{process.kill(-child.pid,'SIGKILL');}catch{}},timeoutMs);",
|
|
3484
|
+
"child.once('error',(error)=>finish(undefined,error));",
|
|
3485
|
+
"child.once('exit',(code)=>finish({exitCode:timedOut?124:(code??137),timedOut}));",
|
|
3486
|
+
"});",
|
|
3487
|
+
"for(const command of request.commands){const remaining=deadline-Date.now();if(remaining<=0){console.error('ZHIVEX_HARNESS_COMMAND_TIMEOUT');process.exit(124);}const result=await run(command,remaining);if(result.timedOut){console.error('ZHIVEX_HARNESS_COMMAND_TIMEOUT');process.exit(124);}if(result.exitCode!==0)process.exit(result.exitCode);}",
|
|
3488
|
+
"const unexpectedProcesses=async()=>(await readdir('/proc')).filter((name)=>/^\\d+$/.test(name)).map(Number).filter((pid)=>pid!==1&&pid!==process.pid);",
|
|
3489
|
+
"if((await unexpectedProcesses()).length>0){console.error('ZHIVEX_HARNESS_BACKGROUND_PROCESS');process.exit(126);}",
|
|
3140
3490
|
"const root='/workspace';",
|
|
3141
3491
|
"const seal=createHash('sha256');",
|
|
3142
3492
|
"const modules=path.join(root,'node_modules');",
|
|
3143
|
-
"
|
|
3493
|
+
"try{",
|
|
3494
|
+
"if(request.hasDependencies===true){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});}",
|
|
3144
3495
|
"const digest=(value)=>`sha256:${createHash('sha256').update(value).digest('hex')}`;",
|
|
3145
3496
|
"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);",
|
|
3146
3497
|
"const visit=async(directory,base='')=>{",
|
|
@@ -3159,8 +3510,10 @@ var OCI_WORKSPACE_SEAL_SCRIPT = [
|
|
|
3159
3510
|
"}",
|
|
3160
3511
|
"};",
|
|
3161
3512
|
"await visit(root);",
|
|
3513
|
+
"if((await unexpectedProcesses()).length>0){console.error('ZHIVEX_HARNESS_BACKGROUND_PROCESS');process.exit(126);}",
|
|
3162
3514
|
"for(const name of await readdir('/tmp'))await rm(path.join('/tmp',name),{recursive:true,force:true});",
|
|
3163
|
-
"
|
|
3515
|
+
"process.stdout.write(`\\nZHIVEX_HARNESS_ATTESTATION:sha256:${seal.digest('hex')}\\n`);",
|
|
3516
|
+
"}catch(error){console.error(`ZHIVEX_HARNESS_ATTESTATION_FAILURE:${error instanceof Error?error.message:String(error)}`);process.exit(127);}"
|
|
3164
3517
|
].join("");
|
|
3165
3518
|
var OCI_SESSION_CONTROLLER_SCRIPT = "setInterval(()=>{},2147483647);";
|
|
3166
3519
|
|
|
@@ -3277,6 +3630,8 @@ ${result.stdout}`)) {
|
|
|
3277
3630
|
`${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`,
|
|
3278
3631
|
"--label",
|
|
3279
3632
|
`com.zhivex.harness.run=${runHash(request.runId)}`,
|
|
3633
|
+
"--label",
|
|
3634
|
+
`${HARNESS_OCI_OWNER_PID_LABEL}=${process.pid}`,
|
|
3280
3635
|
volumeName
|
|
3281
3636
|
];
|
|
3282
3637
|
const args = [
|
|
@@ -3311,15 +3666,16 @@ ${result.stdout}`)) {
|
|
|
3311
3666
|
"--env",
|
|
3312
3667
|
"TMPDIR=/tmp",
|
|
3313
3668
|
"--env",
|
|
3314
|
-
"
|
|
3669
|
+
"NPM_CONFIG_CACHE=/tmp/npm-cache",
|
|
3315
3670
|
"--env",
|
|
3316
3671
|
"CI=1",
|
|
3317
3672
|
...request.dependencyRoot ? ["--volume", `${request.dependencyRoot}:/dependencies:ro`] : [],
|
|
3318
3673
|
"--workdir",
|
|
3319
3674
|
"/workspace",
|
|
3320
3675
|
"--entrypoint",
|
|
3321
|
-
"
|
|
3676
|
+
"node",
|
|
3322
3677
|
request.imageId,
|
|
3678
|
+
"--input-type=module",
|
|
3323
3679
|
"-e",
|
|
3324
3680
|
OCI_SESSION_CONTROLLER_SCRIPT
|
|
3325
3681
|
];
|
|
@@ -3332,7 +3688,7 @@ ${result.stdout}`)) {
|
|
|
3332
3688
|
await this.cli([
|
|
3333
3689
|
"cp",
|
|
3334
3690
|
"-a",
|
|
3335
|
-
`${request.snapshotRoot}${
|
|
3691
|
+
`${request.snapshotRoot}${path8.sep}.`,
|
|
3336
3692
|
`${name}:/workspace`
|
|
3337
3693
|
], 60000);
|
|
3338
3694
|
if (request.dependencyRoot) {
|
|
@@ -3343,20 +3699,19 @@ ${result.stdout}`)) {
|
|
|
3343
3699
|
"--user",
|
|
3344
3700
|
`${uid}:${gid}`,
|
|
3345
3701
|
name,
|
|
3346
|
-
"
|
|
3702
|
+
"node",
|
|
3703
|
+
"--input-type=module",
|
|
3347
3704
|
"-e",
|
|
3348
3705
|
"import { symlink } from 'node:fs/promises'; await symlink('/dependencies', '/workspace/node_modules', 'dir')"
|
|
3349
3706
|
]);
|
|
3350
3707
|
}
|
|
3351
|
-
await this.cli(["pause", name]);
|
|
3352
3708
|
const session = {
|
|
3353
3709
|
key: runHash(request.runId),
|
|
3354
3710
|
requestFingerprint,
|
|
3355
3711
|
name,
|
|
3356
3712
|
volumeName,
|
|
3357
3713
|
snapshotSeal,
|
|
3358
|
-
hostFingerprint
|
|
3359
|
-
paused: true
|
|
3714
|
+
hostFingerprint
|
|
3360
3715
|
};
|
|
3361
3716
|
this.sessions.set(session.key, session);
|
|
3362
3717
|
return session;
|
|
@@ -3369,109 +3724,100 @@ ${result.stdout}`)) {
|
|
|
3369
3724
|
throw error;
|
|
3370
3725
|
}
|
|
3371
3726
|
}
|
|
3372
|
-
async onlyControllerProcessRemains(session) {
|
|
3373
|
-
const result = await this.cli(this.runtime === "podman" ? ["top", session.name, "pid"] : ["top", session.name, "-eo", "pid"]);
|
|
3374
|
-
const processIds = result.stdout.split(/\r?\n/).filter((line) => /^\s*\d+\s*$/.test(line));
|
|
3375
|
-
return processIds.length === 1;
|
|
3376
|
-
}
|
|
3377
|
-
async containerWorkspaceSeal(session, uid, gid, hasDependencies) {
|
|
3378
|
-
if (session.paused) {
|
|
3379
|
-
await this.cli(["unpause", session.name]);
|
|
3380
|
-
session.paused = false;
|
|
3381
|
-
}
|
|
3382
|
-
try {
|
|
3383
|
-
const result = await this.cli([
|
|
3384
|
-
"exec",
|
|
3385
|
-
"--workdir",
|
|
3386
|
-
"/workspace",
|
|
3387
|
-
"--user",
|
|
3388
|
-
`${uid}:${gid}`,
|
|
3389
|
-
"--env",
|
|
3390
|
-
`ZHIVEX_HARNESS_HAS_DEPENDENCIES=${hasDependencies ? "1" : "0"}`,
|
|
3391
|
-
session.name,
|
|
3392
|
-
"bun",
|
|
3393
|
-
"-e",
|
|
3394
|
-
OCI_WORKSPACE_SEAL_SCRIPT
|
|
3395
|
-
], 60000, 20000);
|
|
3396
|
-
const seal = result.stdout.trim();
|
|
3397
|
-
if (!/^sha256:[a-f0-9]{64}$/.test(seal)) {
|
|
3398
|
-
throw new Error("OCI workspace seal helper returned an invalid digest.");
|
|
3399
|
-
}
|
|
3400
|
-
return seal;
|
|
3401
|
-
} finally {
|
|
3402
|
-
if (!session.paused) {
|
|
3403
|
-
await this.cli(["pause", session.name]);
|
|
3404
|
-
session.paused = true;
|
|
3405
|
-
}
|
|
3406
|
-
}
|
|
3407
|
-
}
|
|
3408
3727
|
async runInSession(request, session, sessionReused) {
|
|
3728
|
+
const totalStartedAt = process.hrtime.bigint();
|
|
3409
3729
|
const uid = process.getuid?.() ?? 65532;
|
|
3410
3730
|
const gid = process.getgid?.() ?? 65532;
|
|
3411
|
-
if (session.paused) {
|
|
3412
|
-
await this.cli(["unpause", session.name]);
|
|
3413
|
-
session.paused = false;
|
|
3414
|
-
}
|
|
3415
3731
|
let forcedRemoval;
|
|
3416
3732
|
const forceRemove = () => {
|
|
3417
3733
|
forcedRemoval ??= this.forceRemove(session.name);
|
|
3418
3734
|
};
|
|
3735
|
+
const commandStartedAt = process.hrtime.bigint();
|
|
3419
3736
|
const result = await spawnBounded2([
|
|
3420
3737
|
this.runtime,
|
|
3421
3738
|
"exec",
|
|
3739
|
+
"-i",
|
|
3422
3740
|
"--workdir",
|
|
3423
3741
|
"/workspace",
|
|
3424
3742
|
"--user",
|
|
3425
3743
|
`${uid}:${gid}`,
|
|
3426
3744
|
session.name,
|
|
3427
|
-
|
|
3745
|
+
"node",
|
|
3746
|
+
"--input-type=module",
|
|
3747
|
+
"-e",
|
|
3748
|
+
OCI_COMMAND_RUNNER_SCRIPT
|
|
3428
3749
|
], {
|
|
3429
3750
|
env: this.hostEnvironment,
|
|
3430
|
-
timeoutMs: request.limits.maxProcessRuntimeMs,
|
|
3431
|
-
maxOutputBytes: request.limits.maxProcessOutputBytes,
|
|
3751
|
+
timeoutMs: request.limits.maxProcessRuntimeMs + OCI_ATTESTATION_TIMEOUT_MS,
|
|
3752
|
+
maxOutputBytes: request.limits.maxProcessOutputBytes + OCI_ATTESTATION_PROTOCOL_BYTES,
|
|
3753
|
+
input: JSON.stringify({
|
|
3754
|
+
commands: request.commands,
|
|
3755
|
+
maxProcessRuntimeMs: request.limits.maxProcessRuntimeMs,
|
|
3756
|
+
hasDependencies: Boolean(request.dependencyRoot)
|
|
3757
|
+
}),
|
|
3432
3758
|
...request.abortSignal ? { abortSignal: request.abortSignal } : {},
|
|
3433
3759
|
onStop: forceRemove
|
|
3434
3760
|
});
|
|
3435
|
-
|
|
3761
|
+
const commandAndAttestationMs = elapsedMilliseconds(commandStartedAt);
|
|
3762
|
+
const attestationMatch = result.exitCode === 0 ? result.stdout.match(OCI_ATTESTATION_PATTERN) : null;
|
|
3763
|
+
const commandResult = attestationMatch ? { ...result, stdout: result.stdout.slice(0, -attestationMatch[0].length) } : result;
|
|
3764
|
+
if (Buffer.byteLength(commandResult.stdout) + Buffer.byteLength(commandResult.stderr) > request.limits.maxProcessOutputBytes) {
|
|
3765
|
+
commandResult.outputLimitExceeded = true;
|
|
3766
|
+
}
|
|
3767
|
+
const command = request.commands.length === 1 ? request.commands[0] : ["<oci-batch>", String(request.commands.length)];
|
|
3768
|
+
const backgroundProcess = commandResult.exitCode === 126 && commandResult.stderr.includes(OCI_BACKGROUND_PROCESS_MARKER);
|
|
3769
|
+
const commandTimedOut = commandResult.exitCode === 124 && commandResult.stderr.includes(OCI_COMMAND_TIMEOUT_MARKER);
|
|
3770
|
+
const attestationFailure = commandResult.exitCode === 127 ? commandResult.stderr.match(/ZHIVEX_HARNESS_ATTESTATION_FAILURE:([^\r\n]*)/) : null;
|
|
3771
|
+
if (commandResult.exitCode !== 0 || commandResult.timedOut || commandResult.cancelled || commandResult.outputLimitExceeded) {
|
|
3436
3772
|
forceRemove();
|
|
3437
3773
|
await forcedRemoval;
|
|
3438
3774
|
await this.destroySession(session);
|
|
3775
|
+
if (attestationFailure) {
|
|
3776
|
+
throw new Error(`OCI workspace attestation failed: ${attestationFailure[1] ?? "unknown failure"}.`);
|
|
3777
|
+
}
|
|
3439
3778
|
return {
|
|
3440
|
-
...
|
|
3441
|
-
command
|
|
3779
|
+
...commandResult,
|
|
3780
|
+
command,
|
|
3781
|
+
commands: request.commands,
|
|
3782
|
+
...commandTimedOut ? { timedOut: true, exitCode: 124 } : {},
|
|
3783
|
+
...backgroundProcess ? {
|
|
3784
|
+
exitCode: 126,
|
|
3785
|
+
stderr: [
|
|
3786
|
+
commandResult.stderr.replace(OCI_BACKGROUND_PROCESS_MARKER, "").trim(),
|
|
3787
|
+
"OCI command left background processes running; its workspace changes were discarded."
|
|
3788
|
+
].filter(Boolean).join(`
|
|
3789
|
+
`)
|
|
3790
|
+
} : {},
|
|
3442
3791
|
sessionReused,
|
|
3443
3792
|
workspacePublished: false,
|
|
3444
|
-
workspaceExported: false
|
|
3793
|
+
workspaceExported: false,
|
|
3794
|
+
phaseLatencies: {
|
|
3795
|
+
commandAndAttestationMs,
|
|
3796
|
+
totalMs: elapsedMilliseconds(totalStartedAt)
|
|
3797
|
+
}
|
|
3445
3798
|
};
|
|
3446
3799
|
}
|
|
3447
3800
|
try {
|
|
3448
|
-
if (!
|
|
3449
|
-
|
|
3450
|
-
return {
|
|
3451
|
-
...result,
|
|
3452
|
-
command: request.command,
|
|
3453
|
-
exitCode: 126,
|
|
3454
|
-
stderr: [
|
|
3455
|
-
result.stderr,
|
|
3456
|
-
"OCI command left background processes running; its workspace changes were discarded."
|
|
3457
|
-
].filter(Boolean).join(`
|
|
3458
|
-
`),
|
|
3459
|
-
sessionReused,
|
|
3460
|
-
workspacePublished: false,
|
|
3461
|
-
workspaceExported: false
|
|
3462
|
-
};
|
|
3801
|
+
if (!attestationMatch) {
|
|
3802
|
+
throw new Error("OCI command attestation returned an invalid workspace seal.");
|
|
3463
3803
|
}
|
|
3464
|
-
const workspaceSeal =
|
|
3804
|
+
const workspaceSeal = attestationMatch[1];
|
|
3465
3805
|
if (workspaceSeal === session.snapshotSeal) {
|
|
3466
3806
|
return {
|
|
3467
|
-
...
|
|
3468
|
-
command
|
|
3807
|
+
...commandResult,
|
|
3808
|
+
command,
|
|
3809
|
+
commands: request.commands,
|
|
3469
3810
|
sessionReused,
|
|
3470
3811
|
workspacePublished: true,
|
|
3471
|
-
workspaceExported: false
|
|
3812
|
+
workspaceExported: false,
|
|
3813
|
+
phaseLatencies: {
|
|
3814
|
+
commandAndAttestationMs,
|
|
3815
|
+
totalMs: elapsedMilliseconds(totalStartedAt)
|
|
3816
|
+
}
|
|
3472
3817
|
};
|
|
3473
3818
|
}
|
|
3474
|
-
const
|
|
3819
|
+
const workspaceExportStartedAt = process.hrtime.bigint();
|
|
3820
|
+
const stagedRoot = path8.join(path8.dirname(request.snapshotRoot), `.workspace-sync-${randomUUID3()}`);
|
|
3475
3821
|
try {
|
|
3476
3822
|
await privateDirectory(stagedRoot);
|
|
3477
3823
|
await this.cli(["cp", `${session.name}:/workspace/.`, stagedRoot], 60000);
|
|
@@ -3483,37 +3829,62 @@ ${result.stdout}`)) {
|
|
|
3483
3829
|
await rm(stagedRoot, { recursive: true, force: true });
|
|
3484
3830
|
}
|
|
3485
3831
|
return {
|
|
3486
|
-
...
|
|
3487
|
-
command
|
|
3832
|
+
...commandResult,
|
|
3833
|
+
command,
|
|
3834
|
+
commands: request.commands,
|
|
3488
3835
|
sessionReused,
|
|
3489
3836
|
workspacePublished: true,
|
|
3490
|
-
workspaceExported: true
|
|
3837
|
+
workspaceExported: true,
|
|
3838
|
+
phaseLatencies: {
|
|
3839
|
+
commandAndAttestationMs,
|
|
3840
|
+
workspaceExportMs: elapsedMilliseconds(workspaceExportStartedAt),
|
|
3841
|
+
totalMs: elapsedMilliseconds(totalStartedAt)
|
|
3842
|
+
}
|
|
3491
3843
|
};
|
|
3492
3844
|
} catch (error) {
|
|
3493
3845
|
await this.destroySession(session);
|
|
3494
3846
|
throw error;
|
|
3495
3847
|
}
|
|
3496
3848
|
}
|
|
3497
|
-
async
|
|
3849
|
+
async runBatchLocked(request) {
|
|
3850
|
+
if (request.commands.length === 0 || request.commands.some((command) => command.length === 0)) {
|
|
3851
|
+
throw new Error("OCI command batch must contain at least one non-empty argv command.");
|
|
3852
|
+
}
|
|
3853
|
+
const totalStartedAt = process.hrtime.bigint();
|
|
3498
3854
|
const key = runHash(request.runId);
|
|
3499
3855
|
const requestFingerprint = this.requestFingerprint(request);
|
|
3500
3856
|
let session = this.sessions.get(key);
|
|
3501
3857
|
let sessionReused = Boolean(session);
|
|
3502
3858
|
let validated;
|
|
3859
|
+
let hostSynchronizationMs;
|
|
3860
|
+
let sessionCreationMs;
|
|
3503
3861
|
if (session) {
|
|
3862
|
+
const synchronizationStartedAt = process.hrtime.bigint();
|
|
3504
3863
|
const hostFingerprint = await snapshotMetadataFingerprint(request.snapshotRoot, request.limits.maxWorkspaceBytes);
|
|
3505
3864
|
if (session.requestFingerprint !== requestFingerprint || session.hostFingerprint !== hostFingerprint) {
|
|
3506
3865
|
await this.destroySession(session);
|
|
3507
3866
|
session = undefined;
|
|
3508
3867
|
sessionReused = false;
|
|
3509
3868
|
}
|
|
3869
|
+
hostSynchronizationMs = elapsedMilliseconds(synchronizationStartedAt);
|
|
3510
3870
|
}
|
|
3511
3871
|
if (!session) {
|
|
3872
|
+
const creationStartedAt = process.hrtime.bigint();
|
|
3512
3873
|
validated = await validateSynchronizedSnapshot(request.snapshotRoot, request.snapshotRoot, request.limits.maxWorkspaceBytes, request.limits.maxFileWriteBytes);
|
|
3513
3874
|
session = await this.createSession(request, requestFingerprint, validated.seal, validated.metadataFingerprint);
|
|
3875
|
+
sessionCreationMs = elapsedMilliseconds(creationStartedAt);
|
|
3514
3876
|
}
|
|
3515
3877
|
try {
|
|
3516
|
-
|
|
3878
|
+
const result = await this.runInSession(request, session, sessionReused);
|
|
3879
|
+
return {
|
|
3880
|
+
...result,
|
|
3881
|
+
phaseLatencies: {
|
|
3882
|
+
...result.phaseLatencies,
|
|
3883
|
+
...hostSynchronizationMs !== undefined ? { hostSynchronizationMs } : {},
|
|
3884
|
+
...sessionCreationMs !== undefined ? { sessionCreationMs } : {},
|
|
3885
|
+
totalMs: elapsedMilliseconds(totalStartedAt)
|
|
3886
|
+
}
|
|
3887
|
+
};
|
|
3517
3888
|
} catch (error) {
|
|
3518
3889
|
if (this.sessions.get(key) === session)
|
|
3519
3890
|
await this.destroySession(session).catch(() => {
|
|
@@ -3523,7 +3894,12 @@ ${result.stdout}`)) {
|
|
|
3523
3894
|
}
|
|
3524
3895
|
}
|
|
3525
3896
|
async run(request) {
|
|
3526
|
-
|
|
3897
|
+
const { command, ...shared } = request;
|
|
3898
|
+
const result = await this.runBatch({ ...shared, commands: [command] });
|
|
3899
|
+
return { ...result, command };
|
|
3900
|
+
}
|
|
3901
|
+
async runBatch(request) {
|
|
3902
|
+
return this.withRunLock(request.runId, () => this.runBatchLocked(request));
|
|
3527
3903
|
}
|
|
3528
3904
|
async removeByFilters(filters) {
|
|
3529
3905
|
const listed = await spawnBounded2([this.runtime, "ps", "-aq", ...filters.flatMap((filter) => ["--filter", filter])], {
|
|
@@ -3569,6 +3945,39 @@ ${result.stdout}`)) {
|
|
|
3569
3945
|
}
|
|
3570
3946
|
return removed;
|
|
3571
3947
|
}
|
|
3948
|
+
async removeRunningOrphans() {
|
|
3949
|
+
const listed = await spawnBounded2([
|
|
3950
|
+
this.runtime,
|
|
3951
|
+
"ps",
|
|
3952
|
+
"-q",
|
|
3953
|
+
"--filter",
|
|
3954
|
+
`label=${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`,
|
|
3955
|
+
"--filter",
|
|
3956
|
+
"status=running"
|
|
3957
|
+
], {
|
|
3958
|
+
env: this.hostEnvironment,
|
|
3959
|
+
timeoutMs: 30000,
|
|
3960
|
+
maxOutputBytes: 1024 * 1024
|
|
3961
|
+
});
|
|
3962
|
+
if (listed.exitCode !== 0)
|
|
3963
|
+
return 0;
|
|
3964
|
+
let removed = 0;
|
|
3965
|
+
for (const id of validContainerIds(listed.stdout)) {
|
|
3966
|
+
const inspected = await this.cli([
|
|
3967
|
+
"inspect",
|
|
3968
|
+
id,
|
|
3969
|
+
"--format",
|
|
3970
|
+
"{{json .Config.Labels}}"
|
|
3971
|
+
], 30000, 20000);
|
|
3972
|
+
const labels = JSON.parse(inspected.stdout);
|
|
3973
|
+
const ownerPid = Number(labels[HARNESS_OCI_OWNER_PID_LABEL]);
|
|
3974
|
+
if (hostProcessIsAlive(ownerPid))
|
|
3975
|
+
continue;
|
|
3976
|
+
await this.forceRemove(id);
|
|
3977
|
+
removed += 1;
|
|
3978
|
+
}
|
|
3979
|
+
return removed;
|
|
3980
|
+
}
|
|
3572
3981
|
async removeRunContainers(runId) {
|
|
3573
3982
|
const key = runHash(runId);
|
|
3574
3983
|
const session = this.sessions.get(key);
|
|
@@ -3586,14 +3995,19 @@ ${result.stdout}`)) {
|
|
|
3586
3995
|
return cachedResources + containers + volumes;
|
|
3587
3996
|
}
|
|
3588
3997
|
async cleanupOrphans() {
|
|
3589
|
-
this.sessions.clear();
|
|
3590
3998
|
let removed = 0;
|
|
3999
|
+
for (const session of [...this.sessions.values()]) {
|
|
4000
|
+
await this.destroySession(session);
|
|
4001
|
+
removed += 2;
|
|
4002
|
+
}
|
|
4003
|
+
this.sessions.clear();
|
|
3591
4004
|
for (const status of ["created", "exited", "dead", "paused"]) {
|
|
3592
4005
|
removed += await this.removeByFilters([
|
|
3593
4006
|
`label=${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`,
|
|
3594
4007
|
`status=${status}`
|
|
3595
4008
|
]);
|
|
3596
4009
|
}
|
|
4010
|
+
removed += await this.removeRunningOrphans();
|
|
3597
4011
|
removed += await this.removeVolumesByFilters([
|
|
3598
4012
|
`label=${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`
|
|
3599
4013
|
]);
|
|
@@ -3615,10 +4029,10 @@ var collectSnapshotInventory = async (workspace, metrics) => {
|
|
|
3615
4029
|
if (metrics)
|
|
3616
4030
|
metrics.inventoryPages += 1;
|
|
3617
4031
|
for (const file of page.files) {
|
|
3618
|
-
const absolute =
|
|
4032
|
+
const absolute = path8.join(workspace.root, ...file.path.split("/"));
|
|
3619
4033
|
if (!isInside3(workspace.root, absolute))
|
|
3620
4034
|
throw new Error(`Snapshot file escaped its workspace: ${file.path}.`);
|
|
3621
|
-
const entry = await
|
|
4035
|
+
const entry = await lstat6(absolute);
|
|
3622
4036
|
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
3623
4037
|
throw new Error(`Snapshot source is not a regular file: ${file.path}.`);
|
|
3624
4038
|
}
|
|
@@ -3637,16 +4051,16 @@ var collectSnapshotInventory = async (workspace, metrics) => {
|
|
|
3637
4051
|
return files;
|
|
3638
4052
|
};
|
|
3639
4053
|
var readSnapshotFile = async (workspace, expected, metrics) => {
|
|
3640
|
-
const absolute =
|
|
4054
|
+
const absolute = path8.join(workspace.root, ...expected.path.split("/"));
|
|
3641
4055
|
if (!isInside3(workspace.root, absolute)) {
|
|
3642
4056
|
throw new Error(`Snapshot file escaped its workspace: ${expected.path}.`);
|
|
3643
4057
|
}
|
|
3644
|
-
const before = await
|
|
4058
|
+
const before = await lstat6(absolute);
|
|
3645
4059
|
if (before.isSymbolicLink() || !before.isFile()) {
|
|
3646
4060
|
throw new Error(`Snapshot source is not a regular file: ${expected.path}.`);
|
|
3647
4061
|
}
|
|
3648
4062
|
const contents = await readFile3(absolute);
|
|
3649
|
-
const after = await
|
|
4063
|
+
const after = await lstat6(absolute);
|
|
3650
4064
|
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || (before.mode & 511) !== (after.mode & 511)) {
|
|
3651
4065
|
throw new Error(`Snapshot source changed while reading: ${expected.path}.`);
|
|
3652
4066
|
}
|
|
@@ -3684,11 +4098,11 @@ var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceB
|
|
|
3684
4098
|
}
|
|
3685
4099
|
for (const expected of files.values()) {
|
|
3686
4100
|
const file = await readSnapshotFile(source, expected, metrics);
|
|
3687
|
-
const baseTarget =
|
|
3688
|
-
const snapshotTarget =
|
|
4101
|
+
const baseTarget = path8.join(baseRoot, ...file.path.split("/"));
|
|
4102
|
+
const snapshotTarget = path8.join(snapshotRoot, ...file.path.split("/"));
|
|
3689
4103
|
await Promise.all([
|
|
3690
|
-
mkdir3(
|
|
3691
|
-
mkdir3(
|
|
4104
|
+
mkdir3(path8.dirname(baseTarget), { recursive: true, mode: 448 }),
|
|
4105
|
+
mkdir3(path8.dirname(snapshotTarget), { recursive: true, mode: 448 })
|
|
3692
4106
|
]);
|
|
3693
4107
|
await writeFile(baseTarget, file.contents, { mode: file.mode });
|
|
3694
4108
|
await chmod2(baseTarget, file.mode);
|
|
@@ -3911,7 +4325,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3911
4325
|
}
|
|
3912
4326
|
};
|
|
3913
4327
|
const binding = createAgentExecutionEnvironmentBinding(manifest);
|
|
3914
|
-
const environmentRoot =
|
|
4328
|
+
const environmentRoot = path8.join(options.stateDirectory, "environments");
|
|
3915
4329
|
return {
|
|
3916
4330
|
manifest,
|
|
3917
4331
|
image,
|
|
@@ -3931,14 +4345,14 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3931
4345
|
workspaceExports: 0
|
|
3932
4346
|
};
|
|
3933
4347
|
await privateDirectory(environmentRoot);
|
|
3934
|
-
const directory =
|
|
4348
|
+
const directory = path8.join(environmentRoot, runHash(request.runId));
|
|
3935
4349
|
await privateDirectory(directory);
|
|
3936
|
-
const metadataPath =
|
|
3937
|
-
const baseRoot =
|
|
3938
|
-
const snapshotRoot =
|
|
4350
|
+
const metadataPath = path8.join(directory, "environment.json");
|
|
4351
|
+
const baseRoot = path8.join(directory, "base");
|
|
4352
|
+
const snapshotRoot = path8.join(directory, "workspace");
|
|
3939
4353
|
let metadata;
|
|
3940
4354
|
try {
|
|
3941
|
-
const entry = await
|
|
4355
|
+
const entry = await lstat6(metadataPath);
|
|
3942
4356
|
if (entry.isSymbolicLink() || !entry.isFile())
|
|
3943
4357
|
throw new Error("Execution metadata must be a regular file.");
|
|
3944
4358
|
metadata = JSON.parse(await readFile3(metadataPath, "utf8"));
|
|
@@ -3967,10 +4381,10 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3967
4381
|
await atomicJson(metadataPath, metadata);
|
|
3968
4382
|
const workspace = await Workspace.open(snapshotRoot);
|
|
3969
4383
|
const base = await Workspace.open(baseRoot);
|
|
3970
|
-
const dependencyPath =
|
|
4384
|
+
const dependencyPath = path8.join(options.workspace.root, "node_modules");
|
|
3971
4385
|
let dependencyRoot;
|
|
3972
4386
|
try {
|
|
3973
|
-
const entry = await
|
|
4387
|
+
const entry = await lstat6(dependencyPath);
|
|
3974
4388
|
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
3975
4389
|
throw new Error("node_modules must be a real directory before it can be mounted read-only.");
|
|
3976
4390
|
}
|
|
@@ -3980,31 +4394,33 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3980
4394
|
throw error;
|
|
3981
4395
|
}
|
|
3982
4396
|
}
|
|
3983
|
-
const
|
|
4397
|
+
const validateCommand = (command, args) => {
|
|
3984
4398
|
if (!options.config.allowedCommands.includes(command)) {
|
|
3985
4399
|
throw new Error(`The OCI command "${command}" is not in the explicit allowlist.`);
|
|
3986
4400
|
}
|
|
3987
4401
|
if (args.length > 256 || args.some((value) => value.length > 8192 || value.includes("\x00"))) {
|
|
3988
4402
|
throw new Error("OCI command arguments exceed the bounded argument contract.");
|
|
3989
4403
|
}
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
}
|
|
4404
|
+
return [command, ...args];
|
|
4405
|
+
};
|
|
4406
|
+
const sharedRunRequest = (context) => ({
|
|
4407
|
+
runId: request.runId,
|
|
4408
|
+
snapshotRoot,
|
|
4409
|
+
...dependencyRoot ? { dependencyRoot } : {},
|
|
4410
|
+
imageId: image.imageId,
|
|
4411
|
+
limits: {
|
|
4412
|
+
maxProcessRuntimeMs: options.config.maxProcessRuntimeMs,
|
|
4413
|
+
maxProcessOutputBytes: options.config.maxProcessOutputBytes,
|
|
4414
|
+
maxMemoryMb: options.config.maxMemoryMb,
|
|
4415
|
+
maxPids: options.config.maxPids,
|
|
4416
|
+
maxCpus: options.config.maxCpus,
|
|
4417
|
+
maxWorkspaceBytes: options.config.maxWorkspaceBytes,
|
|
4418
|
+
maxFileWriteBytes: options.config.maxFileWriteBytes,
|
|
4419
|
+
tmpfsMb: options.config.tmpfsMb
|
|
4420
|
+
},
|
|
4421
|
+
...context?.abortSignal ?? request.abortSignal ? { abortSignal: context?.abortSignal ?? request.abortSignal } : {}
|
|
4422
|
+
});
|
|
4423
|
+
const normalizeResult = (result) => {
|
|
4008
4424
|
if (result.sessionReused === false)
|
|
4009
4425
|
ioMetrics.containerStarts += 1;
|
|
4010
4426
|
if (result.sessionReused === true)
|
|
@@ -4025,7 +4441,45 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
4025
4441
|
...result.outputLimitExceeded ? ["OCI command exceeded the output limit."] : []
|
|
4026
4442
|
].filter(Boolean).join(`
|
|
4027
4443
|
`),
|
|
4028
|
-
timedOut: result.timedOut
|
|
4444
|
+
timedOut: result.timedOut,
|
|
4445
|
+
...result.phaseLatencies ? { phaseLatencies: result.phaseLatencies } : {},
|
|
4446
|
+
...result.sessionReused !== undefined ? { sessionReused: result.sessionReused } : {},
|
|
4447
|
+
...result.workspacePublished !== undefined ? { workspacePublished: result.workspacePublished } : {},
|
|
4448
|
+
...result.workspaceExported !== undefined ? { workspaceExported: result.workspaceExported } : {}
|
|
4449
|
+
};
|
|
4450
|
+
};
|
|
4451
|
+
const runCommand2 = async (command, args, context) => normalizeResult(await runtime.run({
|
|
4452
|
+
...sharedRunRequest(context),
|
|
4453
|
+
command: validateCommand(command, args)
|
|
4454
|
+
}));
|
|
4455
|
+
const runCommandBatch = async (commands, context) => {
|
|
4456
|
+
if (commands.length === 0 || commands.length > 32) {
|
|
4457
|
+
throw new Error("OCI command batch must contain between 1 and 32 commands.");
|
|
4458
|
+
}
|
|
4459
|
+
if (commands.reduce((total, item) => total + item.args.length, 0) > 256) {
|
|
4460
|
+
throw new Error("OCI command batch exceeds the bounded argument-count contract.");
|
|
4461
|
+
}
|
|
4462
|
+
const argvCommands = commands.map(({ command, args }) => validateCommand(command, args));
|
|
4463
|
+
if (runtime.runBatch) {
|
|
4464
|
+
return normalizeResult(await runtime.runBatch({
|
|
4465
|
+
...sharedRunRequest(context),
|
|
4466
|
+
commands: argvCommands
|
|
4467
|
+
}));
|
|
4468
|
+
}
|
|
4469
|
+
const results = [];
|
|
4470
|
+
for (const argv of argvCommands) {
|
|
4471
|
+
const result = await runCommand2(argv[0], argv.slice(1), context);
|
|
4472
|
+
results.push(result);
|
|
4473
|
+
if (result.exitCode !== 0)
|
|
4474
|
+
break;
|
|
4475
|
+
}
|
|
4476
|
+
return {
|
|
4477
|
+
command: ["<oci-batch>", String(commands.length)],
|
|
4478
|
+
exitCode: results.at(-1)?.exitCode ?? 1,
|
|
4479
|
+
stdout: results.map((result) => result.stdout).filter(Boolean).join(""),
|
|
4480
|
+
stderr: results.map((result) => result.stderr).filter(Boolean).join(`
|
|
4481
|
+
`),
|
|
4482
|
+
timedOut: results.some((result) => result.timedOut)
|
|
4029
4483
|
};
|
|
4030
4484
|
};
|
|
4031
4485
|
const session = {
|
|
@@ -4045,6 +4499,22 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
4045
4499
|
return { decision: "deny", reason: "The requested OCI executable is not allowlisted." };
|
|
4046
4500
|
}
|
|
4047
4501
|
}
|
|
4502
|
+
if (name === "verify_and_apply_environment_patch" || name === "verify_and_apply_reviewed_edits") {
|
|
4503
|
+
const input = authorization.input;
|
|
4504
|
+
const validBoundInput = name === "verify_and_apply_environment_patch" ? typeof input.patchId === "string" : Array.isArray(input.changes) && input.changes.length > 0;
|
|
4505
|
+
if (!validBoundInput || typeof input.command !== "string" || !options.config.allowedCommands.includes(input.command) || !Array.isArray(input.args)) {
|
|
4506
|
+
return { decision: "deny", reason: "The verified OCI transaction is invalid or contains a non-allowlisted executable." };
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
if (name === "run_environment_batch") {
|
|
4510
|
+
const input = authorization.input;
|
|
4511
|
+
if (!Array.isArray(input.commands) || input.commands.length < 1 || input.commands.length > 32 || input.commands.some((item) => {
|
|
4512
|
+
const candidate = item;
|
|
4513
|
+
return typeof candidate.command !== "string" || !options.config.allowedCommands.includes(candidate.command) || !Array.isArray(candidate.args);
|
|
4514
|
+
})) {
|
|
4515
|
+
return { decision: "deny", reason: "The requested OCI command batch is invalid or contains a non-allowlisted executable." };
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4048
4518
|
return {
|
|
4049
4519
|
decision: "allow",
|
|
4050
4520
|
metadata: {
|
|
@@ -4077,26 +4547,20 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
4077
4547
|
io: { ...ioMetrics }
|
|
4078
4548
|
};
|
|
4079
4549
|
},
|
|
4080
|
-
runCommand,
|
|
4550
|
+
runCommand: runCommand2,
|
|
4551
|
+
runCommandBatch,
|
|
4081
4552
|
async runCheck(check, expectedScript, allowedChecks, context) {
|
|
4082
|
-
if (!/^[A-Za-z0-9:_-]+$/.test(check) || !allowedChecks.includes(check)) {
|
|
4083
|
-
throw new Error(`The check "${check}" is not in the explicit allowlist.`);
|
|
4084
|
-
}
|
|
4085
4553
|
let manifestDocument;
|
|
4086
4554
|
try {
|
|
4087
|
-
manifestDocument = JSON.parse(await readFile3(
|
|
4555
|
+
manifestDocument = JSON.parse(await readFile3(path8.join(snapshotRoot, "package.json"), "utf8"));
|
|
4088
4556
|
} catch (error) {
|
|
4089
4557
|
if (error.code === "ENOENT") {
|
|
4090
4558
|
throw new Error("The environment snapshot does not contain package.json.");
|
|
4091
4559
|
}
|
|
4092
4560
|
throw error;
|
|
4093
4561
|
}
|
|
4094
|
-
const
|
|
4095
|
-
|
|
4096
|
-
throw new Error(`package.json does not define the "${check}" script.`);
|
|
4097
|
-
if (actual !== expectedScript)
|
|
4098
|
-
throw new Error(`The "${check}" script changed or does not match expectedScript.`);
|
|
4099
|
-
return runCommand("bun", ["--no-env-file", "run", check], context);
|
|
4562
|
+
const resolved = await resolvePackageCheckCommand(snapshotRoot, manifestDocument, check, expectedScript, allowedChecks);
|
|
4563
|
+
return runCommand2(resolved.command[0], resolved.command.slice(1), context);
|
|
4100
4564
|
},
|
|
4101
4565
|
async inspectPatch() {
|
|
4102
4566
|
const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes, ioMetrics);
|
|
@@ -4122,7 +4586,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
4122
4586
|
};
|
|
4123
4587
|
};
|
|
4124
4588
|
var cleanupHarnessExecutionArtifacts = async (stateDirectory, before) => {
|
|
4125
|
-
const root =
|
|
4589
|
+
const root = path8.join(stateDirectory, "environments");
|
|
4126
4590
|
const result = { scanned: 0, deleted: 0, skipped: 0 };
|
|
4127
4591
|
let entries;
|
|
4128
4592
|
try {
|
|
@@ -4138,15 +4602,15 @@ var cleanupHarnessExecutionArtifacts = async (stateDirectory, before) => {
|
|
|
4138
4602
|
continue;
|
|
4139
4603
|
}
|
|
4140
4604
|
result.scanned += 1;
|
|
4141
|
-
const directory =
|
|
4605
|
+
const directory = path8.join(root, entry.name);
|
|
4142
4606
|
const resolved = await realpath3(directory);
|
|
4143
4607
|
if (!isInside3(root, resolved)) {
|
|
4144
4608
|
result.skipped += 1;
|
|
4145
4609
|
continue;
|
|
4146
4610
|
}
|
|
4147
4611
|
try {
|
|
4148
|
-
const metadataPath =
|
|
4149
|
-
const metadataEntry = await
|
|
4612
|
+
const metadataPath = path8.join(directory, "environment.json");
|
|
4613
|
+
const metadataEntry = await lstat6(metadataPath);
|
|
4150
4614
|
if (metadataEntry.isSymbolicLink() || !metadataEntry.isFile())
|
|
4151
4615
|
throw new Error("unsafe metadata");
|
|
4152
4616
|
const metadata = JSON.parse(await readFile3(metadataPath, "utf8"));
|
|
@@ -4172,7 +4636,7 @@ var describeOciCommand = (command, args) => commandDisplay([command, ...args]);
|
|
|
4172
4636
|
|
|
4173
4637
|
// src/harness.ts
|
|
4174
4638
|
var APPROVAL_VERSION = "2026-08-17-v5";
|
|
4175
|
-
var TOOL_CONTRACT_VERSION = "workspace-
|
|
4639
|
+
var TOOL_CONTRACT_VERSION = "workspace-verified-transaction-v3";
|
|
4176
4640
|
var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, providerTransportFingerprint, executionEnvironment) => ({
|
|
4177
4641
|
schemaVersion: 1,
|
|
4178
4642
|
id: "zhivex-harness",
|
|
@@ -4202,14 +4666,17 @@ var HARNESS_INSTRUCTIONS = `You are Zhivex Harness, a provider-portable coding a
|
|
|
4202
4666
|
|
|
4203
4667
|
Rules:
|
|
4204
4668
|
- Match the user's language.
|
|
4205
|
-
- Inspect the repository before proposing changes.
|
|
4669
|
+
- Inspect the repository before proposing changes. Use list_files with includeDigests=false for fast topology discovery, then prefer search_many and read_files for independent lookups. Omit cursor on the first paginated call and only reuse an exact nextCursor returned by the immediately preceding matching request. Read the exact digest before proposing any edit.
|
|
4206
4670
|
- Use only workspace-relative paths. Never request or expose secrets.
|
|
4207
4671
|
- Make the smallest coherent change that fully addresses the task.
|
|
4208
4672
|
- For every file edit, first read its digest and call propose_edits. Apply exactly that reviewed proposal with apply_patch.
|
|
4209
4673
|
- apply_patch, move_file, quarantine_file, restore_file, and run_check require explicit approval from the operator.
|
|
4674
|
+
- apply_reviewed_edits is the one-step equivalent of propose_edits plus apply_patch: its complete digest-bound changes are the approval payload and are applied atomically only after approval.
|
|
4675
|
+
- verify_and_apply_reviewed_edits requests one approval for complete digest-bound edits plus exact verifier argv, applies them to a clean OCI snapshot, rejects verifier-created drift, and imports only after exit code 0.
|
|
4210
4676
|
- 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.
|
|
4211
4677
|
- 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.
|
|
4212
|
-
- 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.
|
|
4678
|
+
- run_environment_command executes one allowlisted argv command without a host shell. Prefer run_environment_batch for a reviewed sequence that can share one final workspace attestation. Network, privileges, resources, environment variables, and output are bounded by the OCI policy.
|
|
4679
|
+
- verify_and_apply_environment_patch binds one reviewed patch and verifier argv, runs that verifier inside OCI, rejects any verifier-created patch drift, and imports only after exit code 0.
|
|
4213
4680
|
- Never overwrite stale content. If an expected digest no longer matches, inspect the file again and create a new proposal.
|
|
4214
4681
|
- Deletions are recoverable: use quarantine_file, never permanent deletion. Use restore_file to recover quarantined content.
|
|
4215
4682
|
- Never claim a check passed unless run_check returned exitCode 0.
|
|
@@ -4248,20 +4715,29 @@ var mutationApproval = {
|
|
|
4248
4715
|
approvalVersion: APPROVAL_VERSION,
|
|
4249
4716
|
metadata: toolMetadata(["filesystem", "write"], "high")
|
|
4250
4717
|
};
|
|
4718
|
+
var verifierCommandSchema = z3.strictObject({
|
|
4719
|
+
command: z3.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._+-]*$/),
|
|
4720
|
+
args: z3.array(z3.string().max(8192)).max(256).default([])
|
|
4721
|
+
});
|
|
4722
|
+
var verifiedReviewedEditsInputSchema = editProposalInputSchema.extend({
|
|
4723
|
+
command: verifierCommandSchema.shape.command,
|
|
4724
|
+
args: verifierCommandSchema.shape.args
|
|
4725
|
+
});
|
|
4251
4726
|
var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
4252
4727
|
list_files: tool({
|
|
4253
4728
|
name: "list_files",
|
|
4254
|
-
description: "List regular files
|
|
4729
|
+
description: "List regular files using a stable cursor. Omit cursor on the first call; on later pages pass only the exact nextCursor returned by the preceding matching request. Set includeDigests=false for fast path-only topology discovery; keep it true when size and content digests are required. Build artifacts, dependencies, Git internals, and harness state are ignored.",
|
|
4255
4730
|
schema: z3.object({
|
|
4256
4731
|
path: z3.string().min(1).default("."),
|
|
4257
4732
|
limit: z3.number().int().min(1).max(500).default(200),
|
|
4258
|
-
|
|
4733
|
+
includeDigests: z3.boolean().default(true),
|
|
4734
|
+
cursor: z3.string().min(1).max(2000).optional().describe("Omit on the first page. For a later page, pass only the exact nextCursor returned by the preceding matching list_files result.")
|
|
4259
4735
|
}),
|
|
4260
4736
|
metadata: readOnlyMetadata,
|
|
4261
|
-
execute: async ({ path:
|
|
4262
|
-
|
|
4263
|
-
...cursor ? { cursor } : {}
|
|
4264
|
-
}
|
|
4737
|
+
execute: async ({ path: path9, limit, includeDigests, cursor }, context) => {
|
|
4738
|
+
const selectedWorkspace = harnessExecutionSession(context)?.workspace ?? workspace;
|
|
4739
|
+
return serializeJsonValue2(await (includeDigests ? selectedWorkspace.listFiles(path9, { limit, includeDigests: true, ...cursor ? { cursor } : {} }) : selectedWorkspace.listFiles(path9, { limit, includeDigests: false, ...cursor ? { cursor } : {} })));
|
|
4740
|
+
}
|
|
4265
4741
|
}),
|
|
4266
4742
|
read_file: tool({
|
|
4267
4743
|
name: "read_file",
|
|
@@ -4272,7 +4748,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
4272
4748
|
endLine: z3.number().int().min(1).optional()
|
|
4273
4749
|
}),
|
|
4274
4750
|
metadata: readOnlyMetadata,
|
|
4275
|
-
execute: async ({ path:
|
|
4751
|
+
execute: async ({ path: path9, startLine, endLine }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).readFile(path9, startLine, endLine))
|
|
4276
4752
|
}),
|
|
4277
4753
|
read_files: tool({
|
|
4278
4754
|
name: "read_files",
|
|
@@ -4285,8 +4761,8 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
4285
4761
|
})).min(1).max(20)
|
|
4286
4762
|
}),
|
|
4287
4763
|
metadata: readOnlyMetadata,
|
|
4288
|
-
execute: async ({ files }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).readFiles(files.map(({ path:
|
|
4289
|
-
path:
|
|
4764
|
+
execute: async ({ files }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).readFiles(files.map(({ path: path9, startLine, endLine }) => ({
|
|
4765
|
+
path: path9,
|
|
4290
4766
|
startLine,
|
|
4291
4767
|
...endLine !== undefined ? { endLine } : {}
|
|
4292
4768
|
}))))
|
|
@@ -4302,7 +4778,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
4302
4778
|
cursor: z3.string().min(1).max(2000).optional()
|
|
4303
4779
|
}),
|
|
4304
4780
|
metadata: readOnlyMetadata,
|
|
4305
|
-
execute: async ({ query, path:
|
|
4781
|
+
execute: async ({ query, path: path9, caseSensitive, limit, cursor }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).searchFiles(query, path9, {
|
|
4306
4782
|
caseSensitive,
|
|
4307
4783
|
limit,
|
|
4308
4784
|
...cursor ? { cursor } : {}
|
|
@@ -4328,7 +4804,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
4328
4804
|
}
|
|
4329
4805
|
}),
|
|
4330
4806
|
metadata: readOnlyMetadata,
|
|
4331
|
-
execute: async ({ queries, path:
|
|
4807
|
+
execute: async ({ queries, path: path9, limitPerQuery }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).searchMany(queries, path9, { limitPerQuery }))
|
|
4332
4808
|
}),
|
|
4333
4809
|
propose_edits: tool({
|
|
4334
4810
|
name: "propose_edits",
|
|
@@ -4350,6 +4826,19 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
4350
4826
|
return serializeJsonValue2(editContractDocument("patch-result", await (harnessExecutionSession(context)?.workspace ?? workspace).applyPatch(proposal)));
|
|
4351
4827
|
}
|
|
4352
4828
|
}),
|
|
4829
|
+
apply_reviewed_edits: tool({
|
|
4830
|
+
name: "apply_reviewed_edits",
|
|
4831
|
+
description: "Request approval for complete digest-bound changes and atomically apply them without copying a separate proposalId between model turns. The exact paths, expected digests, and contents are the approval payload.",
|
|
4832
|
+
schema: editProposalInputSchema,
|
|
4833
|
+
...mutationApproval,
|
|
4834
|
+
execute: async ({ changes }, context) => {
|
|
4835
|
+
const proposal = createEditProposal({ changes });
|
|
4836
|
+
return serializeJsonValue2(editContractDocument("patch-result", await (harnessExecutionSession(context)?.workspace ?? workspace).applyPatch({
|
|
4837
|
+
proposalId: proposal.proposalId,
|
|
4838
|
+
changes
|
|
4839
|
+
})));
|
|
4840
|
+
}
|
|
4841
|
+
}),
|
|
4353
4842
|
move_file: tool({
|
|
4354
4843
|
name: "move_file",
|
|
4355
4844
|
description: "Move one regular file without overwriting the destination. The source must still match expectedDigest.",
|
|
@@ -4373,7 +4862,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
4373
4862
|
}),
|
|
4374
4863
|
run_check: tool({
|
|
4375
4864
|
name: "run_check",
|
|
4376
|
-
description: `Run one explicitly allowed package.json script through
|
|
4865
|
+
description: `Run one explicitly allowed package.json script through the repository package manager (${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 implicit lifecycle hook is exposed.`,
|
|
4377
4866
|
schema: z3.object({
|
|
4378
4867
|
check: z3.string().min(1).max(100).regex(/^[A-Za-z0-9:_-]+$/),
|
|
4379
4868
|
expectedScript: z3.string().min(1).max(2000)
|
|
@@ -4425,6 +4914,29 @@ var createExecutionEnvironmentTools = (workspace) => ({
|
|
|
4425
4914
|
metadata: toolMetadata(["code-execution", "filesystem"], "high"),
|
|
4426
4915
|
execute: async ({ command, args }, context) => serializeJsonValue2(await requireExecutionSession(context).runCommand(command, args, context))
|
|
4427
4916
|
}),
|
|
4917
|
+
run_environment_batch: tool({
|
|
4918
|
+
name: "run_environment_batch",
|
|
4919
|
+
description: "Run 1 to 32 reviewed allowlisted argv commands sequentially inside one enforced OCI cycle. Execution stops on the first failure and the workspace is attested and published only after the batch succeeds; no host shell or network is exposed.",
|
|
4920
|
+
schema: z3.strictObject({
|
|
4921
|
+
commands: z3.array(z3.strictObject({
|
|
4922
|
+
command: z3.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._+-]*$/),
|
|
4923
|
+
args: z3.array(z3.string().max(8192)).max(256).default([])
|
|
4924
|
+
})).min(1).max(32)
|
|
4925
|
+
}).superRefine((input, context) => {
|
|
4926
|
+
if (input.commands.reduce((total, command) => total + command.args.length, 0) > 256) {
|
|
4927
|
+
context.addIssue({
|
|
4928
|
+
code: "custom",
|
|
4929
|
+
path: ["commands"],
|
|
4930
|
+
message: "run_environment_batch allows at most 256 aggregate arguments."
|
|
4931
|
+
});
|
|
4932
|
+
}
|
|
4933
|
+
}),
|
|
4934
|
+
requiresApproval: true,
|
|
4935
|
+
approvalMode: "interrupt",
|
|
4936
|
+
approvalVersion: APPROVAL_VERSION,
|
|
4937
|
+
metadata: toolMetadata(["code-execution", "filesystem"], "high"),
|
|
4938
|
+
execute: async ({ commands }, context) => serializeJsonValue2(await requireExecutionSession(context).runCommandBatch(commands, context))
|
|
4939
|
+
}),
|
|
4428
4940
|
environment_status: tool({
|
|
4429
4941
|
name: "environment_status",
|
|
4430
4942
|
description: "Inspect the immutable image binding and enforced policy for the active run without exposing host paths or environment variables.",
|
|
@@ -4445,6 +4957,83 @@ var createExecutionEnvironmentTools = (workspace) => ({
|
|
|
4445
4957
|
schema: z3.strictObject({ patchId: fileDigestSchema }),
|
|
4446
4958
|
...mutationApproval,
|
|
4447
4959
|
execute: async ({ patchId }, context) => serializeJsonValue2(await requireExecutionSession(context).importPatch(workspace, patchId))
|
|
4960
|
+
}),
|
|
4961
|
+
verify_and_apply_environment_patch: tool({
|
|
4962
|
+
name: "verify_and_apply_environment_patch",
|
|
4963
|
+
description: "Request one approval to verify an already inspected content-bound OCI patch with exact allowlisted argv and import it only when verification succeeds without changing the reviewed patch.",
|
|
4964
|
+
schema: z3.strictObject({
|
|
4965
|
+
patchId: fileDigestSchema,
|
|
4966
|
+
command: z3.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._+-]*$/),
|
|
4967
|
+
args: z3.array(z3.string().max(8192)).max(256).default([])
|
|
4968
|
+
}),
|
|
4969
|
+
requiresApproval: true,
|
|
4970
|
+
approvalMode: "interrupt",
|
|
4971
|
+
approvalVersion: "2026-08-21-verify-and-apply-v1",
|
|
4972
|
+
metadata: toolMetadata(["code-execution", "filesystem"], "high"),
|
|
4973
|
+
execute: async ({ patchId, command, args }, context) => {
|
|
4974
|
+
const session = requireExecutionSession(context);
|
|
4975
|
+
const beforeVerification = await session.inspectPatch();
|
|
4976
|
+
if (beforeVerification.patchId !== patchId) {
|
|
4977
|
+
throw new Error("The OCI patch changed after review; inspect it again before verification and import.");
|
|
4978
|
+
}
|
|
4979
|
+
const verification = await session.runCommand(command, args, context);
|
|
4980
|
+
if (verification.exitCode !== 0) {
|
|
4981
|
+
throw new Error(`The approved verifier failed with exit code ${verification.exitCode}; the host workspace was not changed.`);
|
|
4982
|
+
}
|
|
4983
|
+
const afterVerification = await session.inspectPatch();
|
|
4984
|
+
if (afterVerification.patchId !== patchId) {
|
|
4985
|
+
throw new Error("The verifier changed the reviewed OCI patch; the host workspace was not changed.");
|
|
4986
|
+
}
|
|
4987
|
+
const imported = await session.importPatch(workspace, patchId);
|
|
4988
|
+
return serializeJsonValue2({
|
|
4989
|
+
schemaVersion: 1,
|
|
4990
|
+
kind: "verified-environment-patch-import",
|
|
4991
|
+
patchId,
|
|
4992
|
+
verification,
|
|
4993
|
+
imported
|
|
4994
|
+
});
|
|
4995
|
+
}
|
|
4996
|
+
}),
|
|
4997
|
+
verify_and_apply_reviewed_edits: tool({
|
|
4998
|
+
name: "verify_and_apply_reviewed_edits",
|
|
4999
|
+
description: "Request one approval for complete digest-bound edits and exact verifier argv. The transaction requires a clean OCI snapshot, applies the edits atomically, rejects verifier-created drift, and imports the reviewed patch only after exit code 0.",
|
|
5000
|
+
schema: verifiedReviewedEditsInputSchema,
|
|
5001
|
+
requiresApproval: true,
|
|
5002
|
+
approvalMode: "interrupt",
|
|
5003
|
+
approvalVersion: "2026-08-21-verify-reviewed-edits-v1",
|
|
5004
|
+
metadata: toolMetadata(["code-execution", "filesystem", "write"], "high"),
|
|
5005
|
+
execute: async ({ changes, command, args }, context) => {
|
|
5006
|
+
const session = requireExecutionSession(context);
|
|
5007
|
+
const initialPatch = await session.inspectPatch();
|
|
5008
|
+
if (initialPatch.entries.length !== 0) {
|
|
5009
|
+
throw new Error("The verified edit transaction requires a clean OCI snapshot; the host workspace was not changed.");
|
|
5010
|
+
}
|
|
5011
|
+
const proposal = createEditProposal({ changes });
|
|
5012
|
+
await session.workspace.applyPatch({ proposalId: proposal.proposalId, changes });
|
|
5013
|
+
const reviewedPatch = await session.inspectPatch();
|
|
5014
|
+
const approvedPaths = [...new Set(changes.map((change) => change.path))].sort();
|
|
5015
|
+
const reviewedPaths = reviewedPatch.entries.map((entry) => entry.path).sort();
|
|
5016
|
+
if (JSON.stringify(reviewedPaths) !== JSON.stringify(approvedPaths)) {
|
|
5017
|
+
throw new Error("The OCI patch does not match the approved edit paths; the host workspace was not changed.");
|
|
5018
|
+
}
|
|
5019
|
+
const verification = await session.runCommand(command, args, context);
|
|
5020
|
+
if (verification.exitCode !== 0) {
|
|
5021
|
+
throw new Error(`The approved verifier failed with exit code ${verification.exitCode}; the host workspace was not changed.`);
|
|
5022
|
+
}
|
|
5023
|
+
const afterVerification = await session.inspectPatch();
|
|
5024
|
+
if (afterVerification.patchId !== reviewedPatch.patchId) {
|
|
5025
|
+
throw new Error("The verifier changed the reviewed OCI patch; the host workspace was not changed.");
|
|
5026
|
+
}
|
|
5027
|
+
const imported = await session.importPatch(workspace, reviewedPatch.patchId);
|
|
5028
|
+
return serializeJsonValue2({
|
|
5029
|
+
schemaVersion: 1,
|
|
5030
|
+
kind: "verified-reviewed-edit-import",
|
|
5031
|
+
proposalId: proposal.proposalId,
|
|
5032
|
+
patchId: reviewedPatch.patchId,
|
|
5033
|
+
verification,
|
|
5034
|
+
imported
|
|
5035
|
+
});
|
|
5036
|
+
}
|
|
4448
5037
|
})
|
|
4449
5038
|
});
|
|
4450
5039
|
var estimateMessageTokens = (messages) => Math.max(1, Math.ceil(JSON.stringify(messages).length / 4));
|
|
@@ -4455,7 +5044,7 @@ var summarizeHarnessMessages = (messages) => {
|
|
|
4455
5044
|
const parts = (record.parts ?? []).map((part) => {
|
|
4456
5045
|
if (part.type === "text" && typeof part.text === "string") {
|
|
4457
5046
|
const text = redaction.redactText(part.text).replace(/\bBearer\s+\S+/gi, "Bearer [REDACTED]").replace(/\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)|api[_-]?key|access[_-]?token|password)\s*([=:])\s*(?:"[^"]*"|'[^']*'|\S+)/gi, "$1$2[REDACTED]").replace(/\b(?:sk|ghp|gho|github_pat)-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]").replace(/\s+/g, " ").trim();
|
|
4458
|
-
return text.length > 160 ? `${text.slice(0, 160)}
|
|
5047
|
+
return text.length > 160 ? `${text.slice(0, 160)}…` : text;
|
|
4459
5048
|
}
|
|
4460
5049
|
if (part.type === "tool-call") {
|
|
4461
5050
|
const call = part.toolCall;
|
|
@@ -4471,7 +5060,7 @@ var summarizeHarnessMessages = (messages) => {
|
|
|
4471
5060
|
});
|
|
4472
5061
|
const summary = summaries.join(`
|
|
4473
5062
|
`);
|
|
4474
|
-
return summary.length > 4000 ? `${summary.slice(0, 4000)}
|
|
5063
|
+
return summary.length > 4000 ? `${summary.slice(0, 4000)}…` : summary;
|
|
4475
5064
|
};
|
|
4476
5065
|
var compactHarnessMessages = (messages) => {
|
|
4477
5066
|
if (messages.length === 0)
|
|
@@ -4676,6 +5265,272 @@ Available bounded delegations: ${config.orchestration.profiles.map((profile) =>
|
|
|
4676
5265
|
}
|
|
4677
5266
|
};
|
|
4678
5267
|
};
|
|
5268
|
+
var canonicalJson = (value) => {
|
|
5269
|
+
if (value === null || typeof value !== "object")
|
|
5270
|
+
return JSON.stringify(value);
|
|
5271
|
+
if (Array.isArray(value))
|
|
5272
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
5273
|
+
const record = value;
|
|
5274
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
|
|
5275
|
+
};
|
|
5276
|
+
var localApprovalResolutionPayload = (inputDigest, approve, reason) => JSON.stringify({ inputDigest, approve, reason: reason ?? null });
|
|
5277
|
+
var terminalToolCallId = (runId, step, providerToolCallId, toolName2, input) => `tool_${createHash7("sha256").update(`${runId}\x00${step}\x00${providerToolCallId}\x00${toolName2}\x00${canonicalJson(input)}`).digest("hex")}`;
|
|
5278
|
+
var recoverableTerminalStaleDigest = (toolName2, message) => toolName2 === "verify_and_apply_reviewed_edits" && /^Stale patch rejected for .+\.$/.test(message);
|
|
5279
|
+
var executeTerminalReceiptTool = async (harness, waiting, approval, response) => {
|
|
5280
|
+
if (approval.kind !== "local-tool" || response.provider !== approval.provider || response.approvalRequestId !== approval.id || !response.approve || !approval.toolCallId || approval.step === undefined) {
|
|
5281
|
+
throw new Error("Terminal receipt finalization requires one matching approved local-tool request.");
|
|
5282
|
+
}
|
|
5283
|
+
const candidate = harness.agent.tools?.[approval.name];
|
|
5284
|
+
if (!candidate || !("execute" in candidate)) {
|
|
5285
|
+
throw new Error(`Approved terminal tool ${approval.name} is not locally callable.`);
|
|
5286
|
+
}
|
|
5287
|
+
const definition = candidate;
|
|
5288
|
+
const parsedInput = definition.schema.parse(JSON.parse(approval.arguments));
|
|
5289
|
+
const serializedInput = serializeJsonValue2(parsedInput);
|
|
5290
|
+
const session = await harness.executionEnvironment?.acquire({
|
|
5291
|
+
runId: waiting.state.runId,
|
|
5292
|
+
...waiting.state.agentId ? { agentId: waiting.state.agentId } : {},
|
|
5293
|
+
...waiting.state.scope ? { scope: waiting.state.scope } : {},
|
|
5294
|
+
...waiting.state.metadata ? { metadata: waiting.state.metadata } : {}
|
|
5295
|
+
});
|
|
5296
|
+
if (!session)
|
|
5297
|
+
throw new Error("Terminal receipt finalization requires an active execution environment.");
|
|
5298
|
+
const toolVersion = [
|
|
5299
|
+
definition.approvalVersion,
|
|
5300
|
+
`environment:${session.binding.fingerprint}`
|
|
5301
|
+
].filter(Boolean).join("|") || "1";
|
|
5302
|
+
const inputDigest = createHash7("sha256").update(canonicalJson({
|
|
5303
|
+
runId: waiting.state.runId,
|
|
5304
|
+
step: approval.step,
|
|
5305
|
+
toolCallId: approval.toolCallId,
|
|
5306
|
+
toolName: approval.name,
|
|
5307
|
+
input: serializedInput,
|
|
5308
|
+
toolVersion
|
|
5309
|
+
})).digest("hex");
|
|
5310
|
+
if (approval.id !== `approval_${inputDigest}` || approval.inputDigest !== inputDigest || approval.toolVersion !== toolVersion || approval.arguments !== canonicalJson(serializedInput)) {
|
|
5311
|
+
await session.release?.({ status: "failed", error: { message: "Approval binding mismatch." } });
|
|
5312
|
+
throw new Error("The terminal tool approval is stale or does not match the current runtime binding.");
|
|
5313
|
+
}
|
|
5314
|
+
if (harness.agent.toolApprovalSigner) {
|
|
5315
|
+
if (!approval.signature) {
|
|
5316
|
+
await session.release?.({ status: "failed", error: { message: "Missing approval signature." } });
|
|
5317
|
+
throw new Error("The terminal tool approval is missing its required signature.");
|
|
5318
|
+
}
|
|
5319
|
+
const valid = harness.agent.toolApprovalSigner.verify ? await harness.agent.toolApprovalSigner.verify(inputDigest, approval.signature) : await harness.agent.toolApprovalSigner.sign(inputDigest) === approval.signature;
|
|
5320
|
+
if (!valid) {
|
|
5321
|
+
await session.release?.({ status: "failed", error: { message: "Invalid approval signature." } });
|
|
5322
|
+
throw new Error("The terminal tool approval signature is invalid.");
|
|
5323
|
+
}
|
|
5324
|
+
}
|
|
5325
|
+
const toolCall = { id: approval.toolCallId, name: approval.name, input: serializedInput };
|
|
5326
|
+
const context = {
|
|
5327
|
+
runId: waiting.state.runId,
|
|
5328
|
+
...waiting.state.agentId ? { agentId: waiting.state.agentId } : {},
|
|
5329
|
+
...harness.agent.name ? { agentName: harness.agent.name } : {},
|
|
5330
|
+
...waiting.state.scope ? { scope: waiting.state.scope } : {},
|
|
5331
|
+
...waiting.state.metadata ? { metadata: waiting.state.metadata } : {},
|
|
5332
|
+
executionEnvironment: session,
|
|
5333
|
+
toolCall,
|
|
5334
|
+
step: approval.step,
|
|
5335
|
+
model: harness.agent.model,
|
|
5336
|
+
idempotencyKey: `${waiting.state.runId}:${approval.toolCallId}`
|
|
5337
|
+
};
|
|
5338
|
+
const authorization = {
|
|
5339
|
+
manifest: session.manifest,
|
|
5340
|
+
binding: session.binding,
|
|
5341
|
+
tool: definition,
|
|
5342
|
+
toolCall,
|
|
5343
|
+
input: parsedInput,
|
|
5344
|
+
context,
|
|
5345
|
+
phase: "execute"
|
|
5346
|
+
};
|
|
5347
|
+
const decision = await session.authorize(authorization);
|
|
5348
|
+
if (decision.decision === "deny") {
|
|
5349
|
+
await session.release?.({ status: "failed", error: { message: decision.reason } });
|
|
5350
|
+
throw new Error(`Terminal tool execution was denied by the execution environment: ${decision.reason}`);
|
|
5351
|
+
}
|
|
5352
|
+
const resolutionSignature = harness.agent.toolApprovalSigner ? await harness.agent.toolApprovalSigner.sign(localApprovalResolutionPayload(inputDigest, response.approve, response.reason)) : undefined;
|
|
5353
|
+
const durableId = terminalToolCallId(waiting.state.runId, approval.step, approval.toolCallId, approval.name, serializedInput);
|
|
5354
|
+
const idempotencyKey = `${waiting.state.runId}:${durableId}`;
|
|
5355
|
+
const journalCandidate = {
|
|
5356
|
+
runId: waiting.state.runId,
|
|
5357
|
+
...waiting.state.scope ? { scope: waiting.state.scope } : {},
|
|
5358
|
+
toolCallId: durableId,
|
|
5359
|
+
toolName: approval.name,
|
|
5360
|
+
status: "pending",
|
|
5361
|
+
idempotencyKey,
|
|
5362
|
+
revision: 0,
|
|
5363
|
+
input: serializedInput,
|
|
5364
|
+
updatedAt: Date.now()
|
|
5365
|
+
};
|
|
5366
|
+
const journal = harness.store.claimToolExecution ? await harness.store.claimToolExecution(journalCandidate) : undefined;
|
|
5367
|
+
let output;
|
|
5368
|
+
try {
|
|
5369
|
+
if (journal && !journal.claimed) {
|
|
5370
|
+
if (journal.entry.status === "completed") {
|
|
5371
|
+
output = journal.entry.output ?? null;
|
|
5372
|
+
} else if (journal.entry.status === "failed") {
|
|
5373
|
+
throw new Error(journal.entry.error?.message ?? `Tool "${approval.name}" previously failed.`);
|
|
5374
|
+
} else {
|
|
5375
|
+
throw new Error(`Tool "${approval.name}" has an indeterminate durable execution.`);
|
|
5376
|
+
}
|
|
5377
|
+
} else {
|
|
5378
|
+
output = serializeJsonValue2(await session.execute(authorization, () => definition.execute(parsedInput, { ...context, idempotencyKey })));
|
|
5379
|
+
if (journal?.claimed && harness.store.completeToolExecution) {
|
|
5380
|
+
await harness.store.completeToolExecution({
|
|
5381
|
+
...journal.entry,
|
|
5382
|
+
status: "completed",
|
|
5383
|
+
output,
|
|
5384
|
+
completedAt: Date.now(),
|
|
5385
|
+
updatedAt: Date.now()
|
|
5386
|
+
}, { expectedRevision: journal.entry.revision });
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
} catch (error) {
|
|
5390
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5391
|
+
if (journal?.claimed && harness.store.completeToolExecution) {
|
|
5392
|
+
await Promise.resolve(harness.store.completeToolExecution({
|
|
5393
|
+
...journal.entry,
|
|
5394
|
+
status: "failed",
|
|
5395
|
+
error: { message },
|
|
5396
|
+
completedAt: Date.now(),
|
|
5397
|
+
updatedAt: Date.now()
|
|
5398
|
+
}, { expectedRevision: journal.entry.revision })).catch(() => {
|
|
5399
|
+
return;
|
|
5400
|
+
});
|
|
5401
|
+
}
|
|
5402
|
+
await session.release?.({
|
|
5403
|
+
status: "failed",
|
|
5404
|
+
error: { message }
|
|
5405
|
+
});
|
|
5406
|
+
if (!recoverableTerminalStaleDigest(approval.name, message))
|
|
5407
|
+
throw error;
|
|
5408
|
+
const now2 = Date.now();
|
|
5409
|
+
const toolResult2 = {
|
|
5410
|
+
toolCallId: approval.toolCallId,
|
|
5411
|
+
toolName: approval.name,
|
|
5412
|
+
error: { message },
|
|
5413
|
+
isError: true
|
|
5414
|
+
};
|
|
5415
|
+
const messages2 = [
|
|
5416
|
+
...waiting.messages,
|
|
5417
|
+
{ role: "tool", parts: [{ type: "tool-result", toolResult: toolResult2 }] }
|
|
5418
|
+
];
|
|
5419
|
+
const steps2 = waiting.steps.map((step) => step.index === approval.step ? {
|
|
5420
|
+
...step,
|
|
5421
|
+
status: "completed",
|
|
5422
|
+
finishedAt: now2,
|
|
5423
|
+
toolResults: [...step.toolResults, toolResult2]
|
|
5424
|
+
} : step);
|
|
5425
|
+
const previousRevision2 = waiting.state.revision ?? 0;
|
|
5426
|
+
const {
|
|
5427
|
+
finalOutput: _finalOutput,
|
|
5428
|
+
finishReason: _finishReason,
|
|
5429
|
+
providerFinishReason: _providerFinishReason,
|
|
5430
|
+
error: _stateError,
|
|
5431
|
+
...resumableState
|
|
5432
|
+
} = waiting.state;
|
|
5433
|
+
const state2 = {
|
|
5434
|
+
...resumableState,
|
|
5435
|
+
revision: previousRevision2 + 1,
|
|
5436
|
+
status: "running",
|
|
5437
|
+
messages: messages2,
|
|
5438
|
+
steps: steps2,
|
|
5439
|
+
toolResults: [...waiting.toolResults, toolResult2],
|
|
5440
|
+
pendingApprovals: [],
|
|
5441
|
+
approvalHistory: [
|
|
5442
|
+
...waiting.state.approvalHistory ?? [],
|
|
5443
|
+
{
|
|
5444
|
+
requestId: approval.id,
|
|
5445
|
+
kind: "local-tool",
|
|
5446
|
+
provider: approval.provider,
|
|
5447
|
+
approve: true,
|
|
5448
|
+
...response.reason ? { reason: response.reason } : {},
|
|
5449
|
+
toolCallId: approval.toolCallId,
|
|
5450
|
+
step: approval.step,
|
|
5451
|
+
inputDigest,
|
|
5452
|
+
toolVersion,
|
|
5453
|
+
...resolutionSignature ? { signature: resolutionSignature } : {},
|
|
5454
|
+
resolvedAt: now2
|
|
5455
|
+
}
|
|
5456
|
+
],
|
|
5457
|
+
updatedAt: now2
|
|
5458
|
+
};
|
|
5459
|
+
await harness.store.save(state2, { expectedRevision: previousRevision2 });
|
|
5460
|
+
return {
|
|
5461
|
+
status: "running",
|
|
5462
|
+
outputText: state2.outputText,
|
|
5463
|
+
...waiting.usage ? { usage: waiting.usage } : {},
|
|
5464
|
+
messages: messages2,
|
|
5465
|
+
steps: steps2,
|
|
5466
|
+
toolResults: state2.toolResults,
|
|
5467
|
+
state: state2
|
|
5468
|
+
};
|
|
5469
|
+
}
|
|
5470
|
+
const now = Date.now();
|
|
5471
|
+
const toolResult = {
|
|
5472
|
+
toolCallId: approval.toolCallId,
|
|
5473
|
+
toolName: approval.name,
|
|
5474
|
+
output,
|
|
5475
|
+
isError: false
|
|
5476
|
+
};
|
|
5477
|
+
const messages = [
|
|
5478
|
+
...waiting.messages,
|
|
5479
|
+
{ role: "tool", parts: [{ type: "tool-result", toolResult }] }
|
|
5480
|
+
];
|
|
5481
|
+
const steps = waiting.steps.map((step) => step.index === approval.step ? {
|
|
5482
|
+
...step,
|
|
5483
|
+
status: "completed",
|
|
5484
|
+
finishedAt: now,
|
|
5485
|
+
toolResults: [...step.toolResults, toolResult]
|
|
5486
|
+
} : step);
|
|
5487
|
+
const outputText = canonicalJson(output);
|
|
5488
|
+
const previousRevision = waiting.state.revision ?? 0;
|
|
5489
|
+
const state = {
|
|
5490
|
+
...waiting.state,
|
|
5491
|
+
revision: previousRevision + 1,
|
|
5492
|
+
status: "completed",
|
|
5493
|
+
messages,
|
|
5494
|
+
steps,
|
|
5495
|
+
toolResults: [...waiting.toolResults, toolResult],
|
|
5496
|
+
outputText,
|
|
5497
|
+
finalOutput: output,
|
|
5498
|
+
finishReason: "stop",
|
|
5499
|
+
providerFinishReason: "terminal-tool-receipt",
|
|
5500
|
+
pendingApprovals: [],
|
|
5501
|
+
approvalHistory: [
|
|
5502
|
+
...waiting.state.approvalHistory ?? [],
|
|
5503
|
+
{
|
|
5504
|
+
requestId: approval.id,
|
|
5505
|
+
kind: "local-tool",
|
|
5506
|
+
provider: approval.provider,
|
|
5507
|
+
approve: true,
|
|
5508
|
+
...response.reason ? { reason: response.reason } : {},
|
|
5509
|
+
toolCallId: approval.toolCallId,
|
|
5510
|
+
step: approval.step,
|
|
5511
|
+
inputDigest,
|
|
5512
|
+
toolVersion,
|
|
5513
|
+
...resolutionSignature ? { signature: resolutionSignature } : {},
|
|
5514
|
+
resolvedAt: now
|
|
5515
|
+
}
|
|
5516
|
+
],
|
|
5517
|
+
updatedAt: now
|
|
5518
|
+
};
|
|
5519
|
+
await harness.store.save(state, { expectedRevision: previousRevision });
|
|
5520
|
+
await session.release?.({ status: "completed" });
|
|
5521
|
+
return {
|
|
5522
|
+
status: "completed",
|
|
5523
|
+
outputText,
|
|
5524
|
+
finalOutput: output,
|
|
5525
|
+
finishReason: "stop",
|
|
5526
|
+
providerFinishReason: "terminal-tool-receipt",
|
|
5527
|
+
...waiting.usage ? { usage: waiting.usage } : {},
|
|
5528
|
+
messages,
|
|
5529
|
+
steps,
|
|
5530
|
+
toolResults: state.toolResults,
|
|
5531
|
+
state
|
|
5532
|
+
};
|
|
5533
|
+
};
|
|
4679
5534
|
var runHarness = async (harness, input, options = {}) => {
|
|
4680
5535
|
let nextInput = input;
|
|
4681
5536
|
const continuationOptions = {
|
|
@@ -4716,6 +5571,17 @@ var runHarness = async (harness, input, options = {}) => {
|
|
|
4716
5571
|
if (!approvals) {
|
|
4717
5572
|
return result;
|
|
4718
5573
|
}
|
|
5574
|
+
const terminalTools = new Set(options.terminalReceiptTools ?? []);
|
|
5575
|
+
if (result.state.pendingApprovals.length === 1 && approvals.length === 1 && approvals[0]?.approve === true && terminalTools.has(result.state.pendingApprovals[0].name)) {
|
|
5576
|
+
const terminalResult = await executeTerminalReceiptTool(harness, result, result.state.pendingApprovals[0], approvals[0]);
|
|
5577
|
+
if (terminalResult.status === "completed")
|
|
5578
|
+
return terminalResult;
|
|
5579
|
+
nextInput = {
|
|
5580
|
+
...continuationOptions,
|
|
5581
|
+
state: terminalResult.state
|
|
5582
|
+
};
|
|
5583
|
+
continue;
|
|
5584
|
+
}
|
|
4719
5585
|
nextInput = {
|
|
4720
5586
|
...continuationOptions,
|
|
4721
5587
|
state: result.state,
|
|
@@ -4903,11 +5769,10 @@ var streamResultDocument = (result, sequence = 0) => {
|
|
|
4903
5769
|
var serializeStreamResult = (result, sequence = 0) => JSON.stringify(streamResultDocument(result, sequence));
|
|
4904
5770
|
|
|
4905
5771
|
// src/sessions.ts
|
|
4906
|
-
import { createHash as createHash8, randomUUID as randomUUID4 } from "crypto";
|
|
4907
|
-
import { chmod as chmod3, lstat as
|
|
4908
|
-
import
|
|
5772
|
+
import { createHash as createHash8, randomUUID as randomUUID4 } from "node:crypto";
|
|
5773
|
+
import { chmod as chmod3, lstat as lstat7, mkdir as mkdir4, open as open3, realpath as realpath4, stat as stat3 } from "node:fs/promises";
|
|
5774
|
+
import path9 from "node:path";
|
|
4909
5775
|
import { createRedactionPolicy as createRedactionPolicy3 } from "@zhivex-ai/agents";
|
|
4910
|
-
import { Database as Database2 } from "bun:sqlite";
|
|
4911
5776
|
var HARNESS_SESSION_INDEX_FILE = "operations.sqlite";
|
|
4912
5777
|
var HARNESS_SESSION_SCHEMA_VERSION = 1;
|
|
4913
5778
|
var SESSION_RUN_STATUSES = [
|
|
@@ -5011,19 +5876,19 @@ var sessionId = () => `ses_${randomUUID4()}`;
|
|
|
5011
5876
|
var turnId = () => `turn_${randomUUID4()}`;
|
|
5012
5877
|
var isMissing = (error) => error.code === "ENOENT";
|
|
5013
5878
|
var ensurePrivateDatabase = async (workspace, stateDirectory) => {
|
|
5014
|
-
const requestedStateDirectory =
|
|
5879
|
+
const requestedStateDirectory = path9.resolve(stateDirectory);
|
|
5015
5880
|
await validateStateDirectory(workspace, requestedStateDirectory);
|
|
5016
5881
|
await mkdir4(requestedStateDirectory, { recursive: true, mode: 448 });
|
|
5017
|
-
const directoryEntry = await
|
|
5882
|
+
const directoryEntry = await lstat7(requestedStateDirectory);
|
|
5018
5883
|
if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
|
|
5019
5884
|
throw new Error(`The session state directory must be a real directory: ${requestedStateDirectory}.`);
|
|
5020
5885
|
}
|
|
5021
5886
|
await chmod3(requestedStateDirectory, 448);
|
|
5022
5887
|
const canonicalStateDirectory2 = await realpath4(requestedStateDirectory);
|
|
5023
|
-
const databasePath =
|
|
5888
|
+
const databasePath = path9.join(canonicalStateDirectory2, HARNESS_SESSION_INDEX_FILE);
|
|
5024
5889
|
let databaseEntry;
|
|
5025
5890
|
try {
|
|
5026
|
-
databaseEntry = await
|
|
5891
|
+
databaseEntry = await lstat7(databasePath);
|
|
5027
5892
|
} catch (error) {
|
|
5028
5893
|
if (!isMissing(error)) {
|
|
5029
5894
|
throw error;
|
|
@@ -5036,7 +5901,7 @@ var ensurePrivateDatabase = async (workspace, stateDirectory) => {
|
|
|
5036
5901
|
throw creationError;
|
|
5037
5902
|
}
|
|
5038
5903
|
}
|
|
5039
|
-
databaseEntry = await
|
|
5904
|
+
databaseEntry = await lstat7(databasePath);
|
|
5040
5905
|
}
|
|
5041
5906
|
if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
|
|
5042
5907
|
throw new Error(`The session SQLite path must be a real file: ${databasePath}.`);
|
|
@@ -5060,7 +5925,7 @@ var toRunReference = (row) => ({
|
|
|
5060
5925
|
var metadataSize = (input) => Object.values(input).reduce((total, value) => total + (typeof value === "string" ? byteLength(value) : 0), 0);
|
|
5061
5926
|
var isTerminal = (status) => TERMINAL_STATUSES2.has(status);
|
|
5062
5927
|
var openCliSessionStore = async (options) => {
|
|
5063
|
-
const workspace = await realpath4(
|
|
5928
|
+
const workspace = await realpath4(path9.resolve(options.workspace));
|
|
5064
5929
|
const workspaceEntry = await stat3(workspace);
|
|
5065
5930
|
if (!workspaceEntry.isDirectory()) {
|
|
5066
5931
|
throw new Error(`The session workspace is not a directory: ${workspace}.`);
|
|
@@ -5075,7 +5940,7 @@ var openCliSessionStore = async (options) => {
|
|
|
5075
5940
|
const maxIndexBytes = boundedInteger("maxIndexBytes", options.maxIndexBytes, DEFAULT_MAX_INDEX_BYTES, 1024);
|
|
5076
5941
|
const now = options.now ?? Date.now;
|
|
5077
5942
|
const { databasePath, databaseEntry } = await ensurePrivateDatabase(workspace, options.stateDirectory);
|
|
5078
|
-
const database = new
|
|
5943
|
+
const database = new SqliteDatabase(databasePath, { create: false, strict: true });
|
|
5079
5944
|
let closed = false;
|
|
5080
5945
|
const assertOpen = () => {
|
|
5081
5946
|
if (closed) {
|
|
@@ -5195,7 +6060,7 @@ var openCliSessionStore = async (options) => {
|
|
|
5195
6060
|
`).run(candidateTurnId, sourceTurnId ?? null, candidateSessionId, sequence, runId, provider, model, role, status, createdAt, isTerminal(status) ? createdAt : null, entryBytes);
|
|
5196
6061
|
};
|
|
5197
6062
|
try {
|
|
5198
|
-
const openedEntry = await
|
|
6063
|
+
const openedEntry = await lstat7(databasePath);
|
|
5199
6064
|
if (openedEntry.isSymbolicLink() || !openedEntry.isFile() || openedEntry.dev !== databaseEntry.dev || openedEntry.ino !== databaseEntry.ino) {
|
|
5200
6065
|
throw new Error(`The session SQLite path changed while it was being opened: ${databasePath}.`);
|
|
5201
6066
|
}
|
|
@@ -5554,7 +6419,7 @@ var openCliSessionStore = async (options) => {
|
|
|
5554
6419
|
var openSessionStore = openCliSessionStore;
|
|
5555
6420
|
|
|
5556
6421
|
// src/change-envelope.ts
|
|
5557
|
-
import { createHash as createHash9 } from "crypto";
|
|
6422
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
5558
6423
|
import { z as z4 } from "zod";
|
|
5559
6424
|
var CHANGE_ENVELOPE_SCHEMA_VERSION = 1;
|
|
5560
6425
|
var CHANGE_ENVELOPE_DIGEST_ALGORITHM = "sha256";
|
|
@@ -5567,7 +6432,7 @@ var boundedIdentifierSchema = z4.string().min(1).max(200).regex(/^[A-Za-z0-9][A-
|
|
|
5567
6432
|
var approvalScopeSchema = z4.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._:/*-]*$/, "Approval scope must contain only bounded token characters.");
|
|
5568
6433
|
var attestationFormatSchema = z4.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9.+_-]*$/);
|
|
5569
6434
|
var mediaTypeSchema = z4.string().min(3).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/);
|
|
5570
|
-
var addDuplicateIssues = (values, key, context,
|
|
6435
|
+
var addDuplicateIssues = (values, key, context, path10) => {
|
|
5571
6436
|
const seen = new Set;
|
|
5572
6437
|
for (let index = 0;index < values.length; index += 1) {
|
|
5573
6438
|
const value = values[index];
|
|
@@ -5577,22 +6442,22 @@ var addDuplicateIssues = (values, key, context, path9) => {
|
|
|
5577
6442
|
if (seen.has(identifier2)) {
|
|
5578
6443
|
context.addIssue({
|
|
5579
6444
|
code: "custom",
|
|
5580
|
-
path: [index,
|
|
5581
|
-
message: `Duplicate ${
|
|
6445
|
+
path: [index, path10],
|
|
6446
|
+
message: `Duplicate ${path10}: ${identifier2}.`
|
|
5582
6447
|
});
|
|
5583
6448
|
}
|
|
5584
6449
|
seen.add(identifier2);
|
|
5585
6450
|
}
|
|
5586
6451
|
};
|
|
5587
|
-
var addCanonicalOrderIssue = (values, key, context,
|
|
6452
|
+
var addCanonicalOrderIssue = (values, key, context, path10) => {
|
|
5588
6453
|
for (let index = 1;index < values.length; index += 1) {
|
|
5589
6454
|
const previous = values[index - 1];
|
|
5590
6455
|
const current = values[index];
|
|
5591
6456
|
if (previous !== undefined && current !== undefined && key(previous) >= key(current)) {
|
|
5592
6457
|
context.addIssue({
|
|
5593
6458
|
code: "custom",
|
|
5594
|
-
path: [index,
|
|
5595
|
-
message: `${
|
|
6459
|
+
path: [index, path10],
|
|
6460
|
+
message: `${path10} entries must be unique and in canonical ascending order.`
|
|
5596
6461
|
});
|
|
5597
6462
|
}
|
|
5598
6463
|
}
|
|
@@ -5760,7 +6625,7 @@ var changeEnvelopeSchema = z4.strictObject({
|
|
|
5760
6625
|
}
|
|
5761
6626
|
}
|
|
5762
6627
|
});
|
|
5763
|
-
var
|
|
6628
|
+
var canonicalJson2 = (value) => {
|
|
5764
6629
|
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
5765
6630
|
return JSON.stringify(value);
|
|
5766
6631
|
}
|
|
@@ -5770,15 +6635,15 @@ var canonicalJson = (value) => {
|
|
|
5770
6635
|
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
5771
6636
|
}
|
|
5772
6637
|
if (Array.isArray(value)) {
|
|
5773
|
-
return `[${value.map((entry) =>
|
|
6638
|
+
return `[${value.map((entry) => canonicalJson2(entry)).join(",")}]`;
|
|
5774
6639
|
}
|
|
5775
6640
|
if (typeof value === "object") {
|
|
5776
6641
|
const record = value;
|
|
5777
|
-
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${
|
|
6642
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson2(record[key])}`).join(",")}}`;
|
|
5778
6643
|
}
|
|
5779
6644
|
throw new TypeError(`Canonical JSON cannot encode ${typeof value}.`);
|
|
5780
6645
|
};
|
|
5781
|
-
var digestCanonical = (value) => `sha256:${createHash9("sha256").update(
|
|
6646
|
+
var digestCanonical = (value) => `sha256:${createHash9("sha256").update(canonicalJson2(value)).digest("hex")}`;
|
|
5782
6647
|
var digestChangeEnvelopeArtifact = (value) => `sha256:${createHash9("sha256").update(value).digest("hex")}`;
|
|
5783
6648
|
var evidencePayload = (checks) => ({
|
|
5784
6649
|
schemaVersion: CHANGE_ENVELOPE_SCHEMA_VERSION,
|
|
@@ -5826,7 +6691,7 @@ var createChangeEnvelope = (input) => {
|
|
|
5826
6691
|
envelopeId: digestCanonical(payload)
|
|
5827
6692
|
});
|
|
5828
6693
|
};
|
|
5829
|
-
var canonicalizeChangeEnvelope = (input) =>
|
|
6694
|
+
var canonicalizeChangeEnvelope = (input) => canonicalJson2(changeEnvelopeSchema.parse(input));
|
|
5830
6695
|
var computeChangeEnvelopeDigest = (input) => {
|
|
5831
6696
|
const parsed = changeEnvelopeSchema.parse(input);
|
|
5832
6697
|
return digestCanonical(envelopePayload(parsed));
|
|
@@ -5960,11 +6825,11 @@ var verifyChangeEnvelope = (input, options = {}) => {
|
|
|
5960
6825
|
});
|
|
5961
6826
|
}
|
|
5962
6827
|
const preconditions = parsedOptions.preconditions;
|
|
5963
|
-
const comparePrecondition = (field, expected, actual,
|
|
6828
|
+
const comparePrecondition = (field, expected, actual, path10) => {
|
|
5964
6829
|
if (expected !== undefined && expected !== actual) {
|
|
5965
6830
|
issues.push({
|
|
5966
6831
|
code: "precondition-mismatch",
|
|
5967
|
-
path:
|
|
6832
|
+
path: path10,
|
|
5968
6833
|
message: `Change envelope precondition does not match: ${field}.`,
|
|
5969
6834
|
expected,
|
|
5970
6835
|
actual
|
|
@@ -6715,7 +7580,7 @@ Options:
|
|
|
6715
7580
|
--execution <none|oci> Enforced execution backend (default: none)
|
|
6716
7581
|
--oci-runtime <docker|podman> Local OCI runtime (default: docker)
|
|
6717
7582
|
--oci-image <reference> Preloaded immutable-capable OCI image
|
|
6718
|
-
--oci-allow-command <name> Allow one argv executable in OCI; repeatable,
|
|
7583
|
+
--oci-allow-command <name> Allow one argv executable in OCI; repeatable, include a package manager
|
|
6719
7584
|
--oci-max-process-runtime-ms <n> Per-command timeout (default: 120000)
|
|
6720
7585
|
--oci-max-process-output-bytes <n> Combined output ceiling (default: 20000)
|
|
6721
7586
|
--oci-max-memory-mb <n> Container memory ceiling (default: 1024)
|
|
@@ -6767,7 +7632,7 @@ Credentials:
|
|
|
6767
7632
|
Gemini: GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY`;
|
|
6768
7633
|
var summarizeApproval = (approval) => {
|
|
6769
7634
|
const mustShowCompleteProposal = approval.name === "apply_patch";
|
|
6770
|
-
const argumentsText = !mustShowCompleteProposal && approval.arguments.length > 1200 ? `${approval.arguments.slice(0, 1200)}
|
|
7635
|
+
const argumentsText = !mustShowCompleteProposal && approval.arguments.length > 1200 ? `${approval.arguments.slice(0, 1200)}…` : approval.arguments;
|
|
6771
7636
|
return `[${approval.kind ?? "provider"}] ${approval.name}
|
|
6772
7637
|
${argumentsText}`;
|
|
6773
7638
|
};
|
|
@@ -6889,7 +7754,7 @@ var printTerminalResult = (result, harness, output, tracker) => {
|
|
|
6889
7754
|
`);
|
|
6890
7755
|
}
|
|
6891
7756
|
process.stderr.write(`
|
|
6892
|
-
run ${result.state.runId}
|
|
7757
|
+
run ${result.state.runId} · ${result.state.provider}/${result.state.modelId} · ${result.status} · ${result.steps.length} steps · ${harness.workspace.mutationAudit().length} mutations
|
|
6893
7758
|
`);
|
|
6894
7759
|
if (result.status === "waiting_approval") {
|
|
6895
7760
|
for (const approval of result.state.pendingApprovals) {
|
|
@@ -6919,11 +7784,11 @@ var orchestrationObserver = (json) => async (event) => {
|
|
|
6919
7784
|
return;
|
|
6920
7785
|
if (event.type === "subagent-start") {
|
|
6921
7786
|
process.stderr.write(`
|
|
6922
|
-
subagent start
|
|
7787
|
+
subagent start · ${event.childAgentId ?? event.toolName}
|
|
6923
7788
|
`);
|
|
6924
7789
|
} else if (event.type === "subagent-finish") {
|
|
6925
7790
|
process.stderr.write(`
|
|
6926
|
-
subagent finish
|
|
7791
|
+
subagent finish · ${event.childRun.agentId ?? event.childRun.toolName} · ${event.childRun.status}
|
|
6927
7792
|
`);
|
|
6928
7793
|
}
|
|
6929
7794
|
};
|
|
@@ -7189,9 +8054,9 @@ var chat = async (options) => {
|
|
|
7189
8054
|
const current = await refreshSession();
|
|
7190
8055
|
const latest = current.runs.at(-1);
|
|
7191
8056
|
const routeText = routes.size === 0 ? "default" : [...routes.values()].map((route) => `${route.profile}=${route.provider}:${route.model}`).join(", ");
|
|
7192
|
-
return `session ${current.sessionId}
|
|
8057
|
+
return `session ${current.sessionId} · ${harness.config.provider}/${harness.config.model}` + ` · ${messages.length} messages · routes ${routeText}` + `${latest ? ` · last ${latest.runId} (${latest.status})` : ""}`;
|
|
7193
8058
|
};
|
|
7194
|
-
process.stderr.write(`Zhivex Harness ${HARNESS_VERSION}
|
|
8059
|
+
process.stderr.write(`Zhivex Harness ${HARNESS_VERSION} · ${harness.config.provider}/${harness.config.model}
|
|
7195
8060
|
` + `session ${session.sessionId}
|
|
7196
8061
|
` + `Type /help for console commands.
|
|
7197
8062
|
`);
|
|
@@ -7206,9 +8071,9 @@ var chat = async (options) => {
|
|
|
7206
8071
|
break;
|
|
7207
8072
|
}
|
|
7208
8073
|
if (prompt === "/help") {
|
|
7209
|
-
process.stderr.write(`/provider [id]
|
|
7210
|
-
` + `/diff
|
|
7211
|
-
` + `/new [title]
|
|
8074
|
+
process.stderr.write(`/provider [id] · /model [id] · /route [role=provider[:model]] · /status
|
|
8075
|
+
` + `/diff · /review <task> · /resume <last|sessionId> · /compact
|
|
8076
|
+
` + `/new [title] · /rename <title> · /clear · /exit
|
|
7212
8077
|
`);
|
|
7213
8078
|
continue;
|
|
7214
8079
|
}
|
|
@@ -7487,7 +8352,7 @@ var parseNumericVersion = (version) => {
|
|
|
7487
8352
|
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
|
|
7488
8353
|
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;
|
|
7489
8354
|
};
|
|
7490
|
-
var
|
|
8355
|
+
var satisfiesEngine = (version, range) => {
|
|
7491
8356
|
const minimumMatch = /^>=(\d+\.\d+\.\d+)$/.exec(range.trim());
|
|
7492
8357
|
const actual = parseNumericVersion(version);
|
|
7493
8358
|
const minimum = minimumMatch ? parseNumericVersion(minimumMatch[1] ?? "") : undefined;
|
|
@@ -7519,36 +8384,28 @@ var inspectWorkspace = async (workspace) => {
|
|
|
7519
8384
|
};
|
|
7520
8385
|
var inspectGit = async (workspace) => {
|
|
7521
8386
|
try {
|
|
7522
|
-
const versionProcess =
|
|
7523
|
-
|
|
7524
|
-
|
|
8387
|
+
const versionProcess = await runPortableProcess(["git", "--version"], {
|
|
8388
|
+
timeoutMs: 15000,
|
|
8389
|
+
maxOutputCharacters: 4096,
|
|
7525
8390
|
stderr: "ignore"
|
|
7526
8391
|
});
|
|
7527
|
-
|
|
7528
|
-
new Response(versionProcess.stdout).text(),
|
|
7529
|
-
versionProcess.exited
|
|
7530
|
-
]);
|
|
7531
|
-
if (versionExitCode !== 0) {
|
|
8392
|
+
if (versionProcess.exitCode !== 0) {
|
|
7532
8393
|
return diagnostic("git", "warn", "Git is unavailable.", { installed: false });
|
|
7533
8394
|
}
|
|
7534
|
-
const repositoryProcess =
|
|
7535
|
-
env: {
|
|
7536
|
-
|
|
7537
|
-
|
|
8395
|
+
const repositoryProcess = await runPortableProcess(["git", "-C", workspace, "rev-parse", "--is-inside-work-tree"], {
|
|
8396
|
+
env: { GIT_TERMINAL_PROMPT: "0" },
|
|
8397
|
+
timeoutMs: 15000,
|
|
8398
|
+
maxOutputCharacters: 4096,
|
|
7538
8399
|
stderr: "ignore"
|
|
7539
8400
|
});
|
|
7540
|
-
const
|
|
7541
|
-
|
|
7542
|
-
repositoryProcess.exited
|
|
7543
|
-
]);
|
|
7544
|
-
const repository = repositoryExitCode === 0 && repositoryOutput.trim() === "true";
|
|
7545
|
-
return diagnostic("git", repository ? "pass" : "warn", repository ? "Git is installed and the workspace is a repository." : "Git is installed, but the workspace is not a repository.", { installed: true, repository, version: versionOutput.trim() });
|
|
8401
|
+
const repository = repositoryProcess.exitCode === 0 && repositoryProcess.stdout.trim() === "true";
|
|
8402
|
+
return diagnostic("git", repository ? "pass" : "warn", repository ? "Git is installed and the workspace is a repository." : "Git is installed, but the workspace is not a repository.", { installed: true, repository, version: versionProcess.stdout.trim() });
|
|
7546
8403
|
} catch {
|
|
7547
8404
|
return diagnostic("git", "warn", "Git is unavailable.", { installed: false });
|
|
7548
8405
|
}
|
|
7549
8406
|
};
|
|
7550
8407
|
var inspectScripts = async (workspace, allowedChecks) => {
|
|
7551
|
-
const packagePath =
|
|
8408
|
+
const packagePath = path10.join(workspace, "package.json");
|
|
7552
8409
|
try {
|
|
7553
8410
|
const packageStat = await stat4(packagePath);
|
|
7554
8411
|
if (!packageStat.isFile() || packageStat.size > 1024 * 1024) {
|
|
@@ -7561,7 +8418,15 @@ var inspectScripts = async (workspace, allowedChecks) => {
|
|
|
7561
8418
|
const scripts = packageJson.scripts && typeof packageJson.scripts === "object" ? packageJson.scripts : {};
|
|
7562
8419
|
const available = allowedChecks.filter((name) => typeof scripts[name] === "string");
|
|
7563
8420
|
const missing = allowedChecks.filter((name) => typeof scripts[name] !== "string");
|
|
7564
|
-
|
|
8421
|
+
const packageManager = await resolvePackageManager(workspace, packageJson);
|
|
8422
|
+
return diagnostic("scripts", available.length > 0 ? "pass" : "warn", available.length > 0 ? `Supported ${packageManager.manager} check scripts were found.` : `No supported ${packageManager.manager} check scripts were found.`, {
|
|
8423
|
+
packageJson: true,
|
|
8424
|
+
packageManager: packageManager.manager,
|
|
8425
|
+
packageManagerSource: packageManager.source,
|
|
8426
|
+
...packageManager.evidence ? { packageManagerEvidence: packageManager.evidence } : {},
|
|
8427
|
+
available,
|
|
8428
|
+
missing
|
|
8429
|
+
});
|
|
7565
8430
|
} catch (error) {
|
|
7566
8431
|
const code = error instanceof SyntaxError ? "INVALID_JSON" : error.code ?? "UNKNOWN";
|
|
7567
8432
|
return diagnostic("scripts", "warn", "package.json could not be inspected.", {
|
|
@@ -7572,17 +8437,17 @@ var inspectScripts = async (workspace, allowedChecks) => {
|
|
|
7572
8437
|
}
|
|
7573
8438
|
};
|
|
7574
8439
|
var isInsidePath2 = (root, candidate) => {
|
|
7575
|
-
const relative =
|
|
7576
|
-
return relative === "" || !relative.startsWith(`..${
|
|
8440
|
+
const relative = path10.relative(root, candidate);
|
|
8441
|
+
return relative === "" || !relative.startsWith(`..${path10.sep}`) && relative !== ".." && !path10.isAbsolute(relative);
|
|
7577
8442
|
};
|
|
7578
8443
|
var containsSymlink = async (candidate, trustedPrefix) => {
|
|
7579
|
-
const parsed =
|
|
7580
|
-
const parts = trustedPrefix ?
|
|
7581
|
-
let current = trustedPrefix ??
|
|
8444
|
+
const parsed = path10.parse(candidate);
|
|
8445
|
+
const parts = trustedPrefix ? path10.relative(trustedPrefix, candidate).split(path10.sep).filter(Boolean) : candidate.slice(parsed.root.length).split(path10.sep).filter(Boolean).slice(1);
|
|
8446
|
+
let current = trustedPrefix ?? path10.join(parsed.root, candidate.slice(parsed.root.length).split(path10.sep).filter(Boolean)[0] ?? "");
|
|
7582
8447
|
for (const part of parts) {
|
|
7583
|
-
current =
|
|
8448
|
+
current = path10.join(current, part);
|
|
7584
8449
|
try {
|
|
7585
|
-
if ((await
|
|
8450
|
+
if ((await lstat8(current)).isSymbolicLink()) {
|
|
7586
8451
|
return true;
|
|
7587
8452
|
}
|
|
7588
8453
|
} catch (error) {
|
|
@@ -7603,7 +8468,7 @@ var nearestExistingPath = async (candidate) => {
|
|
|
7603
8468
|
if (error.code !== "ENOENT") {
|
|
7604
8469
|
throw error;
|
|
7605
8470
|
}
|
|
7606
|
-
const parent =
|
|
8471
|
+
const parent = path10.dirname(current);
|
|
7607
8472
|
if (parent === current) {
|
|
7608
8473
|
throw error;
|
|
7609
8474
|
}
|
|
@@ -7619,7 +8484,7 @@ var inspectStateDirectory = async (workspace, stateDirectory) => {
|
|
|
7619
8484
|
});
|
|
7620
8485
|
}
|
|
7621
8486
|
if (isInsidePath2(workspace, stateDirectory)) {
|
|
7622
|
-
const segments =
|
|
8487
|
+
const segments = path10.relative(workspace, stateDirectory).split(path10.sep).filter(Boolean);
|
|
7623
8488
|
const sensitiveSegment = segments.find(isSensitiveStateSegment);
|
|
7624
8489
|
if (sensitiveSegment) {
|
|
7625
8490
|
return diagnostic("state-directory", "fail", "State directory is inside a sensitive workspace path.", {
|
|
@@ -7666,9 +8531,9 @@ var inspectOperationsStore = async (stateDirectory, storeBackend) => {
|
|
|
7666
8531
|
migrationAvailable: true
|
|
7667
8532
|
});
|
|
7668
8533
|
}
|
|
7669
|
-
const databasePath =
|
|
8534
|
+
const databasePath = path10.join(stateDirectory, "operations.sqlite");
|
|
7670
8535
|
try {
|
|
7671
|
-
const entry = await
|
|
8536
|
+
const entry = await lstat8(databasePath);
|
|
7672
8537
|
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
7673
8538
|
return diagnostic("operations-store", "fail", "SQLite state path is not a regular file.", {
|
|
7674
8539
|
backend: storeBackend,
|
|
@@ -7796,11 +8661,11 @@ var createDoctorReport = async (options = {}, context = {}) => {
|
|
|
7796
8661
|
...env.ZHIVEX_HARNESS_MCP_CONFIG ? { mcpConfigPath: env.ZHIVEX_HARNESS_MCP_CONFIG } : {},
|
|
7797
8662
|
...options
|
|
7798
8663
|
});
|
|
7799
|
-
const
|
|
8664
|
+
const runtime = context.nodeVersion ? { name: "node", version: context.nodeVersion, required: NODE_ENGINE_RANGE } : context.bunVersion ? { name: "bun", version: context.bunVersion, required: BUN_ENGINE_RANGE } : process.versions.bun ? { name: "bun", version: process.versions.bun, required: BUN_ENGINE_RANGE } : { name: "node", version: process.versions.node, required: NODE_ENGINE_RANGE };
|
|
7800
8665
|
const providers = providerAvailability(env);
|
|
7801
8666
|
const selectedProvider = providers.find((provider) => provider.id === config.provider);
|
|
7802
8667
|
const checks = [];
|
|
7803
|
-
checks.push(diagnostic(
|
|
8668
|
+
checks.push(diagnostic(runtime.name, satisfiesEngine(runtime.version, runtime.required) ? "pass" : "fail", satisfiesEngine(runtime.version, runtime.required) ? `${runtime.name === "node" ? "Node.js" : "Bun"} satisfies the package engine requirement.` : `${runtime.name === "node" ? "Node.js" : "Bun"} does not satisfy the package engine requirement.`, { version: runtime.version, required: runtime.required, primary: runtime.name === "node" }));
|
|
7804
8669
|
checks.push(await inspectWorkspace(config.workspace));
|
|
7805
8670
|
checks.push(await inspectGit(config.workspace));
|
|
7806
8671
|
checks.push(await inspectScripts(config.workspace, config.allowedChecks));
|
|
@@ -7860,7 +8725,7 @@ var createDoctorReport = async (options = {}, context = {}) => {
|
|
|
7860
8725
|
};
|
|
7861
8726
|
};
|
|
7862
8727
|
var formatDoctorReport = (report) => {
|
|
7863
|
-
const symbols = { pass: "
|
|
8728
|
+
const symbols = { pass: "✓", warn: "!", fail: "✗" };
|
|
7864
8729
|
const lines = [
|
|
7865
8730
|
`Zhivex Harness doctor v${report.harnessVersion}`,
|
|
7866
8731
|
...report.checks.map((check) => `${symbols[check.status]} ${check.id}: ${check.message}`),
|
|
@@ -7898,7 +8763,7 @@ var printRunsDocument = (document, json) => {
|
|
|
7898
8763
|
}
|
|
7899
8764
|
if (record.kind === "run-inspection") {
|
|
7900
8765
|
const run = record.run;
|
|
7901
|
-
process.stdout.write(`${String(run.runId)}
|
|
8766
|
+
process.stdout.write(`${String(run.runId)} · ${String(run.status)} · ${String(run.provider)}/${String(run.model)} · ${String(run.steps)} steps · ${String(run.toolCalls)} tools
|
|
7902
8767
|
`);
|
|
7903
8768
|
return;
|
|
7904
8769
|
}
|
|
@@ -7971,14 +8836,14 @@ var printSessionDocument = (document, json) => {
|
|
|
7971
8836
|
if (record.kind === "session-list") {
|
|
7972
8837
|
for (const session2 of record.sessions ?? []) {
|
|
7973
8838
|
const latest = session2.runs.at(-1);
|
|
7974
|
-
process.stdout.write(`${session2.sessionId} ${session2.title ?? "(untitled)"}
|
|
8839
|
+
process.stdout.write(`${session2.sessionId} ${session2.title ?? "(untitled)"} · ${session2.runs.length} runs` + `${latest ? ` · ${latest.provider}/${latest.model} · ${latest.status}` : ""}
|
|
7975
8840
|
`);
|
|
7976
8841
|
}
|
|
7977
8842
|
return;
|
|
7978
8843
|
}
|
|
7979
8844
|
const session = record.session;
|
|
7980
8845
|
if (session) {
|
|
7981
|
-
process.stdout.write(`${session.sessionId}
|
|
8846
|
+
process.stdout.write(`${session.sessionId} · ${session.title ?? "(untitled)"} · ${session.runs.length} runs` + `${session.archivedAt ? " · archived" : ""}
|
|
7982
8847
|
`);
|
|
7983
8848
|
return;
|
|
7984
8849
|
}
|
|
@@ -8049,9 +8914,9 @@ var cliExitCodeForError = (error) => error instanceof CliUsageError ? CLI_EXIT_C
|
|
|
8049
8914
|
var MAX_CHANGE_ENVELOPE_JSON_BYTES = 4 * 1024 * 1024;
|
|
8050
8915
|
var MAX_CHANGE_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
8051
8916
|
var readStableCliArtifact = async (filePath, maxBytes) => {
|
|
8052
|
-
const absolute =
|
|
8917
|
+
const absolute = path10.resolve(process.cwd(), filePath);
|
|
8053
8918
|
try {
|
|
8054
|
-
const before = await
|
|
8919
|
+
const before = await lstat8(absolute);
|
|
8055
8920
|
if (before.isSymbolicLink() || !before.isFile()) {
|
|
8056
8921
|
throw new CliUsageError(`Change artifact must be a regular non-symlink file: ${filePath}`);
|
|
8057
8922
|
}
|
|
@@ -8059,7 +8924,7 @@ var readStableCliArtifact = async (filePath, maxBytes) => {
|
|
|
8059
8924
|
throw new CliUsageError(`Change artifact exceeds the ${maxBytes}-byte limit: ${filePath}`);
|
|
8060
8925
|
}
|
|
8061
8926
|
const contents = await readFile4(absolute);
|
|
8062
|
-
const after = await
|
|
8927
|
+
const after = await lstat8(absolute);
|
|
8063
8928
|
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
|
|
8064
8929
|
throw new CliUsageError(`Change artifact changed while it was being read: ${filePath}`);
|
|
8065
8930
|
}
|
|
@@ -8173,7 +9038,16 @@ var main = async (argv = process.argv.slice(2)) => {
|
|
|
8173
9038
|
await reviewOnce(options);
|
|
8174
9039
|
}
|
|
8175
9040
|
};
|
|
8176
|
-
|
|
9041
|
+
var isMainModule = (() => {
|
|
9042
|
+
if (!process.argv[1])
|
|
9043
|
+
return false;
|
|
9044
|
+
try {
|
|
9045
|
+
return realpathSync2(process.argv[1]) === realpathSync2(fileURLToPath(import.meta.url));
|
|
9046
|
+
} catch {
|
|
9047
|
+
return false;
|
|
9048
|
+
}
|
|
9049
|
+
})();
|
|
9050
|
+
if (isMainModule) {
|
|
8177
9051
|
main().catch((error) => {
|
|
8178
9052
|
const message = error instanceof Error ? error.message : String(error);
|
|
8179
9053
|
process.stderr.write(`Error: ${message}
|
|
@@ -8182,22 +9056,22 @@ if (import.meta.main) {
|
|
|
8182
9056
|
});
|
|
8183
9057
|
}
|
|
8184
9058
|
export {
|
|
8185
|
-
|
|
8186
|
-
summarizeApproval,
|
|
8187
|
-
runResultDocument,
|
|
8188
|
-
resumeCommand,
|
|
8189
|
-
readHarnessResumeRoutes,
|
|
8190
|
-
readHarnessResumeConfig,
|
|
8191
|
-
providersDocument,
|
|
8192
|
-
parseCliArgs,
|
|
8193
|
-
main,
|
|
8194
|
-
formatDoctorReport,
|
|
8195
|
-
createHarnessResumeMetadata,
|
|
8196
|
-
createDoctorReport,
|
|
8197
|
-
cliExitCodeForError,
|
|
8198
|
-
HARNESS_RESUME_METADATA_SCHEMA_VERSION,
|
|
8199
|
-
HARNESS_RESUME_METADATA_KEY,
|
|
8200
|
-
CliUsageError,
|
|
9059
|
+
CLI_EXIT_CODES,
|
|
8201
9060
|
CLI_JSON_SCHEMA_VERSION,
|
|
8202
|
-
|
|
9061
|
+
CliUsageError,
|
|
9062
|
+
HARNESS_RESUME_METADATA_KEY,
|
|
9063
|
+
HARNESS_RESUME_METADATA_SCHEMA_VERSION,
|
|
9064
|
+
cliExitCodeForError,
|
|
9065
|
+
createDoctorReport,
|
|
9066
|
+
createHarnessResumeMetadata,
|
|
9067
|
+
formatDoctorReport,
|
|
9068
|
+
main,
|
|
9069
|
+
parseCliArgs,
|
|
9070
|
+
providersDocument,
|
|
9071
|
+
readHarnessResumeConfig,
|
|
9072
|
+
readHarnessResumeRoutes,
|
|
9073
|
+
resumeCommand,
|
|
9074
|
+
runResultDocument,
|
|
9075
|
+
summarizeApproval,
|
|
9076
|
+
withTemporaryHarnessProfiles
|
|
8203
9077
|
};
|