@zhivex-ai/harness 0.8.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 +61 -0
- package/README.md +59 -15
- package/ROADMAP.md +61 -13
- package/SECURITY.md +7 -1
- package/SUPPORT.md +3 -2
- package/dist/change-envelope.d.ts +271 -0
- package/dist/cli-stream.d.ts +14 -14
- package/dist/cli.d.ts +10 -2
- package/dist/cli.js +2613 -447
- package/dist/config.d.ts +3 -3
- package/dist/execution-environment.d.ts +79 -5
- package/dist/harness.d.ts +70 -17
- package/dist/hostile-repository-demo.js +1912 -385
- package/dist/index.d.ts +7 -3
- package/dist/index.js +3022 -490
- 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 +73 -5
- package/dist/zhx.js +2613 -447
- package/docs/CHANGE_ENVELOPES.md +116 -0
- package/docs/CLI.md +16 -6
- package/docs/DURABLE_OPERATIONS.md +17 -3
- package/docs/EXECUTION_ENVIRONMENTS.md +29 -11
- 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 +6 -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/examples/change-envelope-input.json +26 -0
- package/examples/change.patch +6 -0
- package/package.json +23 -6
package/dist/cli.js
CHANGED
|
@@ -1,23 +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
|
|
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
|
+
import { z as z5 } from "zod";
|
|
10
11
|
import {
|
|
11
12
|
getAgentBudgetStatus as getAgentBudgetStatus3
|
|
12
13
|
} from "@zhivex-ai/agents";
|
|
13
14
|
|
|
14
15
|
// src/config.ts
|
|
15
|
-
import { createHash as createHash2 } from "crypto";
|
|
16
|
-
import { realpathSync } from "fs";
|
|
17
|
-
import path from "path";
|
|
16
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
17
|
+
import { realpathSync } from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
18
19
|
|
|
19
20
|
// src/providers.ts
|
|
20
|
-
import { createHash } from "crypto";
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
21
22
|
import { createGemini } from "@zhivex-ai/gemini";
|
|
22
23
|
import { createMeta } from "@zhivex-ai/meta";
|
|
23
24
|
import { createOpenAI } from "@zhivex-ai/openai";
|
|
@@ -371,11 +372,11 @@ var HARNESS_CONFIG_SCHEMA_VERSION = 4;
|
|
|
371
372
|
var HARNESS_STORE_BACKENDS = ["sqlite", "file"];
|
|
372
373
|
var HARNESS_EXECUTION_BACKENDS = ["none", "oci"];
|
|
373
374
|
var HARNESS_OCI_RUNTIMES = ["docker", "podman"];
|
|
374
|
-
var HARNESS_EXECUTION_POLICY_VERSION = "2026-08-
|
|
375
|
+
var HARNESS_EXECUTION_POLICY_VERSION = "2026-08-21-v3";
|
|
375
376
|
var DEFAULT_OCI_EXECUTION = {
|
|
376
377
|
runtime: "docker",
|
|
377
|
-
image: "
|
|
378
|
-
allowedCommands: ["
|
|
378
|
+
image: "node:24-bookworm-slim",
|
|
379
|
+
allowedCommands: ["node", "npm"],
|
|
379
380
|
maxProcessRuntimeMs: 120000,
|
|
380
381
|
maxProcessOutputBytes: 20000,
|
|
381
382
|
maxMemoryMb: 1024,
|
|
@@ -500,8 +501,8 @@ var resolveOciAllowedCommands = (configured) => {
|
|
|
500
501
|
if (invalid) {
|
|
501
502
|
throw new Error(`Invalid OCI command name: ${invalid}. Use a bare executable name without a path.`);
|
|
502
503
|
}
|
|
503
|
-
if (!commands.
|
|
504
|
-
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.");
|
|
505
506
|
}
|
|
506
507
|
return commands;
|
|
507
508
|
};
|
|
@@ -656,7 +657,7 @@ var createProviderModel = (config, env = process.env, registry = DEFAULT_PROVIDE
|
|
|
656
657
|
var providerAvailability = (env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => registry.availability(env);
|
|
657
658
|
|
|
658
659
|
// src/harness.ts
|
|
659
|
-
import { createHash as createHash7 } from "crypto";
|
|
660
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
660
661
|
import {
|
|
661
662
|
Agent,
|
|
662
663
|
applySafetyPolicyToAgent as applySafetyPolicyToAgent2,
|
|
@@ -726,8 +727,8 @@ var selectHarnessModel = (candidates, required) => {
|
|
|
726
727
|
};
|
|
727
728
|
|
|
728
729
|
// src/edit-contracts.ts
|
|
729
|
-
import { createHash as createHash3 } from "crypto";
|
|
730
|
-
import path2 from "path";
|
|
730
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
731
|
+
import path2 from "node:path";
|
|
731
732
|
import { z } from "zod";
|
|
732
733
|
var EDIT_CONTRACT_SCHEMA_VERSION = 1;
|
|
733
734
|
var EDIT_DIGEST_ALGORITHM = "sha256";
|
|
@@ -861,27 +862,278 @@ var editContractDocument = (kind, result) => ({
|
|
|
861
862
|
});
|
|
862
863
|
|
|
863
864
|
// src/workspace.ts
|
|
864
|
-
import { createHash as createHash4, randomUUID } from "crypto";
|
|
865
|
-
import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "fs/promises";
|
|
866
|
-
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
|
|
867
1115
|
var HARD_IGNORES = new Set([".git", ".next", ".turbo", ".zhivex-harness", "coverage", "dist", "node_modules"]);
|
|
868
1116
|
var IGNORE_FILES = [".gitignore", ".zhivex-harnessignore"];
|
|
869
1117
|
var DEFAULT_CHECKS = ["test", "typecheck", "lint", "build"];
|
|
870
1118
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
871
1119
|
var MAX_TOOL_OUTPUT = 20000;
|
|
1120
|
+
var MAX_READ_BATCH_FILES = 20;
|
|
1121
|
+
var MAX_READ_BATCH_BYTES = 2 * MAX_FILE_BYTES;
|
|
1122
|
+
var MAX_SEARCH_MANY_QUERIES = 10;
|
|
1123
|
+
var MAX_SEARCH_MANY_MATCHES = 500;
|
|
872
1124
|
var isSensitiveName = (name) => {
|
|
873
1125
|
const normalized = name.toLocaleLowerCase();
|
|
874
1126
|
return normalized === ".env" || normalized.startsWith(".env.") && normalized !== ".env.example" || normalized === ".npmrc" || normalized === "id_rsa" || normalized === "id_ed25519" || normalized.endsWith(".key") || normalized.endsWith(".pem") || normalized.endsWith(".p12") || normalized.endsWith(".pfx");
|
|
875
1127
|
};
|
|
876
1128
|
var isHardIgnored = (relativePath) => relativePath.split("/").some((segment) => HARD_IGNORES.has(segment.toLocaleLowerCase()) || isSensitiveName(segment));
|
|
877
1129
|
var isInside = (root, candidate) => {
|
|
878
|
-
const relative =
|
|
879
|
-
return relative === "" || !relative.startsWith(`..${
|
|
1130
|
+
const relative = path4.relative(root, candidate);
|
|
1131
|
+
return relative === "" || !relative.startsWith(`..${path4.sep}`) && relative !== ".." && !path4.isAbsolute(relative);
|
|
880
1132
|
};
|
|
881
|
-
var wirePath = (value) => value.split(
|
|
1133
|
+
var wirePath = (value) => value.split(path4.sep).join("/");
|
|
882
1134
|
var digestBytes = (value) => `sha256:${createHash4("sha256").update(value).digest("hex")}`;
|
|
883
1135
|
var truncate = (value, maxLength = MAX_TOOL_OUTPUT) => value.length <= maxLength ? value : `${value.slice(0, maxLength)}
|
|
884
|
-
|
|
1136
|
+
… output truncated (${value.length - maxLength} characters omitted)`;
|
|
885
1137
|
var cursorEncode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
886
1138
|
var cursorDecode = (value) => {
|
|
887
1139
|
try {
|
|
@@ -890,6 +1142,30 @@ var cursorDecode = (value) => {
|
|
|
890
1142
|
throw new Error("The pagination cursor is invalid.");
|
|
891
1143
|
}
|
|
892
1144
|
};
|
|
1145
|
+
var firstPathAtLeast = (files, target) => {
|
|
1146
|
+
let low = 0;
|
|
1147
|
+
let high = files.length;
|
|
1148
|
+
while (low < high) {
|
|
1149
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
1150
|
+
if ((files[middle]?.path ?? "") < target)
|
|
1151
|
+
low = middle + 1;
|
|
1152
|
+
else
|
|
1153
|
+
high = middle;
|
|
1154
|
+
}
|
|
1155
|
+
return low;
|
|
1156
|
+
};
|
|
1157
|
+
var firstPathAfter = (files, target) => {
|
|
1158
|
+
let low = 0;
|
|
1159
|
+
let high = files.length;
|
|
1160
|
+
while (low < high) {
|
|
1161
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
1162
|
+
if ((files[middle]?.path ?? "") <= target)
|
|
1163
|
+
low = middle + 1;
|
|
1164
|
+
else
|
|
1165
|
+
high = middle;
|
|
1166
|
+
}
|
|
1167
|
+
return low;
|
|
1168
|
+
};
|
|
893
1169
|
var escapeRegex = (value) => value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
894
1170
|
var globRegex = (pattern) => {
|
|
895
1171
|
let source = "";
|
|
@@ -958,34 +1234,22 @@ var isIgnoredByRules = (relativePath, isDirectory, rules) => {
|
|
|
958
1234
|
return ignored;
|
|
959
1235
|
};
|
|
960
1236
|
var spawnBounded = async (command, cwd, timeoutMs = 120000) => {
|
|
961
|
-
|
|
962
|
-
const env = Object.fromEntries(allowed.flatMap((key) => process.env[key] ? [[key, process.env[key]]] : []));
|
|
963
|
-
const child = Bun.spawn(command, { cwd, env, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
964
|
-
let timedOut = false;
|
|
965
|
-
const timeout = setTimeout(() => {
|
|
966
|
-
timedOut = true;
|
|
967
|
-
child.kill();
|
|
968
|
-
}, timeoutMs);
|
|
969
|
-
try {
|
|
970
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
971
|
-
new Response(child.stdout).text(),
|
|
972
|
-
new Response(child.stderr).text(),
|
|
973
|
-
child.exited
|
|
974
|
-
]);
|
|
975
|
-
return { command, exitCode, stdout: truncate(stdout), stderr: truncate(stderr), timedOut };
|
|
976
|
-
} finally {
|
|
977
|
-
clearTimeout(timeout);
|
|
978
|
-
}
|
|
1237
|
+
return await runPortableProcess(command, { cwd, timeoutMs });
|
|
979
1238
|
};
|
|
980
1239
|
|
|
981
1240
|
class Workspace {
|
|
982
1241
|
root;
|
|
983
1242
|
auditEntries = [];
|
|
1243
|
+
indexIdentity = randomUUID();
|
|
1244
|
+
indexRevision = 0;
|
|
1245
|
+
workspaceIndex;
|
|
1246
|
+
indexBuild;
|
|
1247
|
+
indexMetrics = { builds: 0, reuses: 0, stableFileReads: 0 };
|
|
984
1248
|
constructor(root) {
|
|
985
1249
|
this.root = root;
|
|
986
1250
|
}
|
|
987
1251
|
static async open(root) {
|
|
988
|
-
const resolved = await realpath(
|
|
1252
|
+
const resolved = await realpath(path4.resolve(root));
|
|
989
1253
|
if (!(await stat(resolved)).isDirectory())
|
|
990
1254
|
throw new Error(`The workspace is not a directory: ${root}`);
|
|
991
1255
|
return new Workspace(resolved);
|
|
@@ -993,10 +1257,10 @@ class Workspace {
|
|
|
993
1257
|
lexicalPath(relativePath) {
|
|
994
1258
|
if (!relativePath || relativePath.includes("\x00"))
|
|
995
1259
|
throw new Error("The path must be a valid relative path.");
|
|
996
|
-
const candidate =
|
|
1260
|
+
const candidate = path4.resolve(this.root, relativePath);
|
|
997
1261
|
if (!isInside(this.root, candidate))
|
|
998
1262
|
throw new Error(`The path escapes the workspace: ${relativePath}`);
|
|
999
|
-
const relative = wirePath(
|
|
1263
|
+
const relative = wirePath(path4.relative(this.root, candidate));
|
|
1000
1264
|
if (relative && isHardIgnored(relative)) {
|
|
1001
1265
|
const blocked = relative.split("/").find((segment) => HARD_IGNORES.has(segment.toLocaleLowerCase()) || isSensitiveName(segment));
|
|
1002
1266
|
throw new Error(`The path is protected by the harness policy: ${blocked}`);
|
|
@@ -1007,13 +1271,13 @@ class Workspace {
|
|
|
1007
1271
|
const candidate = this.lexicalPath(relativePath);
|
|
1008
1272
|
if (candidate === this.root && options.requireFile)
|
|
1009
1273
|
throw new Error("The workspace root is not a regular file.");
|
|
1010
|
-
const segments =
|
|
1274
|
+
const segments = path4.relative(this.root, candidate).split(path4.sep).filter(Boolean);
|
|
1011
1275
|
let current = this.root;
|
|
1012
1276
|
for (let index = 0;index < segments.length; index += 1) {
|
|
1013
|
-
current =
|
|
1277
|
+
current = path4.join(current, segments[index]);
|
|
1014
1278
|
let entry;
|
|
1015
1279
|
try {
|
|
1016
|
-
entry = await
|
|
1280
|
+
entry = await lstat2(current);
|
|
1017
1281
|
} catch (error) {
|
|
1018
1282
|
if (options.allowMissing && error.code === "ENOENT") {
|
|
1019
1283
|
return { path: candidate, exists: false };
|
|
@@ -1032,33 +1296,60 @@ class Workspace {
|
|
|
1032
1296
|
return { path: candidate, exists: true };
|
|
1033
1297
|
}
|
|
1034
1298
|
async readStableFile(relativePath, allowBinary = true) {
|
|
1299
|
+
this.indexMetrics.stableFileReads += 1;
|
|
1035
1300
|
const safe = await this.safePath(relativePath, { requireFile: true });
|
|
1036
|
-
const before = await
|
|
1301
|
+
const before = await lstat2(safe.path);
|
|
1037
1302
|
if (before.size > MAX_FILE_BYTES)
|
|
1038
1303
|
throw new Error(`The file exceeds the ${MAX_FILE_BYTES}-byte limit.`);
|
|
1039
1304
|
const contents = await readFile(safe.path);
|
|
1040
|
-
const after = await
|
|
1305
|
+
const after = await lstat2(safe.path);
|
|
1041
1306
|
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
|
|
1042
1307
|
throw new Error(`The file changed while it was being read: ${relativePath}`);
|
|
1043
1308
|
}
|
|
1044
1309
|
if (!allowBinary && contents.includes(0))
|
|
1045
1310
|
throw new Error("The file appears to be binary and cannot be read as text.");
|
|
1046
1311
|
return {
|
|
1047
|
-
path: wirePath(
|
|
1312
|
+
path: wirePath(path4.relative(this.root, safe.path)),
|
|
1048
1313
|
absolutePath: safe.path,
|
|
1049
1314
|
contents,
|
|
1050
1315
|
digest: digestBytes(contents),
|
|
1051
1316
|
mode: before.mode & 511
|
|
1052
1317
|
};
|
|
1053
1318
|
}
|
|
1054
|
-
|
|
1319
|
+
fingerprint(relativePath, kind, entry, digest2) {
|
|
1320
|
+
return {
|
|
1321
|
+
path: relativePath || ".",
|
|
1322
|
+
kind,
|
|
1323
|
+
dev: entry.dev,
|
|
1324
|
+
ino: entry.ino,
|
|
1325
|
+
size: entry.size,
|
|
1326
|
+
mtimeMs: entry.mtimeMs,
|
|
1327
|
+
ctimeMs: entry.ctimeMs,
|
|
1328
|
+
...digest2 ? { digest: digest2 } : {}
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
sameFingerprint(entry, fingerprint) {
|
|
1332
|
+
return entry.dev === fingerprint.dev && entry.ino === fingerprint.ino && entry.size === fingerprint.size && entry.mtimeMs === fingerprint.mtimeMs && entry.ctimeMs === fingerprint.ctimeMs;
|
|
1333
|
+
}
|
|
1334
|
+
async rulesForDirectory(directory, base, inherited, ignoreFiles) {
|
|
1055
1335
|
const rules = [...inherited];
|
|
1056
1336
|
for (const filename of IGNORE_FILES) {
|
|
1057
1337
|
try {
|
|
1058
|
-
const ignorePath =
|
|
1059
|
-
const
|
|
1060
|
-
if (
|
|
1061
|
-
|
|
1338
|
+
const ignorePath = path4.join(directory, filename);
|
|
1339
|
+
const before = await lstat2(ignorePath);
|
|
1340
|
+
if (before.isSymbolicLink() || !before.isFile())
|
|
1341
|
+
continue;
|
|
1342
|
+
if (before.size <= MAX_FILE_BYTES) {
|
|
1343
|
+
const contents = await readFile(ignorePath);
|
|
1344
|
+
const after = await lstat2(ignorePath);
|
|
1345
|
+
if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, this.fingerprint("", "ignore-file", before))) {
|
|
1346
|
+
throw new Error(`The ignore file changed while the workspace was being indexed: ${wirePath(path4.relative(this.root, ignorePath))}`);
|
|
1347
|
+
}
|
|
1348
|
+
const digest2 = digestBytes(contents);
|
|
1349
|
+
ignoreFiles.push(this.fingerprint(wirePath(path4.relative(this.root, ignorePath)), "ignore-file", after, digest2));
|
|
1350
|
+
rules.push(...parseIgnoreRules(contents.toString("utf8"), base));
|
|
1351
|
+
} else {
|
|
1352
|
+
ignoreFiles.push(this.fingerprint(wirePath(path4.relative(this.root, ignorePath)), "ignore-file", before));
|
|
1062
1353
|
}
|
|
1063
1354
|
} catch (error) {
|
|
1064
1355
|
if (error.code !== "ENOENT")
|
|
@@ -1067,65 +1358,189 @@ class Workspace {
|
|
|
1067
1358
|
}
|
|
1068
1359
|
return rules;
|
|
1069
1360
|
}
|
|
1070
|
-
async collectFiles() {
|
|
1361
|
+
async collectFiles(version) {
|
|
1071
1362
|
const files = [];
|
|
1363
|
+
const directories = [];
|
|
1364
|
+
const ignoreFiles = [];
|
|
1072
1365
|
const walk = async (directory, base, inherited) => {
|
|
1073
|
-
const
|
|
1366
|
+
const directoryEntry = await lstat2(directory);
|
|
1367
|
+
if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
|
|
1368
|
+
throw new Error(`A workspace directory changed while it was being indexed: ${base || "."}`);
|
|
1369
|
+
}
|
|
1370
|
+
directories.push(this.fingerprint(base, "directory", directoryEntry));
|
|
1371
|
+
const rules = await this.rulesForDirectory(directory, base, inherited, ignoreFiles);
|
|
1074
1372
|
const entries = (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
1075
1373
|
for (const entry of entries) {
|
|
1076
1374
|
const relative = base ? `${base}/${entry.name}` : entry.name;
|
|
1077
1375
|
if (isHardIgnored(relative) || entry.isSymbolicLink() || isIgnoredByRules(relative, entry.isDirectory(), rules))
|
|
1078
1376
|
continue;
|
|
1079
|
-
const absolute =
|
|
1377
|
+
const absolute = path4.join(directory, entry.name);
|
|
1080
1378
|
if (entry.isDirectory())
|
|
1081
1379
|
await walk(absolute, relative, rules);
|
|
1082
1380
|
else if (entry.isFile())
|
|
1083
|
-
files.push({ path: relative
|
|
1381
|
+
files.push({ path: relative });
|
|
1084
1382
|
}
|
|
1085
1383
|
};
|
|
1086
1384
|
await walk(this.root, "", []);
|
|
1087
|
-
|
|
1385
|
+
files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
1386
|
+
return { version, files, directories, ignoreFiles };
|
|
1387
|
+
}
|
|
1388
|
+
async isWorkspaceIndexFresh(index) {
|
|
1389
|
+
try {
|
|
1390
|
+
for (const fingerprint of index.directories) {
|
|
1391
|
+
const absolute = fingerprint.path === "." ? this.root : path4.join(this.root, fingerprint.path);
|
|
1392
|
+
const entry = await lstat2(absolute);
|
|
1393
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() || !this.sameFingerprint(entry, fingerprint))
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
for (const fingerprint of index.ignoreFiles) {
|
|
1397
|
+
const absolute = path4.join(this.root, fingerprint.path);
|
|
1398
|
+
const before = await lstat2(absolute);
|
|
1399
|
+
if (before.isSymbolicLink() || !before.isFile() || !this.sameFingerprint(before, fingerprint))
|
|
1400
|
+
return false;
|
|
1401
|
+
if (fingerprint.digest) {
|
|
1402
|
+
const contents = await readFile(absolute);
|
|
1403
|
+
const after = await lstat2(absolute);
|
|
1404
|
+
if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, fingerprint) || digestBytes(contents) !== fingerprint.digest)
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return true;
|
|
1409
|
+
} catch {
|
|
1410
|
+
return false;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
invalidateWorkspaceIndex() {
|
|
1414
|
+
this.indexRevision += 1;
|
|
1415
|
+
this.workspaceIndex = undefined;
|
|
1416
|
+
}
|
|
1417
|
+
async buildWorkspaceIndex(revision) {
|
|
1418
|
+
const version = `${this.indexIdentity}:${revision}`;
|
|
1419
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
1420
|
+
try {
|
|
1421
|
+
const index = await this.collectFiles(version);
|
|
1422
|
+
if (await this.isWorkspaceIndexFresh(index))
|
|
1423
|
+
return index;
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
const code = error.code;
|
|
1426
|
+
const changedDuringIndexing = error instanceof Error && error.message.includes("while the workspace was being indexed");
|
|
1427
|
+
if (attempt === 2 || code !== "ENOENT" && !changedDuringIndexing)
|
|
1428
|
+
throw error;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
throw new Error("The workspace kept changing while its file index was being built.");
|
|
1432
|
+
}
|
|
1433
|
+
async getWorkspaceIndex() {
|
|
1434
|
+
const cached = this.workspaceIndex;
|
|
1435
|
+
if (cached) {
|
|
1436
|
+
if (await this.isWorkspaceIndexFresh(cached)) {
|
|
1437
|
+
this.indexMetrics.reuses += 1;
|
|
1438
|
+
return cached;
|
|
1439
|
+
}
|
|
1440
|
+
this.invalidateWorkspaceIndex();
|
|
1441
|
+
}
|
|
1442
|
+
const revision = this.indexRevision;
|
|
1443
|
+
if (!this.indexBuild || this.indexBuild.revision !== revision) {
|
|
1444
|
+
const promise = this.buildWorkspaceIndex(revision);
|
|
1445
|
+
this.indexBuild = { revision, promise };
|
|
1446
|
+
}
|
|
1447
|
+
const build = this.indexBuild;
|
|
1448
|
+
let index;
|
|
1449
|
+
try {
|
|
1450
|
+
index = await build.promise;
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
if (this.indexBuild === build)
|
|
1453
|
+
this.indexBuild = undefined;
|
|
1454
|
+
throw error;
|
|
1455
|
+
}
|
|
1456
|
+
if (revision !== this.indexRevision)
|
|
1457
|
+
return this.getWorkspaceIndex();
|
|
1458
|
+
if (this.indexBuild === build)
|
|
1459
|
+
this.indexBuild = undefined;
|
|
1460
|
+
if (this.workspaceIndex?.version !== index.version) {
|
|
1461
|
+
this.workspaceIndex = index;
|
|
1462
|
+
this.indexMetrics.builds += 1;
|
|
1463
|
+
}
|
|
1464
|
+
return this.workspaceIndex;
|
|
1465
|
+
}
|
|
1466
|
+
workspaceIndexDiagnostics() {
|
|
1467
|
+
const index = this.workspaceIndex;
|
|
1468
|
+
return {
|
|
1469
|
+
...this.indexMetrics,
|
|
1470
|
+
...index ? { version: index.version } : {},
|
|
1471
|
+
files: index?.files.length ?? 0,
|
|
1472
|
+
directories: index?.directories.length ?? 0
|
|
1473
|
+
};
|
|
1088
1474
|
}
|
|
1089
1475
|
async listFiles(relativePath = ".", options = {}) {
|
|
1090
1476
|
const start = await this.safePath(relativePath);
|
|
1091
|
-
if (!(await
|
|
1477
|
+
if (!(await lstat2(start.path)).isDirectory())
|
|
1092
1478
|
throw new Error("list_files requires a directory.");
|
|
1093
|
-
const requestPath = wirePath(
|
|
1479
|
+
const requestPath = wirePath(path4.relative(this.root, start.path)) || ".";
|
|
1094
1480
|
const input = typeof options === "number" ? { limit: options } : options;
|
|
1095
1481
|
const limit = input.limit ?? 200;
|
|
1482
|
+
const includeDigests = input.includeDigests ?? true;
|
|
1096
1483
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 5000)
|
|
1097
1484
|
throw new Error("limit must be between 1 and 5000.");
|
|
1098
1485
|
let after = "";
|
|
1486
|
+
let cursorIndexVersion;
|
|
1099
1487
|
if (input.cursor) {
|
|
1100
1488
|
const parsed = cursorDecode(input.cursor);
|
|
1101
|
-
if (parsed.v !==
|
|
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") {
|
|
1102
1490
|
throw new Error("The pagination cursor does not match this list request.");
|
|
1103
1491
|
}
|
|
1104
1492
|
after = parsed.after;
|
|
1493
|
+
cursorIndexVersion = parsed.indexVersion;
|
|
1494
|
+
}
|
|
1495
|
+
const index = await this.getWorkspaceIndex();
|
|
1496
|
+
if (cursorIndexVersion && cursorIndexVersion !== index.version) {
|
|
1497
|
+
throw new Error("The pagination cursor is stale because the workspace changed.");
|
|
1105
1498
|
}
|
|
1106
1499
|
const prefix = requestPath === "." ? "" : `${requestPath}/`;
|
|
1107
|
-
|
|
1108
|
-
const selected =
|
|
1500
|
+
let candidateIndex = after ? firstPathAfter(index.files, after) : requestPath === "." ? 0 : firstPathAtLeast(index.files, prefix);
|
|
1501
|
+
const selected = [];
|
|
1502
|
+
while (candidateIndex < index.files.length && selected.length <= limit) {
|
|
1503
|
+
const candidate = index.files[candidateIndex];
|
|
1504
|
+
if (!candidate || requestPath !== "." && !candidate.path.startsWith(prefix))
|
|
1505
|
+
break;
|
|
1506
|
+
selected.push(candidate);
|
|
1507
|
+
candidateIndex += 1;
|
|
1508
|
+
}
|
|
1509
|
+
const truncated = selected.length > limit;
|
|
1510
|
+
if (truncated)
|
|
1511
|
+
selected.pop();
|
|
1109
1512
|
const files = [];
|
|
1110
|
-
|
|
1111
|
-
const
|
|
1112
|
-
|
|
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 })));
|
|
1113
1520
|
}
|
|
1114
|
-
const truncated = candidates.length > selected.length;
|
|
1115
1521
|
const last = selected.at(-1);
|
|
1116
1522
|
return {
|
|
1117
1523
|
files,
|
|
1118
1524
|
truncated,
|
|
1119
|
-
...truncated && last ? {
|
|
1525
|
+
...truncated && last ? {
|
|
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
|
+
})
|
|
1535
|
+
} : {}
|
|
1120
1536
|
};
|
|
1121
1537
|
}
|
|
1122
|
-
|
|
1538
|
+
renderReadFile(file, startLine = 1, endLine) {
|
|
1123
1539
|
if (!Number.isSafeInteger(startLine) || startLine < 1)
|
|
1124
1540
|
throw new Error("startLine must be a positive integer.");
|
|
1125
1541
|
if (endLine !== undefined && (!Number.isSafeInteger(endLine) || endLine < startLine)) {
|
|
1126
1542
|
throw new Error("endLine must be greater than or equal to startLine.");
|
|
1127
1543
|
}
|
|
1128
|
-
const file = await this.readStableFile(relativePath, false);
|
|
1129
1544
|
const lines = file.contents.toString("utf8").split(/\r?\n/);
|
|
1130
1545
|
const boundedEnd = Math.min(endLine ?? startLine + 399, startLine + 1999, lines.length);
|
|
1131
1546
|
return {
|
|
@@ -1139,6 +1554,37 @@ class Workspace {
|
|
|
1139
1554
|
truncated: boundedEnd < lines.length
|
|
1140
1555
|
};
|
|
1141
1556
|
}
|
|
1557
|
+
async readFile(relativePath, startLine = 1, endLine) {
|
|
1558
|
+
return this.renderReadFile(await this.readStableFile(relativePath, false), startLine, endLine);
|
|
1559
|
+
}
|
|
1560
|
+
async readFiles(requests) {
|
|
1561
|
+
if (!Array.isArray(requests) || requests.length < 1 || requests.length > MAX_READ_BATCH_FILES) {
|
|
1562
|
+
throw new Error(`readFiles requires between 1 and ${MAX_READ_BATCH_FILES} file requests.`);
|
|
1563
|
+
}
|
|
1564
|
+
const ordered = requests.map((request) => ({
|
|
1565
|
+
...request,
|
|
1566
|
+
path: wirePath(path4.relative(this.root, this.lexicalPath(request.path)))
|
|
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));
|
|
1568
|
+
const stableFiles = new Map;
|
|
1569
|
+
let totalBytes = 0;
|
|
1570
|
+
for (const request of ordered) {
|
|
1571
|
+
if (!Number.isSafeInteger(request.startLine ?? 1) || (request.startLine ?? 1) < 1 || request.endLine !== undefined && (!Number.isSafeInteger(request.endLine) || request.endLine < (request.startLine ?? 1))) {
|
|
1572
|
+
throw new Error(`Invalid line range for ${request.path}.`);
|
|
1573
|
+
}
|
|
1574
|
+
if (stableFiles.has(request.path))
|
|
1575
|
+
continue;
|
|
1576
|
+
const file = await this.readStableFile(request.path, false);
|
|
1577
|
+
totalBytes += file.contents.byteLength;
|
|
1578
|
+
if (totalBytes > MAX_READ_BATCH_BYTES) {
|
|
1579
|
+
throw new Error(`readFiles exceeds the aggregate ${MAX_READ_BATCH_BYTES}-byte source limit.`);
|
|
1580
|
+
}
|
|
1581
|
+
stableFiles.set(request.path, file);
|
|
1582
|
+
}
|
|
1583
|
+
return {
|
|
1584
|
+
files: ordered.map((request) => this.renderReadFile(stableFiles.get(request.path), request.startLine ?? 1, request.endLine)),
|
|
1585
|
+
sourceBytes: totalBytes
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1142
1588
|
async inspectFile(relativePath) {
|
|
1143
1589
|
const file = await this.readStableFile(relativePath);
|
|
1144
1590
|
return {
|
|
@@ -1152,30 +1598,38 @@ class Workspace {
|
|
|
1152
1598
|
if (!query || query.length > 200)
|
|
1153
1599
|
throw new Error("The search query must be between 1 and 200 characters.");
|
|
1154
1600
|
const start = await this.safePath(relativePath);
|
|
1155
|
-
if (!(await
|
|
1601
|
+
if (!(await lstat2(start.path)).isDirectory())
|
|
1156
1602
|
throw new Error("search_files requires a directory.");
|
|
1157
|
-
const requestPath = wirePath(
|
|
1603
|
+
const requestPath = wirePath(path4.relative(this.root, start.path)) || ".";
|
|
1158
1604
|
const limit = options.limit ?? options.maxMatches ?? 100;
|
|
1159
1605
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)
|
|
1160
1606
|
throw new Error("limit must be between 1 and 500.");
|
|
1161
1607
|
const caseSensitive = options.caseSensitive ?? false;
|
|
1162
1608
|
let afterPath = "";
|
|
1163
1609
|
let afterLine = 0;
|
|
1610
|
+
let cursorIndexVersion;
|
|
1164
1611
|
if (options.cursor) {
|
|
1165
1612
|
const parsed = cursorDecode(options.cursor);
|
|
1166
|
-
if (parsed.v !==
|
|
1613
|
+
if (parsed.v !== 2 || parsed.kind !== "search" || parsed.path !== requestPath || parsed.query !== query || parsed.caseSensitive !== caseSensitive || parsed.limit !== limit || typeof parsed.afterPath !== "string" || !Number.isSafeInteger(parsed.afterLine) || typeof parsed.indexVersion !== "string") {
|
|
1167
1614
|
throw new Error("The pagination cursor does not match this search request.");
|
|
1168
1615
|
}
|
|
1169
1616
|
afterPath = parsed.afterPath;
|
|
1170
1617
|
afterLine = parsed.afterLine;
|
|
1618
|
+
cursorIndexVersion = parsed.indexVersion;
|
|
1619
|
+
}
|
|
1620
|
+
const index = await this.getWorkspaceIndex();
|
|
1621
|
+
if (cursorIndexVersion && cursorIndexVersion !== index.version) {
|
|
1622
|
+
throw new Error("The pagination cursor is stale because the workspace changed.");
|
|
1171
1623
|
}
|
|
1172
1624
|
const prefix = requestPath === "." ? "" : `${requestPath}/`;
|
|
1173
1625
|
const needle = caseSensitive ? query : query.toLocaleLowerCase();
|
|
1174
1626
|
const matches = [];
|
|
1175
1627
|
let hasMore = false;
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1628
|
+
let candidateIndex = afterPath ? firstPathAtLeast(index.files, afterPath) : requestPath === "." ? 0 : firstPathAtLeast(index.files, prefix);
|
|
1629
|
+
for (;candidateIndex < index.files.length; candidateIndex += 1) {
|
|
1630
|
+
const candidate = index.files[candidateIndex];
|
|
1631
|
+
if (!candidate || requestPath !== "." && !candidate.path.startsWith(prefix))
|
|
1632
|
+
break;
|
|
1179
1633
|
let file;
|
|
1180
1634
|
try {
|
|
1181
1635
|
file = await this.readStableFile(candidate.path, false);
|
|
@@ -1183,11 +1637,11 @@ class Workspace {
|
|
|
1183
1637
|
continue;
|
|
1184
1638
|
}
|
|
1185
1639
|
const lines = file.contents.toString("utf8").split(/\r?\n/);
|
|
1186
|
-
for (let
|
|
1187
|
-
const lineNumber =
|
|
1640
|
+
for (let index2 = 0;index2 < lines.length; index2 += 1) {
|
|
1641
|
+
const lineNumber = index2 + 1;
|
|
1188
1642
|
if (candidate.path === afterPath && lineNumber <= afterLine)
|
|
1189
1643
|
continue;
|
|
1190
|
-
const line = lines[
|
|
1644
|
+
const line = lines[index2] ?? "";
|
|
1191
1645
|
if (!(caseSensitive ? line : line.toLocaleLowerCase()).includes(needle))
|
|
1192
1646
|
continue;
|
|
1193
1647
|
if (matches.length >= limit) {
|
|
@@ -1204,10 +1658,88 @@ class Workspace {
|
|
|
1204
1658
|
matches,
|
|
1205
1659
|
truncated: hasMore,
|
|
1206
1660
|
...hasMore && last ? {
|
|
1207
|
-
nextCursor: cursorEncode({
|
|
1661
|
+
nextCursor: cursorEncode({
|
|
1662
|
+
v: 2,
|
|
1663
|
+
kind: "search",
|
|
1664
|
+
path: requestPath,
|
|
1665
|
+
query,
|
|
1666
|
+
caseSensitive,
|
|
1667
|
+
limit,
|
|
1668
|
+
afterPath: last.path,
|
|
1669
|
+
afterLine: last.line,
|
|
1670
|
+
indexVersion: index.version
|
|
1671
|
+
})
|
|
1208
1672
|
} : {}
|
|
1209
1673
|
};
|
|
1210
1674
|
}
|
|
1675
|
+
async searchMany(queries, relativePath = ".", options = {}) {
|
|
1676
|
+
if (!Array.isArray(queries) || queries.length < 1 || queries.length > MAX_SEARCH_MANY_QUERIES) {
|
|
1677
|
+
throw new Error(`searchMany requires between 1 and ${MAX_SEARCH_MANY_QUERIES} queries.`);
|
|
1678
|
+
}
|
|
1679
|
+
const limitPerQuery = options.limitPerQuery ?? 50;
|
|
1680
|
+
if (!Number.isSafeInteger(limitPerQuery) || limitPerQuery < 1 || limitPerQuery > MAX_SEARCH_MANY_MATCHES || limitPerQuery * queries.length > MAX_SEARCH_MANY_MATCHES) {
|
|
1681
|
+
throw new Error(`searchMany allows at most ${MAX_SEARCH_MANY_MATCHES} aggregate matches.`);
|
|
1682
|
+
}
|
|
1683
|
+
const seen = new Set;
|
|
1684
|
+
const states = queries.map((input) => {
|
|
1685
|
+
if (!input.query || input.query.length > 200)
|
|
1686
|
+
throw new Error("Each search query must be between 1 and 200 characters.");
|
|
1687
|
+
const caseSensitive = input.caseSensitive ?? false;
|
|
1688
|
+
const key = `${caseSensitive ? "1" : "0"}\x00${input.query}`;
|
|
1689
|
+
if (seen.has(key))
|
|
1690
|
+
throw new Error("searchMany queries must be unique.");
|
|
1691
|
+
seen.add(key);
|
|
1692
|
+
return {
|
|
1693
|
+
query: input.query,
|
|
1694
|
+
caseSensitive,
|
|
1695
|
+
needle: caseSensitive ? input.query : input.query.toLocaleLowerCase(),
|
|
1696
|
+
matches: [],
|
|
1697
|
+
truncated: false
|
|
1698
|
+
};
|
|
1699
|
+
});
|
|
1700
|
+
const start = await this.safePath(relativePath);
|
|
1701
|
+
if (!(await lstat2(start.path)).isDirectory())
|
|
1702
|
+
throw new Error("searchMany requires a directory.");
|
|
1703
|
+
const requestPath = wirePath(path4.relative(this.root, start.path)) || ".";
|
|
1704
|
+
const prefix = requestPath === "." ? "" : `${requestPath}/`;
|
|
1705
|
+
const index = await this.getWorkspaceIndex();
|
|
1706
|
+
let candidateIndex = requestPath === "." ? 0 : firstPathAtLeast(index.files, prefix);
|
|
1707
|
+
for (;candidateIndex < index.files.length; candidateIndex += 1) {
|
|
1708
|
+
const candidate = index.files[candidateIndex];
|
|
1709
|
+
if (!candidate || requestPath !== "." && !candidate.path.startsWith(prefix))
|
|
1710
|
+
break;
|
|
1711
|
+
let file;
|
|
1712
|
+
try {
|
|
1713
|
+
file = await this.readStableFile(candidate.path, false);
|
|
1714
|
+
} catch {
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
const lines = file.contents.toString("utf8").split(/\r?\n/);
|
|
1718
|
+
for (let lineIndex = 0;lineIndex < lines.length; lineIndex += 1) {
|
|
1719
|
+
const line = lines[lineIndex] ?? "";
|
|
1720
|
+
const foldedLine = line.toLocaleLowerCase();
|
|
1721
|
+
for (const state of states) {
|
|
1722
|
+
if (state.truncated || !(state.caseSensitive ? line : foldedLine).includes(state.needle))
|
|
1723
|
+
continue;
|
|
1724
|
+
if (state.matches.length >= limitPerQuery) {
|
|
1725
|
+
state.truncated = true;
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
state.matches.push({
|
|
1729
|
+
path: candidate.path,
|
|
1730
|
+
line: lineIndex + 1,
|
|
1731
|
+
text: truncate(line, 500),
|
|
1732
|
+
digest: file.digest
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
if (states.every((state) => state.truncated))
|
|
1737
|
+
break;
|
|
1738
|
+
}
|
|
1739
|
+
return {
|
|
1740
|
+
results: states.map(({ needle: _needle, ...state }) => state)
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1211
1743
|
audit(entry) {
|
|
1212
1744
|
const record = { id: randomUUID(), timestamp: new Date().toISOString(), ...entry };
|
|
1213
1745
|
this.auditEntries.push(record);
|
|
@@ -1217,16 +1749,16 @@ class Workspace {
|
|
|
1217
1749
|
return this.auditEntries.map((entry) => ({ ...entry }));
|
|
1218
1750
|
}
|
|
1219
1751
|
async secureWorkspaceDirectory(directory, mode = 493) {
|
|
1220
|
-
const resolvedInput =
|
|
1752
|
+
const resolvedInput = path4.resolve(directory);
|
|
1221
1753
|
if (!isInside(this.root, resolvedInput)) {
|
|
1222
1754
|
throw new Error("A mutation directory escapes the workspace.");
|
|
1223
1755
|
}
|
|
1224
|
-
const segments =
|
|
1756
|
+
const segments = path4.relative(this.root, resolvedInput).split(path4.sep).filter(Boolean);
|
|
1225
1757
|
let current = this.root;
|
|
1226
1758
|
for (const segment of segments) {
|
|
1227
|
-
current =
|
|
1759
|
+
current = path4.join(current, segment);
|
|
1228
1760
|
try {
|
|
1229
|
-
const entry = await
|
|
1761
|
+
const entry = await lstat2(current);
|
|
1230
1762
|
if (entry.isSymbolicLink())
|
|
1231
1763
|
throw new Error("A mutation directory must not contain a symbolic link.");
|
|
1232
1764
|
if (!entry.isDirectory())
|
|
@@ -1239,7 +1771,7 @@ class Workspace {
|
|
|
1239
1771
|
} catch (mkdirError) {
|
|
1240
1772
|
if (mkdirError.code !== "EEXIST")
|
|
1241
1773
|
throw mkdirError;
|
|
1242
|
-
const raced = await
|
|
1774
|
+
const raced = await lstat2(current);
|
|
1243
1775
|
if (raced.isSymbolicLink() || !raced.isDirectory()) {
|
|
1244
1776
|
throw new Error("A mutation directory changed during creation.");
|
|
1245
1777
|
}
|
|
@@ -1252,8 +1784,8 @@ class Workspace {
|
|
|
1252
1784
|
return canonical;
|
|
1253
1785
|
}
|
|
1254
1786
|
async stageFile(target, contents, mode) {
|
|
1255
|
-
const directory = await this.secureWorkspaceDirectory(
|
|
1256
|
-
const temporary =
|
|
1787
|
+
const directory = await this.secureWorkspaceDirectory(path4.dirname(target));
|
|
1788
|
+
const temporary = path4.join(directory, `.${path4.basename(target)}.zhivex-${randomUUID()}.tmp`);
|
|
1257
1789
|
const handle = await open(temporary, "wx", mode);
|
|
1258
1790
|
try {
|
|
1259
1791
|
await handle.writeFile(contents);
|
|
@@ -1371,6 +1903,7 @@ class Workspace {
|
|
|
1371
1903
|
throw new AggregateError([error, ...rollbackErrors], "Patch failed and rollback was incomplete.");
|
|
1372
1904
|
throw error;
|
|
1373
1905
|
}
|
|
1906
|
+
this.invalidateWorkspaceIndex();
|
|
1374
1907
|
const changes = prepared.map((item) => this.audit({
|
|
1375
1908
|
operation: item.before ? "update" : "create",
|
|
1376
1909
|
path: item.change.path,
|
|
@@ -1411,7 +1944,7 @@ class Workspace {
|
|
|
1411
1944
|
const destination = await this.safePath(parsed.destination, { allowMissing: true });
|
|
1412
1945
|
if (destination.exists)
|
|
1413
1946
|
throw new Error(`Move destination already exists: ${parsed.destination}.`);
|
|
1414
|
-
await this.secureWorkspaceDirectory(
|
|
1947
|
+
await this.secureWorkspaceDirectory(path4.dirname(destination.path));
|
|
1415
1948
|
const destinationRecheck = await this.safePath(parsed.destination, { allowMissing: true });
|
|
1416
1949
|
if (destinationRecheck.exists)
|
|
1417
1950
|
throw new Error(`Move destination already exists: ${parsed.destination}.`);
|
|
@@ -1431,6 +1964,7 @@ class Workspace {
|
|
|
1431
1964
|
await unlink(destination.path).catch(() => {});
|
|
1432
1965
|
throw error;
|
|
1433
1966
|
}
|
|
1967
|
+
this.invalidateWorkspaceIndex();
|
|
1434
1968
|
const audit = this.audit({ operation: "move", path: parsed.source, destination: parsed.destination, beforeDigest: source.digest, afterDigest: source.digest });
|
|
1435
1969
|
return { source: parsed.source, destination: parsed.destination, digest: source.digest, audit };
|
|
1436
1970
|
}
|
|
@@ -1438,9 +1972,9 @@ class Workspace {
|
|
|
1438
1972
|
const segments = [".zhivex-harness", "quarantine"];
|
|
1439
1973
|
let current = this.root;
|
|
1440
1974
|
for (const segment of segments) {
|
|
1441
|
-
current =
|
|
1975
|
+
current = path4.join(current, segment);
|
|
1442
1976
|
try {
|
|
1443
|
-
const entry = await
|
|
1977
|
+
const entry = await lstat2(current);
|
|
1444
1978
|
if (entry.isSymbolicLink()) {
|
|
1445
1979
|
throw new Error("Harness quarantine must not resolve through a symbolic link.");
|
|
1446
1980
|
}
|
|
@@ -1455,7 +1989,7 @@ class Workspace {
|
|
|
1455
1989
|
} catch (mkdirError) {
|
|
1456
1990
|
if (mkdirError.code !== "EEXIST")
|
|
1457
1991
|
throw mkdirError;
|
|
1458
|
-
const racedEntry = await
|
|
1992
|
+
const racedEntry = await lstat2(current);
|
|
1459
1993
|
if (racedEntry.isSymbolicLink() || !racedEntry.isDirectory()) {
|
|
1460
1994
|
throw new Error("Harness quarantine path changed during creation.");
|
|
1461
1995
|
}
|
|
@@ -1469,19 +2003,19 @@ class Workspace {
|
|
|
1469
2003
|
}
|
|
1470
2004
|
async writeManifest(manifest) {
|
|
1471
2005
|
const directory = await this.secureQuarantineDirectory(true);
|
|
1472
|
-
const target =
|
|
2006
|
+
const target = path4.join(directory, `${manifest.quarantineId}.json`);
|
|
1473
2007
|
const temporary = await this.stageFile(target, `${JSON.stringify(manifest, null, 2)}
|
|
1474
2008
|
`, 384);
|
|
1475
2009
|
await rename(temporary, target);
|
|
1476
2010
|
}
|
|
1477
2011
|
async readInternalRegularFile(target, label, maxBytes) {
|
|
1478
|
-
const before = await
|
|
2012
|
+
const before = await lstat2(target);
|
|
1479
2013
|
if (before.isSymbolicLink() || !before.isFile())
|
|
1480
2014
|
throw new Error(`${label} must be a regular non-symlink file.`);
|
|
1481
2015
|
if (before.size > maxBytes)
|
|
1482
2016
|
throw new Error(`${label} exceeds its size limit.`);
|
|
1483
2017
|
const contents = await readFile(target);
|
|
1484
|
-
const after = await
|
|
2018
|
+
const after = await lstat2(target);
|
|
1485
2019
|
if (after.isSymbolicLink() || !after.isFile() || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
|
|
1486
2020
|
throw new Error(`${label} changed while it was being read.`);
|
|
1487
2021
|
}
|
|
@@ -1494,7 +2028,7 @@ class Workspace {
|
|
|
1494
2028
|
throw new Error(`Stale quarantine rejected for ${parsed.path}.`);
|
|
1495
2029
|
const quarantineId = `${Date.now()}-${randomUUID()}`;
|
|
1496
2030
|
const directory = await this.secureQuarantineDirectory(true);
|
|
1497
|
-
const dataPath =
|
|
2031
|
+
const dataPath = path4.join(directory, `${quarantineId}.data`);
|
|
1498
2032
|
if ((await this.readStableFile(parsed.path)).digest !== parsed.expectedDigest)
|
|
1499
2033
|
throw new Error(`Stale quarantine rejected for ${parsed.path}.`);
|
|
1500
2034
|
await link(source.absolutePath, dataPath);
|
|
@@ -1516,9 +2050,10 @@ class Workspace {
|
|
|
1516
2050
|
await unlink(source.absolutePath);
|
|
1517
2051
|
} catch (error) {
|
|
1518
2052
|
await unlink(dataPath).catch(() => {});
|
|
1519
|
-
await unlink(
|
|
2053
|
+
await unlink(path4.join(directory, `${quarantineId}.json`)).catch(() => {});
|
|
1520
2054
|
throw error;
|
|
1521
2055
|
}
|
|
2056
|
+
this.invalidateWorkspaceIndex();
|
|
1522
2057
|
const audit = this.audit({ operation: "quarantine", path: parsed.path, beforeDigest: source.digest, quarantineId });
|
|
1523
2058
|
return { quarantineId, path: parsed.path, digest: source.digest, audit };
|
|
1524
2059
|
}
|
|
@@ -1527,7 +2062,7 @@ class Workspace {
|
|
|
1527
2062
|
const directory = await this.secureQuarantineDirectory(false);
|
|
1528
2063
|
let manifest;
|
|
1529
2064
|
try {
|
|
1530
|
-
const manifestFile = await this.readInternalRegularFile(
|
|
2065
|
+
const manifestFile = await this.readInternalRegularFile(path4.join(directory, `${parsed.quarantineId}.json`), "Quarantine manifest", 64 * 1024);
|
|
1531
2066
|
manifest = JSON.parse(manifestFile.contents.toString("utf8"));
|
|
1532
2067
|
} catch {
|
|
1533
2068
|
throw new Error(`Quarantine entry was not found: ${parsed.quarantineId}.`);
|
|
@@ -1535,7 +2070,7 @@ class Workspace {
|
|
|
1535
2070
|
if (manifest.schemaVersion !== 1 || manifest.quarantineId !== parsed.quarantineId || manifest.status !== "quarantined") {
|
|
1536
2071
|
throw new Error(`Quarantine entry is not restorable: ${parsed.quarantineId}.`);
|
|
1537
2072
|
}
|
|
1538
|
-
const dataPath =
|
|
2073
|
+
const dataPath = path4.join(directory, `${parsed.quarantineId}.data`);
|
|
1539
2074
|
const dataFile = await this.readInternalRegularFile(dataPath, "Quarantine payload", MAX_FILE_BYTES);
|
|
1540
2075
|
const digest2 = digestBytes(dataFile.contents);
|
|
1541
2076
|
if (digest2 !== manifest.digest || parsed.expectedDigest && parsed.expectedDigest !== digest2) {
|
|
@@ -1545,16 +2080,16 @@ class Workspace {
|
|
|
1545
2080
|
const destination = await this.safePath(destinationPath, { allowMissing: true });
|
|
1546
2081
|
if (destination.exists)
|
|
1547
2082
|
throw new Error(`Restore destination already exists: ${destinationPath}.`);
|
|
1548
|
-
await this.secureWorkspaceDirectory(
|
|
2083
|
+
await this.secureWorkspaceDirectory(path4.dirname(destination.path));
|
|
1549
2084
|
const destinationRecheck = await this.safePath(destinationPath, { allowMissing: true });
|
|
1550
2085
|
if (destinationRecheck.exists)
|
|
1551
2086
|
throw new Error(`Restore destination already exists: ${destinationPath}.`);
|
|
1552
|
-
const dataRecheck = await
|
|
2087
|
+
const dataRecheck = await lstat2(dataPath);
|
|
1553
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) {
|
|
1554
2089
|
throw new Error("Quarantine payload changed before restore commit.");
|
|
1555
2090
|
}
|
|
1556
2091
|
await link(dataPath, destination.path);
|
|
1557
|
-
const linkedDestination = await
|
|
2092
|
+
const linkedDestination = await lstat2(destination.path);
|
|
1558
2093
|
if (linkedDestination.isSymbolicLink() || !linkedDestination.isFile() || linkedDestination.dev !== dataFile.stat.dev || linkedDestination.ino !== dataFile.stat.ino || digestBytes(await readFile(destination.path)) !== digest2) {
|
|
1559
2094
|
await unlink(destination.path).catch(() => {});
|
|
1560
2095
|
throw new Error("Restore destination did not match the quarantined payload.");
|
|
@@ -1587,27 +2122,25 @@ class Workspace {
|
|
|
1587
2122
|
}
|
|
1588
2123
|
throw rollbackErrors.length ? new AggregateError([error, ...rollbackErrors], "Restore failed and rollback was incomplete.") : error;
|
|
1589
2124
|
}
|
|
2125
|
+
this.invalidateWorkspaceIndex();
|
|
1590
2126
|
const audit = this.audit({ operation: "restore", path: destinationPath, afterDigest: digest2, quarantineId: parsed.quarantineId });
|
|
1591
2127
|
return { quarantineId: parsed.quarantineId, path: destinationPath, digest: digest2, audit };
|
|
1592
2128
|
}
|
|
1593
2129
|
async runCheck(check, expectedScript, allowedChecks = DEFAULT_CHECKS) {
|
|
1594
|
-
|
|
1595
|
-
throw new Error(`The check "${check}" is not in the explicit allowlist.`);
|
|
1596
|
-
}
|
|
1597
|
-
let scripts = {};
|
|
2130
|
+
let manifest;
|
|
1598
2131
|
try {
|
|
1599
|
-
|
|
1600
|
-
scripts = packageJson.scripts ?? {};
|
|
2132
|
+
manifest = JSON.parse((await this.readStableFile("package.json", false)).contents.toString("utf8"));
|
|
1601
2133
|
} catch (error) {
|
|
1602
2134
|
if (error.code === "ENOENT")
|
|
1603
2135
|
throw new Error("The workspace does not contain a package.json file.");
|
|
1604
2136
|
throw error;
|
|
1605
2137
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
2138
|
+
const resolved = await resolvePackageCheckCommand(this.root, manifest, check, expectedScript, allowedChecks);
|
|
2139
|
+
try {
|
|
2140
|
+
return await spawnBounded(resolved.command, this.root);
|
|
2141
|
+
} finally {
|
|
2142
|
+
this.invalidateWorkspaceIndex();
|
|
2143
|
+
}
|
|
1611
2144
|
}
|
|
1612
2145
|
async gitDiff() {
|
|
1613
2146
|
const [statusResult, diffResult, stagedResult] = await Promise.all([
|
|
@@ -1620,8 +2153,8 @@ class Workspace {
|
|
|
1620
2153
|
}
|
|
1621
2154
|
|
|
1622
2155
|
// src/operations.ts
|
|
1623
|
-
import { chmod, lstat as
|
|
1624
|
-
import
|
|
2156
|
+
import { chmod, lstat as lstat4, mkdir as mkdir2, open as open2 } from "node:fs/promises";
|
|
2157
|
+
import path6 from "node:path";
|
|
1625
2158
|
import {
|
|
1626
2159
|
cancelAgentRun,
|
|
1627
2160
|
cancelAgentRunTree,
|
|
@@ -1638,29 +2171,95 @@ import {
|
|
|
1638
2171
|
createSqliteAgentRunStore
|
|
1639
2172
|
} from "@zhivex-ai/agents/ops";
|
|
1640
2173
|
import { createAgentRunLedger } from "@zhivex-ai/agents/control-plane";
|
|
1641
|
-
|
|
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
|
+
}
|
|
1642
2241
|
|
|
1643
2242
|
// src/state-directory.ts
|
|
1644
|
-
import { lstat as
|
|
1645
|
-
import
|
|
2243
|
+
import { lstat as lstat3 } from "node:fs/promises";
|
|
2244
|
+
import path5 from "node:path";
|
|
1646
2245
|
var SENSITIVE_STATE_SEGMENTS = new Set([".git", ".env", ".npmrc", "dist", "node_modules", "src"]);
|
|
1647
2246
|
var isInside2 = (root, candidate) => {
|
|
1648
|
-
const relative =
|
|
1649
|
-
return relative === "" || !relative.startsWith(`..${
|
|
2247
|
+
const relative = path5.relative(root, candidate);
|
|
2248
|
+
return relative === "" || !relative.startsWith(`..${path5.sep}`) && relative !== ".." && !path5.isAbsolute(relative);
|
|
1650
2249
|
};
|
|
1651
2250
|
var validateStateDirectory = async (workspace, stateDirectory) => {
|
|
1652
|
-
if (stateDirectory === workspace || stateDirectory ===
|
|
2251
|
+
if (stateDirectory === workspace || stateDirectory === path5.parse(stateDirectory).root) {
|
|
1653
2252
|
throw new Error("The state directory cannot be the workspace or filesystem root.");
|
|
1654
2253
|
}
|
|
1655
2254
|
const insideWorkspace = isInside2(workspace, stateDirectory);
|
|
1656
|
-
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);
|
|
1657
2256
|
const sensitiveSegment = insideWorkspace ? relativeSegments.find((segment) => SENSITIVE_STATE_SEGMENTS.has(segment)) : undefined;
|
|
1658
2257
|
if (sensitiveSegment) {
|
|
1659
2258
|
throw new Error(`The state directory is inside the protected workspace path: ${sensitiveSegment}.`);
|
|
1660
2259
|
}
|
|
1661
2260
|
if (!insideWorkspace) {
|
|
1662
2261
|
try {
|
|
1663
|
-
const externalEntry = await
|
|
2262
|
+
const externalEntry = await lstat3(stateDirectory);
|
|
1664
2263
|
if (externalEntry.isSymbolicLink()) {
|
|
1665
2264
|
throw new Error(`The state directory must not be a symbolic link: ${stateDirectory}.`);
|
|
1666
2265
|
}
|
|
@@ -1676,9 +2275,9 @@ var validateStateDirectory = async (workspace, stateDirectory) => {
|
|
|
1676
2275
|
}
|
|
1677
2276
|
let current = workspace;
|
|
1678
2277
|
for (const segment of relativeSegments) {
|
|
1679
|
-
current =
|
|
2278
|
+
current = path5.join(current, segment);
|
|
1680
2279
|
try {
|
|
1681
|
-
const entry = await
|
|
2280
|
+
const entry = await lstat3(current);
|
|
1682
2281
|
if (entry.isSymbolicLink()) {
|
|
1683
2282
|
throw new Error(`The state directory must not resolve through a symbolic link: ${current}.`);
|
|
1684
2283
|
}
|
|
@@ -1865,7 +2464,7 @@ var sqliteAdapter = (database) => ({
|
|
|
1865
2464
|
});
|
|
1866
2465
|
var ensurePrivateStateDirectory = async (directory) => {
|
|
1867
2466
|
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
1868
|
-
const entry = await
|
|
2467
|
+
const entry = await lstat4(directory);
|
|
1869
2468
|
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
1870
2469
|
throw new Error(`The state directory must be a real directory: ${directory}.`);
|
|
1871
2470
|
}
|
|
@@ -1921,17 +2520,17 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
1921
2520
|
backend: "file",
|
|
1922
2521
|
store,
|
|
1923
2522
|
memory: createFileAgentMemoryStore({
|
|
1924
|
-
directory:
|
|
2523
|
+
directory: path6.join(config.stateDirectory, "memory"),
|
|
1925
2524
|
scope: config.scope
|
|
1926
2525
|
}),
|
|
1927
2526
|
migration,
|
|
1928
2527
|
close() {}
|
|
1929
2528
|
};
|
|
1930
2529
|
}
|
|
1931
|
-
const databasePath =
|
|
2530
|
+
const databasePath = path6.join(config.stateDirectory, HARNESS_SQLITE_FILE);
|
|
1932
2531
|
let databaseEntry;
|
|
1933
2532
|
try {
|
|
1934
|
-
databaseEntry = await
|
|
2533
|
+
databaseEntry = await lstat4(databasePath);
|
|
1935
2534
|
} catch (error) {
|
|
1936
2535
|
if (error.code !== "ENOENT") {
|
|
1937
2536
|
throw error;
|
|
@@ -1944,12 +2543,12 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
1944
2543
|
throw creationError;
|
|
1945
2544
|
}
|
|
1946
2545
|
}
|
|
1947
|
-
databaseEntry = await
|
|
2546
|
+
databaseEntry = await lstat4(databasePath);
|
|
1948
2547
|
}
|
|
1949
2548
|
if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
|
|
1950
2549
|
throw new Error(`The SQLite state path must be a real file: ${databasePath}.`);
|
|
1951
2550
|
}
|
|
1952
|
-
const database = new
|
|
2551
|
+
const database = new SqliteDatabase(databasePath, { create: false, strict: true });
|
|
1953
2552
|
let closed = false;
|
|
1954
2553
|
const closeDatabase = () => {
|
|
1955
2554
|
if (!closed) {
|
|
@@ -1958,7 +2557,7 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
1958
2557
|
}
|
|
1959
2558
|
};
|
|
1960
2559
|
try {
|
|
1961
|
-
const openedEntry = await
|
|
2560
|
+
const openedEntry = await lstat4(databasePath);
|
|
1962
2561
|
if (openedEntry.isSymbolicLink() || !openedEntry.isFile() || openedEntry.dev !== databaseEntry.dev || openedEntry.ino !== databaseEntry.ino) {
|
|
1963
2562
|
throw new Error(`The SQLite state path changed while it was being opened: ${databasePath}.`);
|
|
1964
2563
|
}
|
|
@@ -1985,8 +2584,8 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
1985
2584
|
};
|
|
1986
2585
|
|
|
1987
2586
|
// src/mcp.ts
|
|
1988
|
-
import { lstat as
|
|
1989
|
-
import
|
|
2587
|
+
import { lstat as lstat5, readFile as readFile2, realpath as realpath2 } from "node:fs/promises";
|
|
2588
|
+
import path7 from "node:path";
|
|
1990
2589
|
import {
|
|
1991
2590
|
createMcpToolSet,
|
|
1992
2591
|
serializeJsonValue
|
|
@@ -1994,15 +2593,19 @@ import {
|
|
|
1994
2593
|
import { z as z2 } from "zod";
|
|
1995
2594
|
|
|
1996
2595
|
// src/version.ts
|
|
1997
|
-
import { createRequire } from "module";
|
|
1998
|
-
var packageMetadata =
|
|
2596
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
2597
|
+
var packageMetadata = createRequire2(import.meta.url)("../package.json");
|
|
1999
2598
|
if (typeof packageMetadata.version !== "string" || packageMetadata.version.length === 0) {
|
|
2000
2599
|
throw new Error("package.json must contain a non-empty version.");
|
|
2001
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
|
+
}
|
|
2002
2604
|
if (typeof packageMetadata.engines?.bun !== "string" || packageMetadata.engines.bun.length === 0) {
|
|
2003
2605
|
throw new Error("package.json must contain a non-empty engines.bun requirement.");
|
|
2004
2606
|
}
|
|
2005
2607
|
var HARNESS_VERSION = packageMetadata.version;
|
|
2608
|
+
var NODE_ENGINE_RANGE = packageMetadata.engines.node;
|
|
2006
2609
|
var BUN_ENGINE_RANGE = packageMetadata.engines.bun;
|
|
2007
2610
|
|
|
2008
2611
|
// src/mcp.ts
|
|
@@ -2036,8 +2639,8 @@ var configurationSchema = z2.object({
|
|
|
2036
2639
|
servers: z2.array(serverSchema).max(20)
|
|
2037
2640
|
}).strict();
|
|
2038
2641
|
var isInsidePath = (root, candidate) => {
|
|
2039
|
-
const relative =
|
|
2040
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
2642
|
+
const relative = path7.relative(root, candidate);
|
|
2643
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path7.sep}`) && !path7.isAbsolute(relative);
|
|
2041
2644
|
};
|
|
2042
2645
|
var validatedHttpUrl = (value, serverName) => {
|
|
2043
2646
|
let url;
|
|
@@ -2121,7 +2724,7 @@ var normalizeHarnessMcpConfiguration = (value) => {
|
|
|
2121
2724
|
return { schemaVersion: HARNESS_MCP_CONFIG_SCHEMA_VERSION, servers };
|
|
2122
2725
|
};
|
|
2123
2726
|
var loadHarnessMcpConfiguration = async (workspace, configPath) => {
|
|
2124
|
-
const entry = await
|
|
2727
|
+
const entry = await lstat5(configPath);
|
|
2125
2728
|
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
2126
2729
|
throw new Error(`MCP configuration must be a regular non-symlink file: ${configPath}.`);
|
|
2127
2730
|
}
|
|
@@ -2376,7 +2979,7 @@ var createHarnessMcpTools = async (configuration, options = {}) => {
|
|
|
2376
2979
|
var mcpConfigurationFingerprintInput = (configuration) => serializeJsonValue(configuration);
|
|
2377
2980
|
|
|
2378
2981
|
// src/orchestration.ts
|
|
2379
|
-
import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
|
|
2982
|
+
import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
|
|
2380
2983
|
import {
|
|
2381
2984
|
applySafetyPolicyToAgent,
|
|
2382
2985
|
createBudgetGuard,
|
|
@@ -2389,7 +2992,7 @@ var HARNESS_SUBAGENT_PROFILE_DESCRIPTORS = [
|
|
|
2389
2992
|
toolName: "delegate_explorer",
|
|
2390
2993
|
description: "Delegate bounded repository discovery to a read-only explorer.",
|
|
2391
2994
|
instructions: "Inspect the repository with read-only tools. Return evidence with paths and concrete findings. Never propose or perform mutations.",
|
|
2392
|
-
toolNames: ["list_files", "read_file", "search_files", "git_diff"]
|
|
2995
|
+
toolNames: ["list_files", "read_file", "read_files", "search_files", "search_many", "git_diff"]
|
|
2393
2996
|
},
|
|
2394
2997
|
{
|
|
2395
2998
|
id: "implementer",
|
|
@@ -2401,16 +3004,16 @@ var HARNESS_SUBAGENT_PROFILE_DESCRIPTORS = [
|
|
|
2401
3004
|
{
|
|
2402
3005
|
id: "tester",
|
|
2403
3006
|
toolName: "delegate_tester",
|
|
2404
|
-
description: "Delegate focused verification to a tester with read-only inspection and approved
|
|
2405
|
-
instructions: "Verify the delegated behavior with repository evidence and declared
|
|
2406
|
-
toolNames: ["list_files", "read_file", "search_files", "run_check", "git_diff"]
|
|
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.",
|
|
3009
|
+
toolNames: ["list_files", "read_file", "read_files", "search_files", "search_many", "run_check", "git_diff"]
|
|
2407
3010
|
},
|
|
2408
3011
|
{
|
|
2409
3012
|
id: "reviewer",
|
|
2410
3013
|
toolName: "delegate_reviewer",
|
|
2411
3014
|
description: "Delegate an independent read-only review of correctness, safety, and regressions.",
|
|
2412
3015
|
instructions: "Review the requested scope independently. Inspect concrete code and diffs, prioritize actionable defects, and do not mutate the workspace.",
|
|
2413
|
-
toolNames: ["list_files", "read_file", "search_files", "mutation_audit", "git_diff"]
|
|
3016
|
+
toolNames: ["list_files", "read_file", "read_files", "search_files", "search_many", "mutation_audit", "git_diff"]
|
|
2414
3017
|
}
|
|
2415
3018
|
];
|
|
2416
3019
|
var pickTools = (tools, names) => {
|
|
@@ -2547,10 +3150,13 @@ var runHarnessReviewGroup = async (runtime, input, profiles = ["explorer", "revi
|
|
|
2547
3150
|
};
|
|
2548
3151
|
|
|
2549
3152
|
// src/execution-environment.ts
|
|
2550
|
-
import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
|
|
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";
|
|
2551
3156
|
import {
|
|
2552
3157
|
chmod as chmod2,
|
|
2553
|
-
|
|
3158
|
+
copyFile,
|
|
3159
|
+
lstat as lstat6,
|
|
2554
3160
|
mkdir as mkdir3,
|
|
2555
3161
|
readFile as readFile3,
|
|
2556
3162
|
readdir as readdir2,
|
|
@@ -2558,12 +3164,13 @@ import {
|
|
|
2558
3164
|
rename as rename2,
|
|
2559
3165
|
rm,
|
|
2560
3166
|
writeFile
|
|
2561
|
-
} from "fs/promises";
|
|
2562
|
-
import
|
|
3167
|
+
} from "node:fs/promises";
|
|
3168
|
+
import path8 from "node:path";
|
|
2563
3169
|
import { createAgentExecutionEnvironmentBinding } from "@zhivex-ai/agents/beta";
|
|
2564
3170
|
var HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION = 1;
|
|
2565
3171
|
var HARNESS_OCI_LABEL = "com.zhivex.harness.execution";
|
|
2566
3172
|
var HARNESS_OCI_LABEL_VALUE = "v1";
|
|
3173
|
+
var HARNESS_OCI_OWNER_PID_LABEL = "com.zhivex.harness.owner-pid";
|
|
2567
3174
|
var TERMINAL_STATUSES = new Set([
|
|
2568
3175
|
"completed",
|
|
2569
3176
|
"failed",
|
|
@@ -2573,9 +3180,12 @@ var TERMINAL_STATUSES = new Set([
|
|
|
2573
3180
|
var BUILT_IN_TOOL_NAMES = new Set([
|
|
2574
3181
|
"list_files",
|
|
2575
3182
|
"read_file",
|
|
3183
|
+
"read_files",
|
|
2576
3184
|
"search_files",
|
|
3185
|
+
"search_many",
|
|
2577
3186
|
"propose_edits",
|
|
2578
3187
|
"apply_patch",
|
|
3188
|
+
"apply_reviewed_edits",
|
|
2579
3189
|
"move_file",
|
|
2580
3190
|
"quarantine_file",
|
|
2581
3191
|
"restore_file",
|
|
@@ -2583,19 +3193,22 @@ var BUILT_IN_TOOL_NAMES = new Set([
|
|
|
2583
3193
|
"mutation_audit",
|
|
2584
3194
|
"git_diff",
|
|
2585
3195
|
"run_environment_command",
|
|
3196
|
+
"run_environment_batch",
|
|
2586
3197
|
"inspect_environment_patch",
|
|
2587
3198
|
"apply_environment_patch",
|
|
3199
|
+
"verify_and_apply_environment_patch",
|
|
3200
|
+
"verify_and_apply_reviewed_edits",
|
|
2588
3201
|
"environment_status"
|
|
2589
3202
|
]);
|
|
2590
3203
|
var digest2 = (value) => `sha256:${createHash6("sha256").update(value).digest("hex")}`;
|
|
2591
3204
|
var runHash = (runId) => createHash6("sha256").update(runId).digest("hex").slice(0, 24);
|
|
2592
3205
|
var isInside3 = (root, candidate) => {
|
|
2593
|
-
const relative =
|
|
2594
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
3206
|
+
const relative = path8.relative(root, candidate);
|
|
3207
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relative);
|
|
2595
3208
|
};
|
|
2596
3209
|
var privateDirectory = async (directory) => {
|
|
2597
3210
|
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
2598
|
-
const entry = await
|
|
3211
|
+
const entry = await lstat6(directory);
|
|
2599
3212
|
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
2600
3213
|
throw new Error(`Execution artifact directory must be a real directory: ${directory}.`);
|
|
2601
3214
|
}
|
|
@@ -2608,14 +3221,19 @@ var atomicJson = async (target, value) => {
|
|
|
2608
3221
|
await rename2(temporary, target);
|
|
2609
3222
|
};
|
|
2610
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;
|
|
2611
3225
|
var spawnBounded2 = async (command, options) => {
|
|
2612
|
-
const child =
|
|
3226
|
+
const child = spawn2(command[0], command.slice(1), {
|
|
2613
3227
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
2614
3228
|
env: options.env ?? {},
|
|
2615
|
-
|
|
2616
|
-
stdout: "pipe",
|
|
2617
|
-
stderr: "pipe"
|
|
3229
|
+
stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"]
|
|
2618
3230
|
});
|
|
3231
|
+
if (options.input !== undefined) {
|
|
3232
|
+
child.stdin?.on("error", () => {
|
|
3233
|
+
return;
|
|
3234
|
+
});
|
|
3235
|
+
child.stdin?.end(options.input);
|
|
3236
|
+
}
|
|
2619
3237
|
let timedOut = false;
|
|
2620
3238
|
let cancelled = false;
|
|
2621
3239
|
let outputLimitExceeded = false;
|
|
@@ -2643,22 +3261,19 @@ var spawnBounded2 = async (command, options) => {
|
|
|
2643
3261
|
options.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
2644
3262
|
}
|
|
2645
3263
|
const consume = async (stream) => {
|
|
2646
|
-
const reader = stream.getReader();
|
|
2647
3264
|
const chunks = [];
|
|
2648
3265
|
let omittedBytes = 0;
|
|
2649
|
-
for (
|
|
2650
|
-
const
|
|
2651
|
-
|
|
2652
|
-
break;
|
|
2653
|
-
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;
|
|
2654
3269
|
if (retainedOutputBytes < options.maxOutputBytes) {
|
|
2655
3270
|
const remaining = options.maxOutputBytes - retainedOutputBytes;
|
|
2656
|
-
const kept =
|
|
3271
|
+
const kept = chunk.byteLength <= remaining ? chunk : chunk.subarray(0, remaining);
|
|
2657
3272
|
chunks.push(kept);
|
|
2658
3273
|
retainedOutputBytes += kept.byteLength;
|
|
2659
|
-
omittedBytes +=
|
|
3274
|
+
omittedBytes += chunk.byteLength - kept.byteLength;
|
|
2660
3275
|
} else {
|
|
2661
|
-
omittedBytes +=
|
|
3276
|
+
omittedBytes += chunk.byteLength;
|
|
2662
3277
|
}
|
|
2663
3278
|
if (totalOutputBytes > options.maxOutputBytes) {
|
|
2664
3279
|
outputLimitExceeded = true;
|
|
@@ -2667,13 +3282,17 @@ var spawnBounded2 = async (command, options) => {
|
|
|
2667
3282
|
}
|
|
2668
3283
|
const text = new TextDecoder().decode(Buffer.concat(chunks));
|
|
2669
3284
|
return omittedBytes > 0 ? `${text}
|
|
2670
|
-
|
|
3285
|
+
… output limit exceeded (${omittedBytes} bytes omitted)` : text;
|
|
2671
3286
|
};
|
|
3287
|
+
const exited = new Promise((resolve, reject) => {
|
|
3288
|
+
child.once("error", reject);
|
|
3289
|
+
child.once("exit", (exitCode) => resolve(exitCode ?? 137));
|
|
3290
|
+
});
|
|
2672
3291
|
try {
|
|
2673
3292
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2674
3293
|
consume(child.stdout),
|
|
2675
3294
|
consume(child.stderr),
|
|
2676
|
-
|
|
3295
|
+
exited
|
|
2677
3296
|
]);
|
|
2678
3297
|
return {
|
|
2679
3298
|
command,
|
|
@@ -2697,14 +3316,39 @@ var parseRuntimeVersion = (stdout, runtime) => {
|
|
|
2697
3316
|
};
|
|
2698
3317
|
var validContainerIds = (stdout) => stdout.split(/\r?\n/).map((value) => value.trim()).filter((value) => /^[a-f0-9]{12,64}$/.test(value));
|
|
2699
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
|
+
};
|
|
2700
3329
|
var MAX_SYNC_ENTRIES = 1e4;
|
|
3330
|
+
var updateSnapshotMetadata = (target, kind, relative, entry) => {
|
|
3331
|
+
target.update([
|
|
3332
|
+
kind,
|
|
3333
|
+
relative,
|
|
3334
|
+
entry.dev,
|
|
3335
|
+
entry.ino,
|
|
3336
|
+
entry.size,
|
|
3337
|
+
entry.mtimeMs,
|
|
3338
|
+
entry.ctimeMs,
|
|
3339
|
+
entry.mode & 511
|
|
3340
|
+
].join("\x00"));
|
|
3341
|
+
target.update("\x00");
|
|
3342
|
+
};
|
|
2701
3343
|
var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspaceBytes, maxFileWriteBytes) => {
|
|
2702
3344
|
let entries = 0;
|
|
2703
3345
|
let totalBytes = 0;
|
|
3346
|
+
const seal = createHash6("sha256");
|
|
3347
|
+
const metadata = createHash6("sha256");
|
|
2704
3348
|
const visit = async (directory, relativeDirectory = "") => {
|
|
2705
|
-
for (const name of await readdir2(directory)) {
|
|
3349
|
+
for (const name of (await readdir2(directory)).sort()) {
|
|
2706
3350
|
const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
|
2707
|
-
const absolute =
|
|
3351
|
+
const absolute = path8.join(directory, name);
|
|
2708
3352
|
if (!isInside3(stagedRoot, absolute) || relative.length > 1024) {
|
|
2709
3353
|
throw new Error(`OCI workspace export contains an invalid path: ${relative}.`);
|
|
2710
3354
|
}
|
|
@@ -2716,27 +3360,37 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
2716
3360
|
await rm(absolute, { recursive: true, force: true });
|
|
2717
3361
|
continue;
|
|
2718
3362
|
}
|
|
2719
|
-
const entry = await
|
|
3363
|
+
const entry = await lstat6(absolute);
|
|
2720
3364
|
if (entry.isSymbolicLink()) {
|
|
2721
3365
|
throw new Error(`OCI workspace export contains a symbolic link: ${relative}.`);
|
|
2722
3366
|
}
|
|
2723
3367
|
if (entry.isDirectory()) {
|
|
3368
|
+
updateSnapshotMetadata(metadata, "directory", relative, entry);
|
|
3369
|
+
seal.update(`directory\x00${relative}\x00${entry.mode & 511}\x00`);
|
|
2724
3370
|
await visit(absolute, relative);
|
|
2725
3371
|
continue;
|
|
2726
3372
|
}
|
|
2727
3373
|
if (!entry.isFile() || entry.nlink !== 1) {
|
|
2728
3374
|
throw new Error(`OCI workspace export contains a special or linked file: ${relative}.`);
|
|
2729
3375
|
}
|
|
3376
|
+
const contents = await readFile3(absolute);
|
|
3377
|
+
const after = await lstat6(absolute);
|
|
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)) {
|
|
3379
|
+
throw new Error(`OCI workspace export changed while it was being validated: ${relative}.`);
|
|
3380
|
+
}
|
|
2730
3381
|
totalBytes += entry.size;
|
|
2731
3382
|
if (totalBytes > maxWorkspaceBytes) {
|
|
2732
3383
|
throw new Error(`OCI workspace export exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
|
|
2733
3384
|
}
|
|
3385
|
+
const contentDigest = digest2(contents);
|
|
3386
|
+
updateSnapshotMetadata(metadata, "file", relative, after);
|
|
3387
|
+
seal.update(`file\x00${relative}\x00${entry.mode & 511}\x00${entry.size}\x00${contentDigest}\x00`);
|
|
2734
3388
|
if (entry.size > maxFileWriteBytes) {
|
|
2735
|
-
const previous =
|
|
3389
|
+
const previous = path8.join(previousRoot, ...relative.split("/"));
|
|
2736
3390
|
let unchanged = false;
|
|
2737
3391
|
try {
|
|
2738
|
-
const previousEntry = await
|
|
2739
|
-
unchanged = previousEntry.isFile() && !previousEntry.isSymbolicLink() && previousEntry.size === entry.size && digest2(await readFile3(previous)) ===
|
|
3392
|
+
const previousEntry = await lstat6(previous);
|
|
3393
|
+
unchanged = previousEntry.isFile() && !previousEntry.isSymbolicLink() && previousEntry.size === entry.size && digest2(await readFile3(previous)) === contentDigest;
|
|
2740
3394
|
} catch (error) {
|
|
2741
3395
|
if (error.code !== "ENOENT")
|
|
2742
3396
|
throw error;
|
|
@@ -2748,6 +3402,51 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
2748
3402
|
}
|
|
2749
3403
|
};
|
|
2750
3404
|
await visit(stagedRoot);
|
|
3405
|
+
return {
|
|
3406
|
+
entries,
|
|
3407
|
+
totalBytes,
|
|
3408
|
+
seal: `sha256:${seal.digest("hex")}`,
|
|
3409
|
+
metadataFingerprint: `sha256:${metadata.digest("hex")}`
|
|
3410
|
+
};
|
|
3411
|
+
};
|
|
3412
|
+
var snapshotMetadataFingerprint = async (root, maxWorkspaceBytes) => {
|
|
3413
|
+
let entries = 0;
|
|
3414
|
+
let totalBytes = 0;
|
|
3415
|
+
const metadata = createHash6("sha256");
|
|
3416
|
+
const visit = async (directory, relativeDirectory = "") => {
|
|
3417
|
+
for (const name of (await readdir2(directory)).sort()) {
|
|
3418
|
+
const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
|
3419
|
+
if (relative === "node_modules")
|
|
3420
|
+
continue;
|
|
3421
|
+
const absolute = path8.join(directory, name);
|
|
3422
|
+
if (!isInside3(root, absolute) || relative.length > 1024) {
|
|
3423
|
+
throw new Error(`OCI workspace snapshot contains an invalid path: ${relative}.`);
|
|
3424
|
+
}
|
|
3425
|
+
entries += 1;
|
|
3426
|
+
if (entries > MAX_SYNC_ENTRIES) {
|
|
3427
|
+
throw new Error(`OCI workspace snapshot exceeds the ${MAX_SYNC_ENTRIES}-entry limit.`);
|
|
3428
|
+
}
|
|
3429
|
+
const entry = await lstat6(absolute);
|
|
3430
|
+
if (entry.isSymbolicLink()) {
|
|
3431
|
+
throw new Error(`OCI workspace snapshot contains a symbolic link: ${relative}.`);
|
|
3432
|
+
}
|
|
3433
|
+
if (entry.isDirectory()) {
|
|
3434
|
+
updateSnapshotMetadata(metadata, "directory", relative, entry);
|
|
3435
|
+
await visit(absolute, relative);
|
|
3436
|
+
continue;
|
|
3437
|
+
}
|
|
3438
|
+
if (!entry.isFile() || entry.nlink !== 1) {
|
|
3439
|
+
throw new Error(`OCI workspace snapshot contains a special or linked file: ${relative}.`);
|
|
3440
|
+
}
|
|
3441
|
+
totalBytes += entry.size;
|
|
3442
|
+
if (totalBytes > maxWorkspaceBytes) {
|
|
3443
|
+
throw new Error(`OCI workspace snapshot exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
|
|
3444
|
+
}
|
|
3445
|
+
updateSnapshotMetadata(metadata, "file", relative, entry);
|
|
3446
|
+
}
|
|
3447
|
+
};
|
|
3448
|
+
await visit(root);
|
|
3449
|
+
return `sha256:${metadata.digest("hex")}`;
|
|
2751
3450
|
};
|
|
2752
3451
|
var replaceSnapshot = async (snapshotRoot, stagedRoot) => {
|
|
2753
3452
|
const backupRoot = `${snapshotRoot}.${randomUUID3()}.previous`;
|
|
@@ -2762,10 +3461,67 @@ var replaceSnapshot = async (snapshotRoot, stagedRoot) => {
|
|
|
2762
3461
|
}
|
|
2763
3462
|
await rm(backupRoot, { recursive: true, force: true });
|
|
2764
3463
|
};
|
|
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 = [
|
|
3470
|
+
"import { createHash } from 'node:crypto';",
|
|
3471
|
+
"import { spawn } from 'node:child_process';",
|
|
3472
|
+
"import { readFileSync } from 'node:fs';",
|
|
3473
|
+
"import { lstat, readFile, readdir, readlink, rm } from 'node:fs/promises';",
|
|
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);}",
|
|
3490
|
+
"const root='/workspace';",
|
|
3491
|
+
"const seal=createHash('sha256');",
|
|
3492
|
+
"const modules=path.join(root,'node_modules');",
|
|
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});}",
|
|
3495
|
+
"const digest=(value)=>`sha256:${createHash('sha256').update(value).digest('hex')}`;",
|
|
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);",
|
|
3497
|
+
"const visit=async(directory,base='')=>{",
|
|
3498
|
+
"for(const name of (await readdir(directory)).sort()){",
|
|
3499
|
+
"const relative=base?`${base}/${name}`:name;",
|
|
3500
|
+
"if(relative==='node_modules')continue;",
|
|
3501
|
+
"const absolute=path.join(directory,name);",
|
|
3502
|
+
"const entry=await lstat(absolute);",
|
|
3503
|
+
"if(entry.isSymbolicLink())throw new Error(`symbolic link: ${relative}`);",
|
|
3504
|
+
"if(entry.isDirectory()){seal.update(`directory\\0${relative}\\0${entry.mode&0o777}\\0`);await visit(absolute,relative);continue;}",
|
|
3505
|
+
"if(!entry.isFile()||entry.nlink!==1)throw new Error(`special or linked file: ${relative}`);",
|
|
3506
|
+
"const contents=await readFile(absolute);",
|
|
3507
|
+
"const after=await lstat(absolute);",
|
|
3508
|
+
"if(!same(entry,after))throw new Error(`changed while sealing: ${relative}`);",
|
|
3509
|
+
"seal.update(`file\\0${relative}\\0${entry.mode&0o777}\\0${entry.size}\\0${digest(contents)}\\0`);",
|
|
3510
|
+
"}",
|
|
3511
|
+
"};",
|
|
3512
|
+
"await visit(root);",
|
|
3513
|
+
"if((await unexpectedProcesses()).length>0){console.error('ZHIVEX_HARNESS_BACKGROUND_PROCESS');process.exit(126);}",
|
|
3514
|
+
"for(const name of await readdir('/tmp'))await rm(path.join('/tmp',name),{recursive:true,force:true});",
|
|
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);}"
|
|
3517
|
+
].join("");
|
|
3518
|
+
var OCI_SESSION_CONTROLLER_SCRIPT = "setInterval(()=>{},2147483647);";
|
|
2765
3519
|
|
|
2766
3520
|
class CliOciRuntimeAdapter {
|
|
2767
3521
|
runtime;
|
|
2768
3522
|
hostEnvironment;
|
|
3523
|
+
sessions = new Map;
|
|
3524
|
+
runQueues = new Map;
|
|
2769
3525
|
constructor(runtime) {
|
|
2770
3526
|
this.runtime = runtime;
|
|
2771
3527
|
this.hostEnvironment = Object.fromEntries(["DOCKER_CONFIG", "DOCKER_CONTEXT", "DOCKER_HOST", "HOME", "PATH", "XDG_RUNTIME_DIR"].flatMap((name) => process.env[name] ? [[name, process.env[name]]] : []));
|
|
@@ -2818,10 +3574,44 @@ ${result.stdout}`)) {
|
|
|
2818
3574
|
throw new Error(`${this.runtime} could not remove OCI workspace volume ${name}: ${result.stderr.trim()}`);
|
|
2819
3575
|
}
|
|
2820
3576
|
}
|
|
2821
|
-
|
|
3577
|
+
requestFingerprint(request) {
|
|
3578
|
+
return digest2(JSON.stringify({
|
|
3579
|
+
runId: request.runId,
|
|
3580
|
+
snapshotRoot: request.snapshotRoot,
|
|
3581
|
+
dependencyRoot: request.dependencyRoot ?? null,
|
|
3582
|
+
imageId: request.imageId,
|
|
3583
|
+
limits: request.limits
|
|
3584
|
+
}));
|
|
3585
|
+
}
|
|
3586
|
+
async withRunLock(runId, operation) {
|
|
3587
|
+
const key = runHash(runId);
|
|
3588
|
+
const previous = this.runQueues.get(key) ?? Promise.resolve();
|
|
3589
|
+
let release;
|
|
3590
|
+
const gate = new Promise((resolve) => {
|
|
3591
|
+
release = resolve;
|
|
3592
|
+
});
|
|
3593
|
+
const tail = previous.then(() => gate, () => gate);
|
|
3594
|
+
this.runQueues.set(key, tail);
|
|
3595
|
+
await previous.catch(() => {
|
|
3596
|
+
return;
|
|
3597
|
+
});
|
|
3598
|
+
try {
|
|
3599
|
+
return await operation();
|
|
3600
|
+
} finally {
|
|
3601
|
+
release();
|
|
3602
|
+
if (this.runQueues.get(key) === tail)
|
|
3603
|
+
this.runQueues.delete(key);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
async destroySession(session) {
|
|
3607
|
+
if (this.sessions.get(session.key) === session)
|
|
3608
|
+
this.sessions.delete(session.key);
|
|
3609
|
+
await this.forceRemove(session.name);
|
|
3610
|
+
await this.removeVolume(session.volumeName);
|
|
3611
|
+
}
|
|
3612
|
+
async createSession(request, requestFingerprint, snapshotSeal, hostFingerprint) {
|
|
2822
3613
|
const resourceName = `zhivex-harness-${runHash(request.runId)}-${randomUUID3().slice(0, 8)}`;
|
|
2823
3614
|
const name = `${resourceName}-job`;
|
|
2824
|
-
const exportName = `${resourceName}-export`;
|
|
2825
3615
|
const volumeName = `${resourceName}-workspace`;
|
|
2826
3616
|
const uid = process.getuid?.() ?? 65532;
|
|
2827
3617
|
const gid = process.getgid?.() ?? 65532;
|
|
@@ -2840,6 +3630,8 @@ ${result.stdout}`)) {
|
|
|
2840
3630
|
`${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`,
|
|
2841
3631
|
"--label",
|
|
2842
3632
|
`com.zhivex.harness.run=${runHash(request.runId)}`,
|
|
3633
|
+
"--label",
|
|
3634
|
+
`${HARNESS_OCI_OWNER_PID_LABEL}=${process.pid}`,
|
|
2843
3635
|
volumeName
|
|
2844
3636
|
];
|
|
2845
3637
|
const args = [
|
|
@@ -2874,38 +3666,30 @@ ${result.stdout}`)) {
|
|
|
2874
3666
|
"--env",
|
|
2875
3667
|
"TMPDIR=/tmp",
|
|
2876
3668
|
"--env",
|
|
2877
|
-
"
|
|
3669
|
+
"NPM_CONFIG_CACHE=/tmp/npm-cache",
|
|
2878
3670
|
"--env",
|
|
2879
3671
|
"CI=1",
|
|
2880
|
-
"--volume",
|
|
2881
|
-
`${request.snapshotRoot}:/seed:ro`,
|
|
2882
3672
|
...request.dependencyRoot ? ["--volume", `${request.dependencyRoot}:/dependencies:ro`] : [],
|
|
2883
3673
|
"--workdir",
|
|
2884
3674
|
"/workspace",
|
|
2885
3675
|
"--entrypoint",
|
|
2886
|
-
"
|
|
3676
|
+
"node",
|
|
2887
3677
|
request.imageId,
|
|
3678
|
+
"--input-type=module",
|
|
2888
3679
|
"-e",
|
|
2889
|
-
|
|
3680
|
+
OCI_SESSION_CONTROLLER_SCRIPT
|
|
2890
3681
|
];
|
|
2891
|
-
let
|
|
2892
|
-
const forceRemove = () => {
|
|
2893
|
-
forcedRemoval ??= this.forceRemove(name);
|
|
2894
|
-
};
|
|
3682
|
+
let volumeCreated = false;
|
|
2895
3683
|
try {
|
|
2896
3684
|
await this.cli(volumeArgs);
|
|
3685
|
+
volumeCreated = true;
|
|
2897
3686
|
await this.cli(args);
|
|
2898
3687
|
await this.cli(["start", name]);
|
|
2899
3688
|
await this.cli([
|
|
2900
|
-
"
|
|
2901
|
-
"
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
`${uid}:${gid}`,
|
|
2905
|
-
name,
|
|
2906
|
-
"bun",
|
|
2907
|
-
"-e",
|
|
2908
|
-
"import { cp } from 'node:fs/promises'; await cp('/seed', '/workspace', { recursive: true, force: false, errorOnExist: true })"
|
|
3689
|
+
"cp",
|
|
3690
|
+
"-a",
|
|
3691
|
+
`${request.snapshotRoot}${path8.sep}.`,
|
|
3692
|
+
`${name}:/workspace`
|
|
2909
3693
|
], 60000);
|
|
2910
3694
|
if (request.dependencyRoot) {
|
|
2911
3695
|
await this.cli([
|
|
@@ -2915,93 +3699,208 @@ ${result.stdout}`)) {
|
|
|
2915
3699
|
"--user",
|
|
2916
3700
|
`${uid}:${gid}`,
|
|
2917
3701
|
name,
|
|
2918
|
-
"
|
|
3702
|
+
"node",
|
|
3703
|
+
"--input-type=module",
|
|
2919
3704
|
"-e",
|
|
2920
3705
|
"import { symlink } from 'node:fs/promises'; await symlink('/dependencies', '/workspace/node_modules', 'dir')"
|
|
2921
3706
|
]);
|
|
2922
3707
|
}
|
|
2923
|
-
const
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
"--workdir",
|
|
2927
|
-
"/workspace",
|
|
2928
|
-
"--user",
|
|
2929
|
-
`${uid}:${gid}`,
|
|
3708
|
+
const session = {
|
|
3709
|
+
key: runHash(request.runId),
|
|
3710
|
+
requestFingerprint,
|
|
2930
3711
|
name,
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
if (
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
3712
|
+
volumeName,
|
|
3713
|
+
snapshotSeal,
|
|
3714
|
+
hostFingerprint
|
|
3715
|
+
};
|
|
3716
|
+
this.sessions.set(session.key, session);
|
|
3717
|
+
return session;
|
|
3718
|
+
} catch (error) {
|
|
3719
|
+
await this.forceRemove(name);
|
|
3720
|
+
if (volumeCreated)
|
|
3721
|
+
await this.removeVolume(volumeName).catch(() => {
|
|
3722
|
+
return;
|
|
3723
|
+
});
|
|
3724
|
+
throw error;
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
async runInSession(request, session, sessionReused) {
|
|
3728
|
+
const totalStartedAt = process.hrtime.bigint();
|
|
3729
|
+
const uid = process.getuid?.() ?? 65532;
|
|
3730
|
+
const gid = process.getgid?.() ?? 65532;
|
|
3731
|
+
let forcedRemoval;
|
|
3732
|
+
const forceRemove = () => {
|
|
3733
|
+
forcedRemoval ??= this.forceRemove(session.name);
|
|
3734
|
+
};
|
|
3735
|
+
const commandStartedAt = process.hrtime.bigint();
|
|
3736
|
+
const result = await spawnBounded2([
|
|
3737
|
+
this.runtime,
|
|
3738
|
+
"exec",
|
|
3739
|
+
"-i",
|
|
3740
|
+
"--workdir",
|
|
3741
|
+
"/workspace",
|
|
3742
|
+
"--user",
|
|
3743
|
+
`${uid}:${gid}`,
|
|
3744
|
+
session.name,
|
|
3745
|
+
"node",
|
|
3746
|
+
"--input-type=module",
|
|
3747
|
+
"-e",
|
|
3748
|
+
OCI_COMMAND_RUNNER_SCRIPT
|
|
3749
|
+
], {
|
|
3750
|
+
env: this.hostEnvironment,
|
|
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
|
+
}),
|
|
3758
|
+
...request.abortSignal ? { abortSignal: request.abortSignal } : {},
|
|
3759
|
+
onStop: forceRemove
|
|
3760
|
+
});
|
|
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) {
|
|
3772
|
+
forceRemove();
|
|
3773
|
+
await forcedRemoval;
|
|
3774
|
+
await this.destroySession(session);
|
|
3775
|
+
if (attestationFailure) {
|
|
3776
|
+
throw new Error(`OCI workspace attestation failed: ${attestationFailure[1] ?? "unknown failure"}.`);
|
|
3777
|
+
}
|
|
3778
|
+
return {
|
|
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
|
+
} : {},
|
|
3791
|
+
sessionReused,
|
|
3792
|
+
workspacePublished: false,
|
|
3793
|
+
workspaceExported: false,
|
|
3794
|
+
phaseLatencies: {
|
|
3795
|
+
commandAndAttestationMs,
|
|
3796
|
+
totalMs: elapsedMilliseconds(totalStartedAt)
|
|
3797
|
+
}
|
|
3798
|
+
};
|
|
3799
|
+
}
|
|
3800
|
+
try {
|
|
3801
|
+
if (!attestationMatch) {
|
|
3802
|
+
throw new Error("OCI command attestation returned an invalid workspace seal.");
|
|
3803
|
+
}
|
|
3804
|
+
const workspaceSeal = attestationMatch[1];
|
|
3805
|
+
if (workspaceSeal === session.snapshotSeal) {
|
|
3806
|
+
return {
|
|
3807
|
+
...commandResult,
|
|
3808
|
+
command,
|
|
3809
|
+
commands: request.commands,
|
|
3810
|
+
sessionReused,
|
|
3811
|
+
workspacePublished: true,
|
|
3812
|
+
workspaceExported: false,
|
|
3813
|
+
phaseLatencies: {
|
|
3814
|
+
commandAndAttestationMs,
|
|
3815
|
+
totalMs: elapsedMilliseconds(totalStartedAt)
|
|
2988
3816
|
}
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3817
|
+
};
|
|
3818
|
+
}
|
|
3819
|
+
const workspaceExportStartedAt = process.hrtime.bigint();
|
|
3820
|
+
const stagedRoot = path8.join(path8.dirname(request.snapshotRoot), `.workspace-sync-${randomUUID3()}`);
|
|
3821
|
+
try {
|
|
3822
|
+
await privateDirectory(stagedRoot);
|
|
3823
|
+
await this.cli(["cp", `${session.name}:/workspace/.`, stagedRoot], 60000);
|
|
3824
|
+
const validated = await validateSynchronizedSnapshot(stagedRoot, request.snapshotRoot, request.limits.maxWorkspaceBytes, request.limits.maxFileWriteBytes);
|
|
3825
|
+
await replaceSnapshot(request.snapshotRoot, stagedRoot);
|
|
3826
|
+
session.snapshotSeal = validated.seal;
|
|
3827
|
+
session.hostFingerprint = validated.metadataFingerprint;
|
|
3828
|
+
} finally {
|
|
3829
|
+
await rm(stagedRoot, { recursive: true, force: true });
|
|
3830
|
+
}
|
|
3831
|
+
return {
|
|
3832
|
+
...commandResult,
|
|
3833
|
+
command,
|
|
3834
|
+
commands: request.commands,
|
|
3835
|
+
sessionReused,
|
|
3836
|
+
workspacePublished: true,
|
|
3837
|
+
workspaceExported: true,
|
|
3838
|
+
phaseLatencies: {
|
|
3839
|
+
commandAndAttestationMs,
|
|
3840
|
+
workspaceExportMs: elapsedMilliseconds(workspaceExportStartedAt),
|
|
3841
|
+
totalMs: elapsedMilliseconds(totalStartedAt)
|
|
2993
3842
|
}
|
|
3843
|
+
};
|
|
3844
|
+
} catch (error) {
|
|
3845
|
+
await this.destroySession(session);
|
|
3846
|
+
throw error;
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
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();
|
|
3854
|
+
const key = runHash(request.runId);
|
|
3855
|
+
const requestFingerprint = this.requestFingerprint(request);
|
|
3856
|
+
let session = this.sessions.get(key);
|
|
3857
|
+
let sessionReused = Boolean(session);
|
|
3858
|
+
let validated;
|
|
3859
|
+
let hostSynchronizationMs;
|
|
3860
|
+
let sessionCreationMs;
|
|
3861
|
+
if (session) {
|
|
3862
|
+
const synchronizationStartedAt = process.hrtime.bigint();
|
|
3863
|
+
const hostFingerprint = await snapshotMetadataFingerprint(request.snapshotRoot, request.limits.maxWorkspaceBytes);
|
|
3864
|
+
if (session.requestFingerprint !== requestFingerprint || session.hostFingerprint !== hostFingerprint) {
|
|
3865
|
+
await this.destroySession(session);
|
|
3866
|
+
session = undefined;
|
|
3867
|
+
sessionReused = false;
|
|
2994
3868
|
}
|
|
3869
|
+
hostSynchronizationMs = elapsedMilliseconds(synchronizationStartedAt);
|
|
3870
|
+
}
|
|
3871
|
+
if (!session) {
|
|
3872
|
+
const creationStartedAt = process.hrtime.bigint();
|
|
3873
|
+
validated = await validateSynchronizedSnapshot(request.snapshotRoot, request.snapshotRoot, request.limits.maxWorkspaceBytes, request.limits.maxFileWriteBytes);
|
|
3874
|
+
session = await this.createSession(request, requestFingerprint, validated.seal, validated.metadataFingerprint);
|
|
3875
|
+
sessionCreationMs = elapsedMilliseconds(creationStartedAt);
|
|
3876
|
+
}
|
|
3877
|
+
try {
|
|
3878
|
+
const result = await this.runInSession(request, session, sessionReused);
|
|
2995
3879
|
return {
|
|
2996
3880
|
...result,
|
|
2997
|
-
|
|
3881
|
+
phaseLatencies: {
|
|
3882
|
+
...result.phaseLatencies,
|
|
3883
|
+
...hostSynchronizationMs !== undefined ? { hostSynchronizationMs } : {},
|
|
3884
|
+
...sessionCreationMs !== undefined ? { sessionCreationMs } : {},
|
|
3885
|
+
totalMs: elapsedMilliseconds(totalStartedAt)
|
|
3886
|
+
}
|
|
2998
3887
|
};
|
|
2999
|
-
}
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3888
|
+
} catch (error) {
|
|
3889
|
+
if (this.sessions.get(key) === session)
|
|
3890
|
+
await this.destroySession(session).catch(() => {
|
|
3891
|
+
return;
|
|
3892
|
+
});
|
|
3893
|
+
throw error;
|
|
3003
3894
|
}
|
|
3004
3895
|
}
|
|
3896
|
+
async run(request) {
|
|
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));
|
|
3903
|
+
}
|
|
3005
3904
|
async removeByFilters(filters) {
|
|
3006
3905
|
const listed = await spawnBounded2([this.runtime, "ps", "-aq", ...filters.flatMap((filter) => ["--filter", filter])], {
|
|
3007
3906
|
env: this.hostEnvironment,
|
|
@@ -3046,57 +3945,150 @@ ${result.stdout}`)) {
|
|
|
3046
3945
|
}
|
|
3047
3946
|
return removed;
|
|
3048
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
|
+
}
|
|
3049
3981
|
async removeRunContainers(runId) {
|
|
3982
|
+
const key = runHash(runId);
|
|
3983
|
+
const session = this.sessions.get(key);
|
|
3984
|
+
let cachedResources = 0;
|
|
3985
|
+
if (session) {
|
|
3986
|
+
await this.destroySession(session);
|
|
3987
|
+
cachedResources = 2;
|
|
3988
|
+
}
|
|
3050
3989
|
const filters = [
|
|
3051
3990
|
`label=${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`,
|
|
3052
|
-
`label=com.zhivex.harness.run=${
|
|
3991
|
+
`label=com.zhivex.harness.run=${key}`
|
|
3053
3992
|
];
|
|
3054
3993
|
const containers = await this.removeByFilters(filters);
|
|
3055
3994
|
const volumes = await this.removeVolumesByFilters(filters);
|
|
3056
|
-
return containers + volumes;
|
|
3995
|
+
return cachedResources + containers + volumes;
|
|
3057
3996
|
}
|
|
3058
3997
|
async cleanupOrphans() {
|
|
3059
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();
|
|
3060
4004
|
for (const status of ["created", "exited", "dead", "paused"]) {
|
|
3061
4005
|
removed += await this.removeByFilters([
|
|
3062
4006
|
`label=${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`,
|
|
3063
4007
|
`status=${status}`
|
|
3064
4008
|
]);
|
|
3065
4009
|
}
|
|
4010
|
+
removed += await this.removeRunningOrphans();
|
|
3066
4011
|
removed += await this.removeVolumesByFilters([
|
|
3067
4012
|
`label=${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`
|
|
3068
4013
|
]);
|
|
3069
4014
|
return removed;
|
|
3070
4015
|
}
|
|
3071
4016
|
}
|
|
3072
|
-
var
|
|
4017
|
+
var SNAPSHOT_INVENTORY_PAGE_SIZE = 5000;
|
|
4018
|
+
var COPY_ON_WRITE_UNSUPPORTED_CODES = new Set(["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP"]);
|
|
4019
|
+
var collectSnapshotInventory = async (workspace, metrics) => {
|
|
3073
4020
|
const files = new Map;
|
|
4021
|
+
if (metrics)
|
|
4022
|
+
metrics.inventoryPasses += 1;
|
|
3074
4023
|
let cursor;
|
|
3075
4024
|
do {
|
|
3076
|
-
const page = await workspace.listFiles(".", {
|
|
4025
|
+
const page = await workspace.listFiles(".", {
|
|
4026
|
+
limit: SNAPSHOT_INVENTORY_PAGE_SIZE,
|
|
4027
|
+
...cursor ? { cursor } : {}
|
|
4028
|
+
});
|
|
4029
|
+
if (metrics)
|
|
4030
|
+
metrics.inventoryPages += 1;
|
|
3077
4031
|
for (const file of page.files) {
|
|
3078
|
-
const absolute =
|
|
4032
|
+
const absolute = path8.join(workspace.root, ...file.path.split("/"));
|
|
3079
4033
|
if (!isInside3(workspace.root, absolute))
|
|
3080
4034
|
throw new Error(`Snapshot file escaped its workspace: ${file.path}.`);
|
|
3081
|
-
const
|
|
3082
|
-
if (
|
|
4035
|
+
const entry = await lstat6(absolute);
|
|
4036
|
+
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
3083
4037
|
throw new Error(`Snapshot source is not a regular file: ${file.path}.`);
|
|
3084
|
-
const contents = await readFile3(absolute);
|
|
3085
|
-
const after = await lstat5(absolute);
|
|
3086
|
-
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
|
|
3087
|
-
throw new Error(`Snapshot source changed while reading: ${file.path}.`);
|
|
3088
4038
|
}
|
|
3089
|
-
if (
|
|
3090
|
-
throw new Error(`Snapshot
|
|
3091
|
-
|
|
4039
|
+
if (entry.size !== file.size) {
|
|
4040
|
+
throw new Error(`Snapshot source changed while inventorying: ${file.path}.`);
|
|
4041
|
+
}
|
|
4042
|
+
files.set(file.path, {
|
|
4043
|
+
path: file.path,
|
|
4044
|
+
digest: file.digest,
|
|
4045
|
+
mode: entry.mode & 511,
|
|
4046
|
+
bytes: file.size
|
|
4047
|
+
});
|
|
3092
4048
|
}
|
|
3093
4049
|
cursor = page.nextCursor;
|
|
3094
4050
|
} while (cursor);
|
|
3095
4051
|
return files;
|
|
3096
4052
|
};
|
|
3097
|
-
var
|
|
3098
|
-
const
|
|
3099
|
-
|
|
4053
|
+
var readSnapshotFile = async (workspace, expected, metrics) => {
|
|
4054
|
+
const absolute = path8.join(workspace.root, ...expected.path.split("/"));
|
|
4055
|
+
if (!isInside3(workspace.root, absolute)) {
|
|
4056
|
+
throw new Error(`Snapshot file escaped its workspace: ${expected.path}.`);
|
|
4057
|
+
}
|
|
4058
|
+
const before = await lstat6(absolute);
|
|
4059
|
+
if (before.isSymbolicLink() || !before.isFile()) {
|
|
4060
|
+
throw new Error(`Snapshot source is not a regular file: ${expected.path}.`);
|
|
4061
|
+
}
|
|
4062
|
+
const contents = await readFile3(absolute);
|
|
4063
|
+
const after = await lstat6(absolute);
|
|
4064
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || (before.mode & 511) !== (after.mode & 511)) {
|
|
4065
|
+
throw new Error(`Snapshot source changed while reading: ${expected.path}.`);
|
|
4066
|
+
}
|
|
4067
|
+
const actualDigest = digest2(contents);
|
|
4068
|
+
if (actualDigest !== expected.digest || contents.byteLength !== expected.bytes || (before.mode & 511) !== expected.mode) {
|
|
4069
|
+
throw new Error(`Snapshot source changed after inventory: ${expected.path}.`);
|
|
4070
|
+
}
|
|
4071
|
+
if (metrics) {
|
|
4072
|
+
metrics.verifiedContentReads += 1;
|
|
4073
|
+
metrics.verifiedContentBytes += contents.byteLength;
|
|
4074
|
+
}
|
|
4075
|
+
return { ...expected, contents };
|
|
4076
|
+
};
|
|
4077
|
+
var copySnapshotFile = async (source, target, metrics) => {
|
|
4078
|
+
try {
|
|
4079
|
+
await copyFile(source, target, fsConstants.COPYFILE_FICLONE_FORCE);
|
|
4080
|
+
} catch (error) {
|
|
4081
|
+
const code = error.code;
|
|
4082
|
+
if (!code || !COPY_ON_WRITE_UNSUPPORTED_CODES.has(code))
|
|
4083
|
+
throw error;
|
|
4084
|
+
if (metrics)
|
|
4085
|
+
metrics.snapshotCloneFallbacks += 1;
|
|
4086
|
+
await copyFile(source, target);
|
|
4087
|
+
}
|
|
4088
|
+
};
|
|
4089
|
+
var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceBytes, metrics) => {
|
|
4090
|
+
const files = await collectSnapshotInventory(source, metrics);
|
|
4091
|
+
const totalBytes = [...files.values()].reduce((total, file) => total + file.bytes, 0);
|
|
3100
4092
|
if (totalBytes > maxWorkspaceBytes) {
|
|
3101
4093
|
throw new Error(`Workspace snapshot exceeds the ${maxWorkspaceBytes}-byte OCI limit.`);
|
|
3102
4094
|
}
|
|
@@ -3104,13 +4096,22 @@ var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceB
|
|
|
3104
4096
|
await rm(root, { recursive: true, force: true });
|
|
3105
4097
|
await privateDirectory(root);
|
|
3106
4098
|
}
|
|
3107
|
-
for (const
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
4099
|
+
for (const expected of files.values()) {
|
|
4100
|
+
const file = await readSnapshotFile(source, expected, metrics);
|
|
4101
|
+
const baseTarget = path8.join(baseRoot, ...file.path.split("/"));
|
|
4102
|
+
const snapshotTarget = path8.join(snapshotRoot, ...file.path.split("/"));
|
|
4103
|
+
await Promise.all([
|
|
4104
|
+
mkdir3(path8.dirname(baseTarget), { recursive: true, mode: 448 }),
|
|
4105
|
+
mkdir3(path8.dirname(snapshotTarget), { recursive: true, mode: 448 })
|
|
4106
|
+
]);
|
|
4107
|
+
await writeFile(baseTarget, file.contents, { mode: file.mode });
|
|
4108
|
+
await chmod2(baseTarget, file.mode);
|
|
4109
|
+
await copySnapshotFile(baseTarget, snapshotTarget, metrics);
|
|
4110
|
+
await chmod2(snapshotTarget, file.mode);
|
|
4111
|
+
}
|
|
4112
|
+
if (metrics) {
|
|
4113
|
+
metrics.snapshotFiles += files.size;
|
|
4114
|
+
metrics.snapshotBytes += totalBytes;
|
|
3114
4115
|
}
|
|
3115
4116
|
return { files: files.size, bytes: totalBytes };
|
|
3116
4117
|
};
|
|
@@ -3129,9 +4130,11 @@ var textContent = (file, maxFileWriteBytes) => {
|
|
|
3129
4130
|
}
|
|
3130
4131
|
return file.contents.toString("utf8");
|
|
3131
4132
|
};
|
|
3132
|
-
var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes) => {
|
|
3133
|
-
const before = await
|
|
3134
|
-
|
|
4133
|
+
var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes, metrics) => {
|
|
4134
|
+
const [before, after] = await Promise.all([
|
|
4135
|
+
collectSnapshotInventory(base, metrics),
|
|
4136
|
+
collectSnapshotInventory(current, metrics)
|
|
4137
|
+
]);
|
|
3135
4138
|
const paths = [...new Set([...before.keys(), ...after.keys()])].sort();
|
|
3136
4139
|
const entries = [];
|
|
3137
4140
|
let totalBytes = 0;
|
|
@@ -3141,9 +4144,9 @@ var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes) =>
|
|
|
3141
4144
|
if (oldFile?.digest === newFile?.digest && oldFile?.mode === newFile?.mode)
|
|
3142
4145
|
continue;
|
|
3143
4146
|
const operation = !oldFile ? "create" : !newFile ? "delete" : "update";
|
|
3144
|
-
const beforeContent = oldFile ? textContent(oldFile, maxFileWriteBytes) : undefined;
|
|
3145
|
-
const afterContent = newFile ? textContent(newFile, maxFileWriteBytes) : undefined;
|
|
3146
|
-
const bytes = newFile?.
|
|
4147
|
+
const beforeContent = oldFile ? textContent(await readSnapshotFile(base, oldFile, metrics), maxFileWriteBytes) : undefined;
|
|
4148
|
+
const afterContent = newFile ? textContent(await readSnapshotFile(current, newFile, metrics), maxFileWriteBytes) : undefined;
|
|
4149
|
+
const bytes = newFile?.bytes ?? 0;
|
|
3147
4150
|
totalBytes += bytes;
|
|
3148
4151
|
entries.push({
|
|
3149
4152
|
path: filePath,
|
|
@@ -3183,8 +4186,8 @@ var inspectHostPrecondition = async (workspace, filePath, expectedDigest, expect
|
|
|
3183
4186
|
throw error;
|
|
3184
4187
|
}
|
|
3185
4188
|
};
|
|
3186
|
-
var importPatch = async (runId, expectedPatchId, host, base, current, maxFileWriteBytes) => {
|
|
3187
|
-
const patch = await createEnvironmentPatch(runId, base, current, maxFileWriteBytes);
|
|
4189
|
+
var importPatch = async (runId, expectedPatchId, host, base, current, maxFileWriteBytes, metrics) => {
|
|
4190
|
+
const patch = await createEnvironmentPatch(runId, base, current, maxFileWriteBytes, metrics);
|
|
3188
4191
|
if (patch.patchId !== expectedPatchId) {
|
|
3189
4192
|
throw new Error("Environment patch changed after review; inspect it again before import.");
|
|
3190
4193
|
}
|
|
@@ -3278,7 +4281,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3278
4281
|
version: `${HARNESS_EXECUTION_POLICY_VERSION}:${image.imageDigest}`,
|
|
3279
4282
|
backend: "container",
|
|
3280
4283
|
assurance: "enforced",
|
|
3281
|
-
isolation: "per-
|
|
4284
|
+
isolation: "per-run",
|
|
3282
4285
|
workspace: {
|
|
3283
4286
|
id: workspaceIdentity,
|
|
3284
4287
|
root: "/workspace",
|
|
@@ -3315,26 +4318,41 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3315
4318
|
maxPids: options.config.maxPids,
|
|
3316
4319
|
maxCpus: options.config.maxCpus,
|
|
3317
4320
|
tmpfsMb: options.config.tmpfsMb,
|
|
4321
|
+
containerLifecycle: "warm-per-acquired-run",
|
|
4322
|
+
failedCommandRecovery: "discard-and-reseed",
|
|
3318
4323
|
patchImportApproval: true,
|
|
3319
4324
|
hostMutationMode: "reviewed-environment-patch"
|
|
3320
4325
|
}
|
|
3321
4326
|
};
|
|
3322
4327
|
const binding = createAgentExecutionEnvironmentBinding(manifest);
|
|
3323
|
-
const environmentRoot =
|
|
4328
|
+
const environmentRoot = path8.join(options.stateDirectory, "environments");
|
|
3324
4329
|
return {
|
|
3325
4330
|
manifest,
|
|
3326
4331
|
image,
|
|
3327
4332
|
runtime,
|
|
3328
4333
|
async acquire(request) {
|
|
4334
|
+
const ioMetrics = {
|
|
4335
|
+
inventoryPasses: 0,
|
|
4336
|
+
inventoryPages: 0,
|
|
4337
|
+
verifiedContentReads: 0,
|
|
4338
|
+
verifiedContentBytes: 0,
|
|
4339
|
+
snapshotFiles: 0,
|
|
4340
|
+
snapshotBytes: 0,
|
|
4341
|
+
snapshotCloneFallbacks: 0,
|
|
4342
|
+
containerStarts: 0,
|
|
4343
|
+
containerReuses: 0,
|
|
4344
|
+
workspacePublishes: 0,
|
|
4345
|
+
workspaceExports: 0
|
|
4346
|
+
};
|
|
3329
4347
|
await privateDirectory(environmentRoot);
|
|
3330
|
-
const directory =
|
|
4348
|
+
const directory = path8.join(environmentRoot, runHash(request.runId));
|
|
3331
4349
|
await privateDirectory(directory);
|
|
3332
|
-
const metadataPath =
|
|
3333
|
-
const baseRoot =
|
|
3334
|
-
const snapshotRoot =
|
|
4350
|
+
const metadataPath = path8.join(directory, "environment.json");
|
|
4351
|
+
const baseRoot = path8.join(directory, "base");
|
|
4352
|
+
const snapshotRoot = path8.join(directory, "workspace");
|
|
3335
4353
|
let metadata;
|
|
3336
4354
|
try {
|
|
3337
|
-
const entry = await
|
|
4355
|
+
const entry = await lstat6(metadataPath);
|
|
3338
4356
|
if (entry.isSymbolicLink() || !entry.isFile())
|
|
3339
4357
|
throw new Error("Execution metadata must be a regular file.");
|
|
3340
4358
|
metadata = JSON.parse(await readFile3(metadataPath, "utf8"));
|
|
@@ -3344,7 +4362,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3344
4362
|
} catch (error) {
|
|
3345
4363
|
if (error.code !== "ENOENT")
|
|
3346
4364
|
throw error;
|
|
3347
|
-
const snapshot = await copyWorkspaceSnapshot(options.workspace, baseRoot, snapshotRoot, options.config.maxWorkspaceBytes);
|
|
4365
|
+
const snapshot = await copyWorkspaceSnapshot(options.workspace, baseRoot, snapshotRoot, options.config.maxWorkspaceBytes, ioMetrics);
|
|
3348
4366
|
const now = new Date().toISOString();
|
|
3349
4367
|
metadata = {
|
|
3350
4368
|
schemaVersion: HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION,
|
|
@@ -3363,10 +4381,10 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3363
4381
|
await atomicJson(metadataPath, metadata);
|
|
3364
4382
|
const workspace = await Workspace.open(snapshotRoot);
|
|
3365
4383
|
const base = await Workspace.open(baseRoot);
|
|
3366
|
-
const dependencyPath =
|
|
4384
|
+
const dependencyPath = path8.join(options.workspace.root, "node_modules");
|
|
3367
4385
|
let dependencyRoot;
|
|
3368
4386
|
try {
|
|
3369
|
-
const entry = await
|
|
4387
|
+
const entry = await lstat6(dependencyPath);
|
|
3370
4388
|
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
3371
4389
|
throw new Error("node_modules must be a real directory before it can be mounted read-only.");
|
|
3372
4390
|
}
|
|
@@ -3376,31 +4394,41 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3376
4394
|
throw error;
|
|
3377
4395
|
}
|
|
3378
4396
|
}
|
|
3379
|
-
const
|
|
4397
|
+
const validateCommand = (command, args) => {
|
|
3380
4398
|
if (!options.config.allowedCommands.includes(command)) {
|
|
3381
4399
|
throw new Error(`The OCI command "${command}" is not in the explicit allowlist.`);
|
|
3382
4400
|
}
|
|
3383
4401
|
if (args.length > 256 || args.some((value) => value.length > 8192 || value.includes("\x00"))) {
|
|
3384
4402
|
throw new Error("OCI command arguments exceed the bounded argument contract.");
|
|
3385
4403
|
}
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
}
|
|
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) => {
|
|
4424
|
+
if (result.sessionReused === false)
|
|
4425
|
+
ioMetrics.containerStarts += 1;
|
|
4426
|
+
if (result.sessionReused === true)
|
|
4427
|
+
ioMetrics.containerReuses += 1;
|
|
4428
|
+
if (result.workspacePublished === true)
|
|
4429
|
+
ioMetrics.workspacePublishes += 1;
|
|
4430
|
+
if (result.workspaceExported === true)
|
|
4431
|
+
ioMetrics.workspaceExports += 1;
|
|
3404
4432
|
const forcedExitCode = result.cancelled ? 130 : result.timedOut ? 124 : result.outputLimitExceeded ? 125 : result.exitCode;
|
|
3405
4433
|
return {
|
|
3406
4434
|
command: result.command,
|
|
@@ -3413,7 +4441,45 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3413
4441
|
...result.outputLimitExceeded ? ["OCI command exceeded the output limit."] : []
|
|
3414
4442
|
].filter(Boolean).join(`
|
|
3415
4443
|
`),
|
|
3416
|
-
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)
|
|
3417
4483
|
};
|
|
3418
4484
|
};
|
|
3419
4485
|
const session = {
|
|
@@ -3433,6 +4499,22 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3433
4499
|
return { decision: "deny", reason: "The requested OCI executable is not allowlisted." };
|
|
3434
4500
|
}
|
|
3435
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
|
+
}
|
|
3436
4518
|
return {
|
|
3437
4519
|
decision: "allow",
|
|
3438
4520
|
metadata: {
|
|
@@ -3447,7 +4529,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3447
4529
|
return await operation();
|
|
3448
4530
|
},
|
|
3449
4531
|
async status() {
|
|
3450
|
-
const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes);
|
|
4532
|
+
const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes, ioMetrics);
|
|
3451
4533
|
return {
|
|
3452
4534
|
schemaVersion: HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION,
|
|
3453
4535
|
kind: "environment-status",
|
|
@@ -3461,32 +4543,27 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3461
4543
|
rootFilesystem: "read-only",
|
|
3462
4544
|
workspace: "ephemeral-snapshot",
|
|
3463
4545
|
patchId: patch.patchId,
|
|
3464
|
-
changedFiles: patch.entries.length
|
|
4546
|
+
changedFiles: patch.entries.length,
|
|
4547
|
+
io: { ...ioMetrics }
|
|
3465
4548
|
};
|
|
3466
4549
|
},
|
|
3467
|
-
runCommand,
|
|
4550
|
+
runCommand: runCommand2,
|
|
4551
|
+
runCommandBatch,
|
|
3468
4552
|
async runCheck(check, expectedScript, allowedChecks, context) {
|
|
3469
|
-
if (!/^[A-Za-z0-9:_-]+$/.test(check) || !allowedChecks.includes(check)) {
|
|
3470
|
-
throw new Error(`The check "${check}" is not in the explicit allowlist.`);
|
|
3471
|
-
}
|
|
3472
4553
|
let manifestDocument;
|
|
3473
4554
|
try {
|
|
3474
|
-
manifestDocument = JSON.parse(await readFile3(
|
|
4555
|
+
manifestDocument = JSON.parse(await readFile3(path8.join(snapshotRoot, "package.json"), "utf8"));
|
|
3475
4556
|
} catch (error) {
|
|
3476
4557
|
if (error.code === "ENOENT") {
|
|
3477
4558
|
throw new Error("The environment snapshot does not contain package.json.");
|
|
3478
4559
|
}
|
|
3479
4560
|
throw error;
|
|
3480
4561
|
}
|
|
3481
|
-
const
|
|
3482
|
-
|
|
3483
|
-
throw new Error(`package.json does not define the "${check}" script.`);
|
|
3484
|
-
if (actual !== expectedScript)
|
|
3485
|
-
throw new Error(`The "${check}" script changed or does not match expectedScript.`);
|
|
3486
|
-
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);
|
|
3487
4564
|
},
|
|
3488
4565
|
async inspectPatch() {
|
|
3489
|
-
const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes);
|
|
4566
|
+
const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes, ioMetrics);
|
|
3490
4567
|
const payload = patchPayload(request.runId, patch.entries);
|
|
3491
4568
|
return {
|
|
3492
4569
|
...payload,
|
|
@@ -3495,7 +4572,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3495
4572
|
};
|
|
3496
4573
|
},
|
|
3497
4574
|
importPatch(host, patchId) {
|
|
3498
|
-
return importPatch(request.runId, patchId, host, base, workspace, options.config.maxFileWriteBytes);
|
|
4575
|
+
return importPatch(request.runId, patchId, host, base, workspace, options.config.maxFileWriteBytes, ioMetrics);
|
|
3499
4576
|
},
|
|
3500
4577
|
async release(result) {
|
|
3501
4578
|
await runtime.removeRunContainers(request.runId);
|
|
@@ -3509,7 +4586,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3509
4586
|
};
|
|
3510
4587
|
};
|
|
3511
4588
|
var cleanupHarnessExecutionArtifacts = async (stateDirectory, before) => {
|
|
3512
|
-
const root =
|
|
4589
|
+
const root = path8.join(stateDirectory, "environments");
|
|
3513
4590
|
const result = { scanned: 0, deleted: 0, skipped: 0 };
|
|
3514
4591
|
let entries;
|
|
3515
4592
|
try {
|
|
@@ -3525,15 +4602,15 @@ var cleanupHarnessExecutionArtifacts = async (stateDirectory, before) => {
|
|
|
3525
4602
|
continue;
|
|
3526
4603
|
}
|
|
3527
4604
|
result.scanned += 1;
|
|
3528
|
-
const directory =
|
|
4605
|
+
const directory = path8.join(root, entry.name);
|
|
3529
4606
|
const resolved = await realpath3(directory);
|
|
3530
4607
|
if (!isInside3(root, resolved)) {
|
|
3531
4608
|
result.skipped += 1;
|
|
3532
4609
|
continue;
|
|
3533
4610
|
}
|
|
3534
4611
|
try {
|
|
3535
|
-
const metadataPath =
|
|
3536
|
-
const metadataEntry = await
|
|
4612
|
+
const metadataPath = path8.join(directory, "environment.json");
|
|
4613
|
+
const metadataEntry = await lstat6(metadataPath);
|
|
3537
4614
|
if (metadataEntry.isSymbolicLink() || !metadataEntry.isFile())
|
|
3538
4615
|
throw new Error("unsafe metadata");
|
|
3539
4616
|
const metadata = JSON.parse(await readFile3(metadataPath, "utf8"));
|
|
@@ -3559,7 +4636,7 @@ var describeOciCommand = (command, args) => commandDisplay([command, ...args]);
|
|
|
3559
4636
|
|
|
3560
4637
|
// src/harness.ts
|
|
3561
4638
|
var APPROVAL_VERSION = "2026-08-17-v5";
|
|
3562
|
-
var TOOL_CONTRACT_VERSION = "
|
|
4639
|
+
var TOOL_CONTRACT_VERSION = "workspace-verified-transaction-v3";
|
|
3563
4640
|
var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, providerTransportFingerprint, executionEnvironment) => ({
|
|
3564
4641
|
schemaVersion: 1,
|
|
3565
4642
|
id: "zhivex-harness",
|
|
@@ -3589,14 +4666,17 @@ var HARNESS_INSTRUCTIONS = `You are Zhivex Harness, a provider-portable coding a
|
|
|
3589
4666
|
|
|
3590
4667
|
Rules:
|
|
3591
4668
|
- Match the user's language.
|
|
3592
|
-
- 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.
|
|
3593
4670
|
- Use only workspace-relative paths. Never request or expose secrets.
|
|
3594
4671
|
- Make the smallest coherent change that fully addresses the task.
|
|
3595
4672
|
- For every file edit, first read its digest and call propose_edits. Apply exactly that reviewed proposal with apply_patch.
|
|
3596
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.
|
|
3597
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.
|
|
3598
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.
|
|
3599
|
-
- 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.
|
|
3600
4680
|
- Never overwrite stale content. If an expected digest no longer matches, inspect the file again and create a new proposal.
|
|
3601
4681
|
- Deletions are recoverable: use quarantine_file, never permanent deletion. Use restore_file to recover quarantined content.
|
|
3602
4682
|
- Never claim a check passed unless run_check returned exitCode 0.
|
|
@@ -3635,20 +4715,29 @@ var mutationApproval = {
|
|
|
3635
4715
|
approvalVersion: APPROVAL_VERSION,
|
|
3636
4716
|
metadata: toolMetadata(["filesystem", "write"], "high")
|
|
3637
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
|
+
});
|
|
3638
4726
|
var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
3639
4727
|
list_files: tool({
|
|
3640
4728
|
name: "list_files",
|
|
3641
|
-
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.",
|
|
3642
4730
|
schema: z3.object({
|
|
3643
4731
|
path: z3.string().min(1).default("."),
|
|
3644
4732
|
limit: z3.number().int().min(1).max(500).default(200),
|
|
3645
|
-
|
|
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.")
|
|
3646
4735
|
}),
|
|
3647
4736
|
metadata: readOnlyMetadata,
|
|
3648
|
-
execute: async ({ path:
|
|
3649
|
-
|
|
3650
|
-
...cursor ? { cursor } : {}
|
|
3651
|
-
}
|
|
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
|
+
}
|
|
3652
4741
|
}),
|
|
3653
4742
|
read_file: tool({
|
|
3654
4743
|
name: "read_file",
|
|
@@ -3659,7 +4748,24 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
3659
4748
|
endLine: z3.number().int().min(1).optional()
|
|
3660
4749
|
}),
|
|
3661
4750
|
metadata: readOnlyMetadata,
|
|
3662
|
-
execute: async ({ path:
|
|
4751
|
+
execute: async ({ path: path9, startLine, endLine }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).readFile(path9, startLine, endLine))
|
|
4752
|
+
}),
|
|
4753
|
+
read_files: tool({
|
|
4754
|
+
name: "read_files",
|
|
4755
|
+
description: "Read up to 20 independent UTF-8 file slices in one bounded call. Duplicate paths are read once and results use deterministic path/range order.",
|
|
4756
|
+
schema: z3.object({
|
|
4757
|
+
files: z3.array(z3.object({
|
|
4758
|
+
path: z3.string().min(1),
|
|
4759
|
+
startLine: z3.number().int().min(1).default(1),
|
|
4760
|
+
endLine: z3.number().int().min(1).optional()
|
|
4761
|
+
})).min(1).max(20)
|
|
4762
|
+
}),
|
|
4763
|
+
metadata: readOnlyMetadata,
|
|
4764
|
+
execute: async ({ files }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).readFiles(files.map(({ path: path9, startLine, endLine }) => ({
|
|
4765
|
+
path: path9,
|
|
4766
|
+
startLine,
|
|
4767
|
+
...endLine !== undefined ? { endLine } : {}
|
|
4768
|
+
}))))
|
|
3663
4769
|
}),
|
|
3664
4770
|
search_files: tool({
|
|
3665
4771
|
name: "search_files",
|
|
@@ -3672,12 +4778,34 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
3672
4778
|
cursor: z3.string().min(1).max(2000).optional()
|
|
3673
4779
|
}),
|
|
3674
4780
|
metadata: readOnlyMetadata,
|
|
3675
|
-
execute: async ({ query, path:
|
|
4781
|
+
execute: async ({ query, path: path9, caseSensitive, limit, cursor }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).searchFiles(query, path9, {
|
|
3676
4782
|
caseSensitive,
|
|
3677
4783
|
limit,
|
|
3678
4784
|
...cursor ? { cursor } : {}
|
|
3679
4785
|
}))
|
|
3680
4786
|
}),
|
|
4787
|
+
search_many: tool({
|
|
4788
|
+
name: "search_many",
|
|
4789
|
+
description: "Search up to 10 independent literal queries in one workspace pass, with at most 500 aggregate matches.",
|
|
4790
|
+
schema: z3.object({
|
|
4791
|
+
queries: z3.array(z3.object({
|
|
4792
|
+
query: z3.string().min(1).max(200),
|
|
4793
|
+
caseSensitive: z3.boolean().default(false)
|
|
4794
|
+
})).min(1).max(10),
|
|
4795
|
+
path: z3.string().min(1).default("."),
|
|
4796
|
+
limitPerQuery: z3.number().int().min(1).max(500).default(50)
|
|
4797
|
+
}).superRefine((input, context) => {
|
|
4798
|
+
if (input.queries.length * input.limitPerQuery > 500) {
|
|
4799
|
+
context.addIssue({
|
|
4800
|
+
code: "custom",
|
|
4801
|
+
path: ["limitPerQuery"],
|
|
4802
|
+
message: "search_many allows at most 500 aggregate matches."
|
|
4803
|
+
});
|
|
4804
|
+
}
|
|
4805
|
+
}),
|
|
4806
|
+
metadata: readOnlyMetadata,
|
|
4807
|
+
execute: async ({ queries, path: path9, limitPerQuery }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).searchMany(queries, path9, { limitPerQuery }))
|
|
4808
|
+
}),
|
|
3681
4809
|
propose_edits: tool({
|
|
3682
4810
|
name: "propose_edits",
|
|
3683
4811
|
description: "Validate a bounded multi-file edit against current SHA-256 digests and return a deterministic proposalId for operator review. This tool does not write files.",
|
|
@@ -3698,6 +4826,19 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
3698
4826
|
return serializeJsonValue2(editContractDocument("patch-result", await (harnessExecutionSession(context)?.workspace ?? workspace).applyPatch(proposal)));
|
|
3699
4827
|
}
|
|
3700
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
|
+
}),
|
|
3701
4842
|
move_file: tool({
|
|
3702
4843
|
name: "move_file",
|
|
3703
4844
|
description: "Move one regular file without overwriting the destination. The source must still match expectedDigest.",
|
|
@@ -3721,7 +4862,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
3721
4862
|
}),
|
|
3722
4863
|
run_check: tool({
|
|
3723
4864
|
name: "run_check",
|
|
3724
|
-
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.`,
|
|
3725
4866
|
schema: z3.object({
|
|
3726
4867
|
check: z3.string().min(1).max(100).regex(/^[A-Za-z0-9:_-]+$/),
|
|
3727
4868
|
expectedScript: z3.string().min(1).max(2000)
|
|
@@ -3773,6 +4914,29 @@ var createExecutionEnvironmentTools = (workspace) => ({
|
|
|
3773
4914
|
metadata: toolMetadata(["code-execution", "filesystem"], "high"),
|
|
3774
4915
|
execute: async ({ command, args }, context) => serializeJsonValue2(await requireExecutionSession(context).runCommand(command, args, context))
|
|
3775
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
|
+
}),
|
|
3776
4940
|
environment_status: tool({
|
|
3777
4941
|
name: "environment_status",
|
|
3778
4942
|
description: "Inspect the immutable image binding and enforced policy for the active run without exposing host paths or environment variables.",
|
|
@@ -3793,6 +4957,83 @@ var createExecutionEnvironmentTools = (workspace) => ({
|
|
|
3793
4957
|
schema: z3.strictObject({ patchId: fileDigestSchema }),
|
|
3794
4958
|
...mutationApproval,
|
|
3795
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
|
+
}
|
|
3796
5037
|
})
|
|
3797
5038
|
});
|
|
3798
5039
|
var estimateMessageTokens = (messages) => Math.max(1, Math.ceil(JSON.stringify(messages).length / 4));
|
|
@@ -3803,7 +5044,7 @@ var summarizeHarnessMessages = (messages) => {
|
|
|
3803
5044
|
const parts = (record.parts ?? []).map((part) => {
|
|
3804
5045
|
if (part.type === "text" && typeof part.text === "string") {
|
|
3805
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();
|
|
3806
|
-
return text.length > 160 ? `${text.slice(0, 160)}
|
|
5047
|
+
return text.length > 160 ? `${text.slice(0, 160)}…` : text;
|
|
3807
5048
|
}
|
|
3808
5049
|
if (part.type === "tool-call") {
|
|
3809
5050
|
const call = part.toolCall;
|
|
@@ -3819,7 +5060,7 @@ var summarizeHarnessMessages = (messages) => {
|
|
|
3819
5060
|
});
|
|
3820
5061
|
const summary = summaries.join(`
|
|
3821
5062
|
`);
|
|
3822
|
-
return summary.length > 4000 ? `${summary.slice(0, 4000)}
|
|
5063
|
+
return summary.length > 4000 ? `${summary.slice(0, 4000)}…` : summary;
|
|
3823
5064
|
};
|
|
3824
5065
|
var compactHarnessMessages = (messages) => {
|
|
3825
5066
|
if (messages.length === 0)
|
|
@@ -4024,6 +5265,272 @@ Available bounded delegations: ${config.orchestration.profiles.map((profile) =>
|
|
|
4024
5265
|
}
|
|
4025
5266
|
};
|
|
4026
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
|
+
};
|
|
4027
5534
|
var runHarness = async (harness, input, options = {}) => {
|
|
4028
5535
|
let nextInput = input;
|
|
4029
5536
|
const continuationOptions = {
|
|
@@ -4064,6 +5571,17 @@ var runHarness = async (harness, input, options = {}) => {
|
|
|
4064
5571
|
if (!approvals) {
|
|
4065
5572
|
return result;
|
|
4066
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
|
+
}
|
|
4067
5585
|
nextInput = {
|
|
4068
5586
|
...continuationOptions,
|
|
4069
5587
|
state: result.state,
|
|
@@ -4251,11 +5769,10 @@ var streamResultDocument = (result, sequence = 0) => {
|
|
|
4251
5769
|
var serializeStreamResult = (result, sequence = 0) => JSON.stringify(streamResultDocument(result, sequence));
|
|
4252
5770
|
|
|
4253
5771
|
// src/sessions.ts
|
|
4254
|
-
import { createHash as createHash8, randomUUID as randomUUID4 } from "crypto";
|
|
4255
|
-
import { chmod as chmod3, lstat as
|
|
4256
|
-
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";
|
|
4257
5775
|
import { createRedactionPolicy as createRedactionPolicy3 } from "@zhivex-ai/agents";
|
|
4258
|
-
import { Database as Database2 } from "bun:sqlite";
|
|
4259
5776
|
var HARNESS_SESSION_INDEX_FILE = "operations.sqlite";
|
|
4260
5777
|
var HARNESS_SESSION_SCHEMA_VERSION = 1;
|
|
4261
5778
|
var SESSION_RUN_STATUSES = [
|
|
@@ -4359,19 +5876,19 @@ var sessionId = () => `ses_${randomUUID4()}`;
|
|
|
4359
5876
|
var turnId = () => `turn_${randomUUID4()}`;
|
|
4360
5877
|
var isMissing = (error) => error.code === "ENOENT";
|
|
4361
5878
|
var ensurePrivateDatabase = async (workspace, stateDirectory) => {
|
|
4362
|
-
const requestedStateDirectory =
|
|
5879
|
+
const requestedStateDirectory = path9.resolve(stateDirectory);
|
|
4363
5880
|
await validateStateDirectory(workspace, requestedStateDirectory);
|
|
4364
5881
|
await mkdir4(requestedStateDirectory, { recursive: true, mode: 448 });
|
|
4365
|
-
const directoryEntry = await
|
|
5882
|
+
const directoryEntry = await lstat7(requestedStateDirectory);
|
|
4366
5883
|
if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
|
|
4367
5884
|
throw new Error(`The session state directory must be a real directory: ${requestedStateDirectory}.`);
|
|
4368
5885
|
}
|
|
4369
5886
|
await chmod3(requestedStateDirectory, 448);
|
|
4370
5887
|
const canonicalStateDirectory2 = await realpath4(requestedStateDirectory);
|
|
4371
|
-
const databasePath =
|
|
5888
|
+
const databasePath = path9.join(canonicalStateDirectory2, HARNESS_SESSION_INDEX_FILE);
|
|
4372
5889
|
let databaseEntry;
|
|
4373
5890
|
try {
|
|
4374
|
-
databaseEntry = await
|
|
5891
|
+
databaseEntry = await lstat7(databasePath);
|
|
4375
5892
|
} catch (error) {
|
|
4376
5893
|
if (!isMissing(error)) {
|
|
4377
5894
|
throw error;
|
|
@@ -4384,7 +5901,7 @@ var ensurePrivateDatabase = async (workspace, stateDirectory) => {
|
|
|
4384
5901
|
throw creationError;
|
|
4385
5902
|
}
|
|
4386
5903
|
}
|
|
4387
|
-
databaseEntry = await
|
|
5904
|
+
databaseEntry = await lstat7(databasePath);
|
|
4388
5905
|
}
|
|
4389
5906
|
if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
|
|
4390
5907
|
throw new Error(`The session SQLite path must be a real file: ${databasePath}.`);
|
|
@@ -4408,7 +5925,7 @@ var toRunReference = (row) => ({
|
|
|
4408
5925
|
var metadataSize = (input) => Object.values(input).reduce((total, value) => total + (typeof value === "string" ? byteLength(value) : 0), 0);
|
|
4409
5926
|
var isTerminal = (status) => TERMINAL_STATUSES2.has(status);
|
|
4410
5927
|
var openCliSessionStore = async (options) => {
|
|
4411
|
-
const workspace = await realpath4(
|
|
5928
|
+
const workspace = await realpath4(path9.resolve(options.workspace));
|
|
4412
5929
|
const workspaceEntry = await stat3(workspace);
|
|
4413
5930
|
if (!workspaceEntry.isDirectory()) {
|
|
4414
5931
|
throw new Error(`The session workspace is not a directory: ${workspace}.`);
|
|
@@ -4423,7 +5940,7 @@ var openCliSessionStore = async (options) => {
|
|
|
4423
5940
|
const maxIndexBytes = boundedInteger("maxIndexBytes", options.maxIndexBytes, DEFAULT_MAX_INDEX_BYTES, 1024);
|
|
4424
5941
|
const now = options.now ?? Date.now;
|
|
4425
5942
|
const { databasePath, databaseEntry } = await ensurePrivateDatabase(workspace, options.stateDirectory);
|
|
4426
|
-
const database = new
|
|
5943
|
+
const database = new SqliteDatabase(databasePath, { create: false, strict: true });
|
|
4427
5944
|
let closed = false;
|
|
4428
5945
|
const assertOpen = () => {
|
|
4429
5946
|
if (closed) {
|
|
@@ -4543,7 +6060,7 @@ var openCliSessionStore = async (options) => {
|
|
|
4543
6060
|
`).run(candidateTurnId, sourceTurnId ?? null, candidateSessionId, sequence, runId, provider, model, role, status, createdAt, isTerminal(status) ? createdAt : null, entryBytes);
|
|
4544
6061
|
};
|
|
4545
6062
|
try {
|
|
4546
|
-
const openedEntry = await
|
|
6063
|
+
const openedEntry = await lstat7(databasePath);
|
|
4547
6064
|
if (openedEntry.isSymbolicLink() || !openedEntry.isFile() || openedEntry.dev !== databaseEntry.dev || openedEntry.ino !== databaseEntry.ino) {
|
|
4548
6065
|
throw new Error(`The session SQLite path changed while it was being opened: ${databasePath}.`);
|
|
4549
6066
|
}
|
|
@@ -4901,6 +6418,512 @@ var openCliSessionStore = async (options) => {
|
|
|
4901
6418
|
};
|
|
4902
6419
|
var openSessionStore = openCliSessionStore;
|
|
4903
6420
|
|
|
6421
|
+
// src/change-envelope.ts
|
|
6422
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
6423
|
+
import { z as z4 } from "zod";
|
|
6424
|
+
var CHANGE_ENVELOPE_SCHEMA_VERSION = 1;
|
|
6425
|
+
var CHANGE_ENVELOPE_DIGEST_ALGORITHM = "sha256";
|
|
6426
|
+
var MAX_CHANGE_ENVELOPE_CHECKS = 100;
|
|
6427
|
+
var MAX_CHANGE_ENVELOPE_APPROVALS = 100;
|
|
6428
|
+
var MAX_CHANGE_ENVELOPE_APPROVAL_SCOPES = 32;
|
|
6429
|
+
var MAX_CHANGE_ENVELOPE_ATTESTATIONS = 32;
|
|
6430
|
+
var timestampSchema = z4.iso.datetime({ precision: 3 });
|
|
6431
|
+
var boundedIdentifierSchema = z4.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/, "Identifier must contain only bounded non-secret token characters.");
|
|
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.");
|
|
6433
|
+
var attestationFormatSchema = z4.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9.+_-]*$/);
|
|
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!#$&^_.+-]*$/);
|
|
6435
|
+
var addDuplicateIssues = (values, key, context, path10) => {
|
|
6436
|
+
const seen = new Set;
|
|
6437
|
+
for (let index = 0;index < values.length; index += 1) {
|
|
6438
|
+
const value = values[index];
|
|
6439
|
+
if (value === undefined)
|
|
6440
|
+
continue;
|
|
6441
|
+
const identifier2 = key(value);
|
|
6442
|
+
if (seen.has(identifier2)) {
|
|
6443
|
+
context.addIssue({
|
|
6444
|
+
code: "custom",
|
|
6445
|
+
path: [index, path10],
|
|
6446
|
+
message: `Duplicate ${path10}: ${identifier2}.`
|
|
6447
|
+
});
|
|
6448
|
+
}
|
|
6449
|
+
seen.add(identifier2);
|
|
6450
|
+
}
|
|
6451
|
+
};
|
|
6452
|
+
var addCanonicalOrderIssue = (values, key, context, path10) => {
|
|
6453
|
+
for (let index = 1;index < values.length; index += 1) {
|
|
6454
|
+
const previous = values[index - 1];
|
|
6455
|
+
const current = values[index];
|
|
6456
|
+
if (previous !== undefined && current !== undefined && key(previous) >= key(current)) {
|
|
6457
|
+
context.addIssue({
|
|
6458
|
+
code: "custom",
|
|
6459
|
+
path: [index, path10],
|
|
6460
|
+
message: `${path10} entries must be unique and in canonical ascending order.`
|
|
6461
|
+
});
|
|
6462
|
+
}
|
|
6463
|
+
}
|
|
6464
|
+
};
|
|
6465
|
+
var approvalScopesInputSchema = z4.array(approvalScopeSchema).min(1).max(MAX_CHANGE_ENVELOPE_APPROVAL_SCOPES).superRefine((scopes, context) => addDuplicateIssues(scopes, (scope) => scope, context, "scope"));
|
|
6466
|
+
var canonicalApprovalScopesSchema = z4.array(approvalScopeSchema).min(1).max(MAX_CHANGE_ENVELOPE_APPROVAL_SCOPES).superRefine((scopes, context) => addCanonicalOrderIssue(scopes, (scope) => scope, context, "scope"));
|
|
6467
|
+
var changeEnvelopeCheckSchema = z4.strictObject({
|
|
6468
|
+
checkId: boundedIdentifierSchema,
|
|
6469
|
+
status: z4.enum(["passed", "failed", "skipped"]),
|
|
6470
|
+
redacted: z4.literal(true),
|
|
6471
|
+
startedAt: timestampSchema,
|
|
6472
|
+
completedAt: timestampSchema,
|
|
6473
|
+
durationMs: z4.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
6474
|
+
exitCode: z4.number().int().min(0).max(255).optional(),
|
|
6475
|
+
redactedOutputDigest: fileDigestSchema.optional(),
|
|
6476
|
+
runnerFingerprint: fileDigestSchema.optional()
|
|
6477
|
+
}).superRefine((check, context) => {
|
|
6478
|
+
const startedAt = Date.parse(check.startedAt);
|
|
6479
|
+
const completedAt = Date.parse(check.completedAt);
|
|
6480
|
+
if (completedAt < startedAt) {
|
|
6481
|
+
context.addIssue({
|
|
6482
|
+
code: "custom",
|
|
6483
|
+
path: ["completedAt"],
|
|
6484
|
+
message: "Check completion cannot precede its start."
|
|
6485
|
+
});
|
|
6486
|
+
}
|
|
6487
|
+
if (check.status === "passed" && check.exitCode !== undefined && check.exitCode !== 0) {
|
|
6488
|
+
context.addIssue({
|
|
6489
|
+
code: "custom",
|
|
6490
|
+
path: ["exitCode"],
|
|
6491
|
+
message: "A passed check cannot have a non-zero exit code."
|
|
6492
|
+
});
|
|
6493
|
+
}
|
|
6494
|
+
if (check.status === "failed" && check.exitCode === 0) {
|
|
6495
|
+
context.addIssue({
|
|
6496
|
+
code: "custom",
|
|
6497
|
+
path: ["exitCode"],
|
|
6498
|
+
message: "A failed check cannot have a zero exit code."
|
|
6499
|
+
});
|
|
6500
|
+
}
|
|
6501
|
+
if (check.status === "skipped" && check.exitCode !== undefined) {
|
|
6502
|
+
context.addIssue({
|
|
6503
|
+
code: "custom",
|
|
6504
|
+
path: ["exitCode"],
|
|
6505
|
+
message: "A skipped check cannot have an exit code."
|
|
6506
|
+
});
|
|
6507
|
+
}
|
|
6508
|
+
});
|
|
6509
|
+
var checkInputListSchema = z4.array(changeEnvelopeCheckSchema).min(1).max(MAX_CHANGE_ENVELOPE_CHECKS).superRefine((checks, context) => addDuplicateIssues(checks, (check) => check.checkId, context, "checkId"));
|
|
6510
|
+
var canonicalCheckListSchema = z4.array(changeEnvelopeCheckSchema).min(1).max(MAX_CHANGE_ENVELOPE_CHECKS).superRefine((checks, context) => addCanonicalOrderIssue(checks, (check) => check.checkId, context, "checkId"));
|
|
6511
|
+
var changeEnvelopeApprovalSchema = z4.strictObject({
|
|
6512
|
+
kind: z4.literal("approval-reference"),
|
|
6513
|
+
approvalId: boundedIdentifierSchema,
|
|
6514
|
+
approvalDigest: fileDigestSchema,
|
|
6515
|
+
approverDigest: fileDigestSchema,
|
|
6516
|
+
scopes: canonicalApprovalScopesSchema,
|
|
6517
|
+
approvedAt: timestampSchema,
|
|
6518
|
+
expiresAt: timestampSchema.optional()
|
|
6519
|
+
}).superRefine((approval, context) => {
|
|
6520
|
+
if (approval.expiresAt && Date.parse(approval.expiresAt) <= Date.parse(approval.approvedAt)) {
|
|
6521
|
+
context.addIssue({
|
|
6522
|
+
code: "custom",
|
|
6523
|
+
path: ["expiresAt"],
|
|
6524
|
+
message: "Approval expiry must be later than approval time."
|
|
6525
|
+
});
|
|
6526
|
+
}
|
|
6527
|
+
});
|
|
6528
|
+
var changeEnvelopeApprovalInputSchema = z4.strictObject({
|
|
6529
|
+
kind: z4.literal("approval-reference"),
|
|
6530
|
+
approvalId: boundedIdentifierSchema,
|
|
6531
|
+
approvalDigest: fileDigestSchema,
|
|
6532
|
+
approverDigest: fileDigestSchema,
|
|
6533
|
+
scopes: approvalScopesInputSchema,
|
|
6534
|
+
approvedAt: timestampSchema,
|
|
6535
|
+
expiresAt: timestampSchema.optional()
|
|
6536
|
+
}).superRefine((approval, context) => {
|
|
6537
|
+
if (approval.expiresAt && Date.parse(approval.expiresAt) <= Date.parse(approval.approvedAt)) {
|
|
6538
|
+
context.addIssue({
|
|
6539
|
+
code: "custom",
|
|
6540
|
+
path: ["expiresAt"],
|
|
6541
|
+
message: "Approval expiry must be later than approval time."
|
|
6542
|
+
});
|
|
6543
|
+
}
|
|
6544
|
+
});
|
|
6545
|
+
var approvalInputListSchema = z4.array(changeEnvelopeApprovalInputSchema).min(1).max(MAX_CHANGE_ENVELOPE_APPROVALS).superRefine((approvals, context) => addDuplicateIssues(approvals, (approval) => approval.approvalId, context, "approvalId"));
|
|
6546
|
+
var canonicalApprovalListSchema = z4.array(changeEnvelopeApprovalSchema).min(1).max(MAX_CHANGE_ENVELOPE_APPROVALS).superRefine((approvals, context) => addCanonicalOrderIssue(approvals, (approval) => approval.approvalId, context, "approvalId"));
|
|
6547
|
+
var externalAttestationReferenceSchema = z4.strictObject({
|
|
6548
|
+
kind: z4.literal("external-attestation-reference"),
|
|
6549
|
+
attestationId: boundedIdentifierSchema,
|
|
6550
|
+
format: attestationFormatSchema,
|
|
6551
|
+
mediaType: mediaTypeSchema,
|
|
6552
|
+
bundleDigest: fileDigestSchema
|
|
6553
|
+
});
|
|
6554
|
+
var externalAttestationInputListSchema = z4.array(externalAttestationReferenceSchema).min(1).max(MAX_CHANGE_ENVELOPE_ATTESTATIONS).superRefine((attestations, context) => addDuplicateIssues(attestations, (attestation) => attestation.attestationId, context, "attestationId"));
|
|
6555
|
+
var canonicalExternalAttestationListSchema = z4.array(externalAttestationReferenceSchema).min(1).max(MAX_CHANGE_ENVELOPE_ATTESTATIONS).superRefine((attestations, context) => addCanonicalOrderIssue(attestations, (attestation) => attestation.attestationId, context, "attestationId"));
|
|
6556
|
+
var changeEnvelopeBaseSchema = z4.strictObject({
|
|
6557
|
+
workspaceDigest: fileDigestSchema,
|
|
6558
|
+
treeDigest: fileDigestSchema
|
|
6559
|
+
});
|
|
6560
|
+
var changeEnvelopePatchSchema = z4.strictObject({
|
|
6561
|
+
patchId: boundedIdentifierSchema,
|
|
6562
|
+
patchDigest: fileDigestSchema
|
|
6563
|
+
});
|
|
6564
|
+
var changeEnvelopeFingerprintsSchema = z4.strictObject({
|
|
6565
|
+
harness: fileDigestSchema,
|
|
6566
|
+
policy: fileDigestSchema,
|
|
6567
|
+
environment: fileDigestSchema
|
|
6568
|
+
});
|
|
6569
|
+
var changeEnvelopeEvidenceSchema = z4.strictObject({
|
|
6570
|
+
schemaVersion: z4.literal(CHANGE_ENVELOPE_SCHEMA_VERSION),
|
|
6571
|
+
kind: z4.literal("check-evidence-summary"),
|
|
6572
|
+
digestAlgorithm: z4.literal(CHANGE_ENVELOPE_DIGEST_ALGORITHM),
|
|
6573
|
+
evidenceDigest: fileDigestSchema,
|
|
6574
|
+
checks: canonicalCheckListSchema
|
|
6575
|
+
});
|
|
6576
|
+
var changeEnvelopeInputSchema = z4.strictObject({
|
|
6577
|
+
createdAt: timestampSchema,
|
|
6578
|
+
expiresAt: timestampSchema.optional(),
|
|
6579
|
+
base: changeEnvelopeBaseSchema,
|
|
6580
|
+
patch: changeEnvelopePatchSchema,
|
|
6581
|
+
fingerprints: changeEnvelopeFingerprintsSchema,
|
|
6582
|
+
checks: checkInputListSchema,
|
|
6583
|
+
approvals: approvalInputListSchema.optional(),
|
|
6584
|
+
externalAttestations: externalAttestationInputListSchema.optional()
|
|
6585
|
+
});
|
|
6586
|
+
var changeEnvelopeSchema = z4.strictObject({
|
|
6587
|
+
schemaVersion: z4.literal(CHANGE_ENVELOPE_SCHEMA_VERSION),
|
|
6588
|
+
kind: z4.literal("change-envelope"),
|
|
6589
|
+
digestAlgorithm: z4.literal(CHANGE_ENVELOPE_DIGEST_ALGORITHM),
|
|
6590
|
+
envelopeId: fileDigestSchema,
|
|
6591
|
+
createdAt: timestampSchema,
|
|
6592
|
+
expiresAt: timestampSchema.optional(),
|
|
6593
|
+
base: changeEnvelopeBaseSchema,
|
|
6594
|
+
patch: changeEnvelopePatchSchema,
|
|
6595
|
+
fingerprints: changeEnvelopeFingerprintsSchema,
|
|
6596
|
+
evidence: changeEnvelopeEvidenceSchema,
|
|
6597
|
+
approvals: canonicalApprovalListSchema.optional(),
|
|
6598
|
+
externalAttestations: canonicalExternalAttestationListSchema.optional()
|
|
6599
|
+
}).superRefine((envelope, context) => {
|
|
6600
|
+
if (envelope.expiresAt && Date.parse(envelope.expiresAt) <= Date.parse(envelope.createdAt)) {
|
|
6601
|
+
context.addIssue({
|
|
6602
|
+
code: "custom",
|
|
6603
|
+
path: ["expiresAt"],
|
|
6604
|
+
message: "Envelope expiry must be later than creation time."
|
|
6605
|
+
});
|
|
6606
|
+
}
|
|
6607
|
+
for (let index = 0;index < envelope.evidence.checks.length; index += 1) {
|
|
6608
|
+
const check = envelope.evidence.checks[index];
|
|
6609
|
+
if (check && Date.parse(check.completedAt) > Date.parse(envelope.createdAt)) {
|
|
6610
|
+
context.addIssue({
|
|
6611
|
+
code: "custom",
|
|
6612
|
+
path: ["evidence", "checks", index, "completedAt"],
|
|
6613
|
+
message: "Check completion cannot be later than envelope creation time."
|
|
6614
|
+
});
|
|
6615
|
+
}
|
|
6616
|
+
}
|
|
6617
|
+
for (let index = 0;index < (envelope.approvals?.length ?? 0); index += 1) {
|
|
6618
|
+
const approval = envelope.approvals?.[index];
|
|
6619
|
+
if (approval && Date.parse(approval.approvedAt) > Date.parse(envelope.createdAt)) {
|
|
6620
|
+
context.addIssue({
|
|
6621
|
+
code: "custom",
|
|
6622
|
+
path: ["approvals", index, "approvedAt"],
|
|
6623
|
+
message: "Approval time cannot be later than envelope creation time."
|
|
6624
|
+
});
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
});
|
|
6628
|
+
var canonicalJson2 = (value) => {
|
|
6629
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
6630
|
+
return JSON.stringify(value);
|
|
6631
|
+
}
|
|
6632
|
+
if (typeof value === "number") {
|
|
6633
|
+
if (!Number.isFinite(value))
|
|
6634
|
+
throw new TypeError("Canonical JSON cannot encode a non-finite number.");
|
|
6635
|
+
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
6636
|
+
}
|
|
6637
|
+
if (Array.isArray(value)) {
|
|
6638
|
+
return `[${value.map((entry) => canonicalJson2(entry)).join(",")}]`;
|
|
6639
|
+
}
|
|
6640
|
+
if (typeof value === "object") {
|
|
6641
|
+
const record = value;
|
|
6642
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson2(record[key])}`).join(",")}}`;
|
|
6643
|
+
}
|
|
6644
|
+
throw new TypeError(`Canonical JSON cannot encode ${typeof value}.`);
|
|
6645
|
+
};
|
|
6646
|
+
var digestCanonical = (value) => `sha256:${createHash9("sha256").update(canonicalJson2(value)).digest("hex")}`;
|
|
6647
|
+
var digestChangeEnvelopeArtifact = (value) => `sha256:${createHash9("sha256").update(value).digest("hex")}`;
|
|
6648
|
+
var evidencePayload = (checks) => ({
|
|
6649
|
+
schemaVersion: CHANGE_ENVELOPE_SCHEMA_VERSION,
|
|
6650
|
+
kind: "check-evidence-summary",
|
|
6651
|
+
digestAlgorithm: CHANGE_ENVELOPE_DIGEST_ALGORITHM,
|
|
6652
|
+
checks: [...checks]
|
|
6653
|
+
});
|
|
6654
|
+
var envelopePayload = (envelope) => {
|
|
6655
|
+
const { envelopeId: _envelopeId, ...payload } = envelope;
|
|
6656
|
+
return payload;
|
|
6657
|
+
};
|
|
6658
|
+
var sortBy = (values, key) => [...values].sort((left, right) => {
|
|
6659
|
+
const leftKey = key(left);
|
|
6660
|
+
const rightKey = key(right);
|
|
6661
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
6662
|
+
});
|
|
6663
|
+
var createChangeEnvelope = (input) => {
|
|
6664
|
+
const parsed = changeEnvelopeInputSchema.parse(input);
|
|
6665
|
+
const checks = sortBy(parsed.checks, (check) => check.checkId);
|
|
6666
|
+
const evidenceWithoutDigest = evidencePayload(checks);
|
|
6667
|
+
const evidence = {
|
|
6668
|
+
...evidenceWithoutDigest,
|
|
6669
|
+
evidenceDigest: digestCanonical(evidenceWithoutDigest)
|
|
6670
|
+
};
|
|
6671
|
+
const approvals = parsed.approvals ? sortBy(parsed.approvals, (approval) => approval.approvalId).map((approval) => ({
|
|
6672
|
+
...approval,
|
|
6673
|
+
scopes: [...approval.scopes].sort()
|
|
6674
|
+
})) : undefined;
|
|
6675
|
+
const externalAttestations = parsed.externalAttestations ? sortBy(parsed.externalAttestations, (attestation) => attestation.attestationId) : undefined;
|
|
6676
|
+
const payload = {
|
|
6677
|
+
schemaVersion: CHANGE_ENVELOPE_SCHEMA_VERSION,
|
|
6678
|
+
kind: "change-envelope",
|
|
6679
|
+
digestAlgorithm: CHANGE_ENVELOPE_DIGEST_ALGORITHM,
|
|
6680
|
+
createdAt: parsed.createdAt,
|
|
6681
|
+
...parsed.expiresAt ? { expiresAt: parsed.expiresAt } : {},
|
|
6682
|
+
base: parsed.base,
|
|
6683
|
+
patch: parsed.patch,
|
|
6684
|
+
fingerprints: parsed.fingerprints,
|
|
6685
|
+
evidence,
|
|
6686
|
+
...approvals ? { approvals } : {},
|
|
6687
|
+
...externalAttestations ? { externalAttestations } : {}
|
|
6688
|
+
};
|
|
6689
|
+
return changeEnvelopeSchema.parse({
|
|
6690
|
+
...payload,
|
|
6691
|
+
envelopeId: digestCanonical(payload)
|
|
6692
|
+
});
|
|
6693
|
+
};
|
|
6694
|
+
var canonicalizeChangeEnvelope = (input) => canonicalJson2(changeEnvelopeSchema.parse(input));
|
|
6695
|
+
var computeChangeEnvelopeDigest = (input) => {
|
|
6696
|
+
const parsed = changeEnvelopeSchema.parse(input);
|
|
6697
|
+
return digestCanonical(envelopePayload(parsed));
|
|
6698
|
+
};
|
|
6699
|
+
var computeChangeEnvelopeEvidenceDigest = (input) => {
|
|
6700
|
+
const parsed = changeEnvelopeEvidenceSchema.parse(input);
|
|
6701
|
+
return digestCanonical(evidencePayload(parsed.checks));
|
|
6702
|
+
};
|
|
6703
|
+
var changeEnvelopePreconditionsSchema = z4.strictObject({
|
|
6704
|
+
envelopeId: fileDigestSchema.optional(),
|
|
6705
|
+
baseWorkspaceDigest: fileDigestSchema.optional(),
|
|
6706
|
+
baseTreeDigest: fileDigestSchema.optional(),
|
|
6707
|
+
patchId: boundedIdentifierSchema.optional(),
|
|
6708
|
+
patchDigest: fileDigestSchema.optional(),
|
|
6709
|
+
harnessFingerprint: fileDigestSchema.optional(),
|
|
6710
|
+
policyFingerprint: fileDigestSchema.optional(),
|
|
6711
|
+
environmentFingerprint: fileDigestSchema.optional(),
|
|
6712
|
+
evidenceDigest: fileDigestSchema.optional(),
|
|
6713
|
+
requiredPassedChecks: z4.array(boundedIdentifierSchema).max(MAX_CHANGE_ENVELOPE_CHECKS).optional(),
|
|
6714
|
+
requiredApprovalDigests: z4.array(fileDigestSchema).max(MAX_CHANGE_ENVELOPE_APPROVALS).optional(),
|
|
6715
|
+
requiredApprovalScopes: z4.array(approvalScopeSchema).max(MAX_CHANGE_ENVELOPE_APPROVAL_SCOPES).optional(),
|
|
6716
|
+
requiredAttestationDigests: z4.array(fileDigestSchema).max(MAX_CHANGE_ENVELOPE_ATTESTATIONS).optional()
|
|
6717
|
+
}).superRefine((preconditions, context) => {
|
|
6718
|
+
if (preconditions.requiredPassedChecks) {
|
|
6719
|
+
addDuplicateIssues(preconditions.requiredPassedChecks, (checkId) => checkId, context, "requiredPassedCheck");
|
|
6720
|
+
}
|
|
6721
|
+
if (preconditions.requiredApprovalDigests) {
|
|
6722
|
+
addDuplicateIssues(preconditions.requiredApprovalDigests, (digest3) => digest3, context, "requiredApprovalDigest");
|
|
6723
|
+
}
|
|
6724
|
+
if (preconditions.requiredApprovalScopes) {
|
|
6725
|
+
addDuplicateIssues(preconditions.requiredApprovalScopes, (scope) => scope, context, "requiredApprovalScope");
|
|
6726
|
+
}
|
|
6727
|
+
if (preconditions.requiredAttestationDigests) {
|
|
6728
|
+
addDuplicateIssues(preconditions.requiredAttestationDigests, (digest3) => digest3, context, "requiredAttestationDigest");
|
|
6729
|
+
}
|
|
6730
|
+
});
|
|
6731
|
+
var verificationOptionsSchema = z4.strictObject({
|
|
6732
|
+
now: z4.union([z4.date(), z4.number().int(), timestampSchema]).optional(),
|
|
6733
|
+
preconditions: changeEnvelopePreconditionsSchema.optional()
|
|
6734
|
+
});
|
|
6735
|
+
var verificationTime = (value) => {
|
|
6736
|
+
const date = value === undefined ? new Date : value instanceof Date ? value : new Date(value);
|
|
6737
|
+
if (!Number.isFinite(date.getTime()))
|
|
6738
|
+
throw new TypeError("Verification time must be a valid date.");
|
|
6739
|
+
return date;
|
|
6740
|
+
};
|
|
6741
|
+
var verifyChangeEnvelope = (input, options = {}) => {
|
|
6742
|
+
const parsedOptions = verificationOptionsSchema.parse(options);
|
|
6743
|
+
const now = verificationTime(parsedOptions.now);
|
|
6744
|
+
const checkedAt = now.toISOString();
|
|
6745
|
+
const parsedEnvelope = changeEnvelopeSchema.safeParse(input);
|
|
6746
|
+
if (!parsedEnvelope.success) {
|
|
6747
|
+
return {
|
|
6748
|
+
valid: false,
|
|
6749
|
+
verificationScope: "integrity-expiration-and-preconditions-only",
|
|
6750
|
+
issues: parsedEnvelope.error.issues.map((issue) => ({
|
|
6751
|
+
code: "invalid-schema",
|
|
6752
|
+
path: issue.path.map((segment) => typeof segment === "symbol" ? segment.description ?? "symbol" : segment),
|
|
6753
|
+
message: issue.message
|
|
6754
|
+
})),
|
|
6755
|
+
integrity: { envelopeDigestValid: false, evidenceDigestValid: false },
|
|
6756
|
+
expiration: { checkedAt, envelopeNotYetValid: false, envelopeExpired: false, expiredApprovalIds: [] },
|
|
6757
|
+
checks: {
|
|
6758
|
+
recorded: 0,
|
|
6759
|
+
passed: [],
|
|
6760
|
+
failed: [],
|
|
6761
|
+
skipped: [],
|
|
6762
|
+
requiredPassed: [...parsedOptions.preconditions?.requiredPassedChecks ?? []].sort(),
|
|
6763
|
+
missingPassed: [...parsedOptions.preconditions?.requiredPassedChecks ?? []].sort()
|
|
6764
|
+
},
|
|
6765
|
+
approvals: {
|
|
6766
|
+
recorded: 0,
|
|
6767
|
+
requiredScopes: [...parsedOptions.preconditions?.requiredApprovalScopes ?? []].sort(),
|
|
6768
|
+
missingScopes: [...parsedOptions.preconditions?.requiredApprovalScopes ?? []].sort(),
|
|
6769
|
+
authenticity: "not-verified"
|
|
6770
|
+
},
|
|
6771
|
+
externalAttestations: {
|
|
6772
|
+
referenced: 0,
|
|
6773
|
+
requiredDigests: [...parsedOptions.preconditions?.requiredAttestationDigests ?? []].sort(),
|
|
6774
|
+
missingDigests: [...parsedOptions.preconditions?.requiredAttestationDigests ?? []].sort(),
|
|
6775
|
+
authenticity: "not-verified"
|
|
6776
|
+
}
|
|
6777
|
+
};
|
|
6778
|
+
}
|
|
6779
|
+
const envelope = parsedEnvelope.data;
|
|
6780
|
+
const issues = [];
|
|
6781
|
+
const computedEvidenceDigest = digestCanonical(evidencePayload(envelope.evidence.checks));
|
|
6782
|
+
const evidenceDigestValid = computedEvidenceDigest === envelope.evidence.evidenceDigest;
|
|
6783
|
+
if (!evidenceDigestValid) {
|
|
6784
|
+
issues.push({
|
|
6785
|
+
code: "evidence-digest-mismatch",
|
|
6786
|
+
path: ["evidence", "evidenceDigest"],
|
|
6787
|
+
message: "Evidence digest does not match the canonical redacted check summary.",
|
|
6788
|
+
expected: computedEvidenceDigest,
|
|
6789
|
+
actual: envelope.evidence.evidenceDigest
|
|
6790
|
+
});
|
|
6791
|
+
}
|
|
6792
|
+
const computedEnvelopeDigest = digestCanonical(envelopePayload(envelope));
|
|
6793
|
+
const envelopeDigestValid = computedEnvelopeDigest === envelope.envelopeId;
|
|
6794
|
+
if (!envelopeDigestValid) {
|
|
6795
|
+
issues.push({
|
|
6796
|
+
code: "envelope-digest-mismatch",
|
|
6797
|
+
path: ["envelopeId"],
|
|
6798
|
+
message: "Envelope digest does not match its canonical payload.",
|
|
6799
|
+
expected: computedEnvelopeDigest,
|
|
6800
|
+
actual: envelope.envelopeId
|
|
6801
|
+
});
|
|
6802
|
+
}
|
|
6803
|
+
const envelopeNotYetValid = now.getTime() < Date.parse(envelope.createdAt);
|
|
6804
|
+
if (envelopeNotYetValid) {
|
|
6805
|
+
issues.push({
|
|
6806
|
+
code: "envelope-not-yet-valid",
|
|
6807
|
+
path: ["createdAt"],
|
|
6808
|
+
message: "Change envelope creation time is in the future."
|
|
6809
|
+
});
|
|
6810
|
+
}
|
|
6811
|
+
const envelopeExpired = envelope.expiresAt !== undefined && now.getTime() >= Date.parse(envelope.expiresAt);
|
|
6812
|
+
if (envelopeExpired) {
|
|
6813
|
+
issues.push({
|
|
6814
|
+
code: "envelope-expired",
|
|
6815
|
+
path: ["expiresAt"],
|
|
6816
|
+
message: "Change envelope has expired."
|
|
6817
|
+
});
|
|
6818
|
+
}
|
|
6819
|
+
const expiredApprovalIds = (envelope.approvals ?? []).filter((approval) => approval.expiresAt !== undefined && now.getTime() >= Date.parse(approval.expiresAt)).map((approval) => approval.approvalId);
|
|
6820
|
+
for (const approvalId of expiredApprovalIds) {
|
|
6821
|
+
issues.push({
|
|
6822
|
+
code: "approval-expired",
|
|
6823
|
+
path: ["approvals", envelope.approvals?.findIndex((approval) => approval.approvalId === approvalId) ?? 0, "expiresAt"],
|
|
6824
|
+
message: `Recorded approval has expired: ${approvalId}.`
|
|
6825
|
+
});
|
|
6826
|
+
}
|
|
6827
|
+
const preconditions = parsedOptions.preconditions;
|
|
6828
|
+
const comparePrecondition = (field, expected, actual, path10) => {
|
|
6829
|
+
if (expected !== undefined && expected !== actual) {
|
|
6830
|
+
issues.push({
|
|
6831
|
+
code: "precondition-mismatch",
|
|
6832
|
+
path: path10,
|
|
6833
|
+
message: `Change envelope precondition does not match: ${field}.`,
|
|
6834
|
+
expected,
|
|
6835
|
+
actual
|
|
6836
|
+
});
|
|
6837
|
+
}
|
|
6838
|
+
};
|
|
6839
|
+
comparePrecondition("envelopeId", preconditions?.envelopeId, envelope.envelopeId, ["envelopeId"]);
|
|
6840
|
+
comparePrecondition("baseWorkspaceDigest", preconditions?.baseWorkspaceDigest, envelope.base.workspaceDigest, ["base", "workspaceDigest"]);
|
|
6841
|
+
comparePrecondition("baseTreeDigest", preconditions?.baseTreeDigest, envelope.base.treeDigest, ["base", "treeDigest"]);
|
|
6842
|
+
comparePrecondition("patchId", preconditions?.patchId, envelope.patch.patchId, ["patch", "patchId"]);
|
|
6843
|
+
comparePrecondition("patchDigest", preconditions?.patchDigest, envelope.patch.patchDigest, ["patch", "patchDigest"]);
|
|
6844
|
+
comparePrecondition("harnessFingerprint", preconditions?.harnessFingerprint, envelope.fingerprints.harness, ["fingerprints", "harness"]);
|
|
6845
|
+
comparePrecondition("policyFingerprint", preconditions?.policyFingerprint, envelope.fingerprints.policy, ["fingerprints", "policy"]);
|
|
6846
|
+
comparePrecondition("environmentFingerprint", preconditions?.environmentFingerprint, envelope.fingerprints.environment, ["fingerprints", "environment"]);
|
|
6847
|
+
comparePrecondition("evidenceDigest", preconditions?.evidenceDigest, envelope.evidence.evidenceDigest, ["evidence", "evidenceDigest"]);
|
|
6848
|
+
const passedChecks = envelope.evidence.checks.filter((check) => check.status === "passed").map((check) => check.checkId);
|
|
6849
|
+
const failedChecks = envelope.evidence.checks.filter((check) => check.status === "failed").map((check) => check.checkId);
|
|
6850
|
+
const skippedChecks = envelope.evidence.checks.filter((check) => check.status === "skipped").map((check) => check.checkId);
|
|
6851
|
+
const passedCheckSet = new Set(passedChecks);
|
|
6852
|
+
const requiredPassed = [...preconditions?.requiredPassedChecks ?? []].sort();
|
|
6853
|
+
const missingPassed = requiredPassed.filter((checkId) => !passedCheckSet.has(checkId));
|
|
6854
|
+
for (const checkId of missingPassed) {
|
|
6855
|
+
issues.push({
|
|
6856
|
+
code: "missing-passed-check",
|
|
6857
|
+
path: ["evidence", "checks"],
|
|
6858
|
+
message: `Required passed check is not recorded: ${checkId}.`,
|
|
6859
|
+
expected: checkId
|
|
6860
|
+
});
|
|
6861
|
+
}
|
|
6862
|
+
const expiredApprovals = new Set(expiredApprovalIds);
|
|
6863
|
+
const activeApprovals = (envelope.approvals ?? []).filter((approval) => !expiredApprovals.has(approval.approvalId));
|
|
6864
|
+
const approvalDigests = new Set(activeApprovals.map((approval) => approval.approvalDigest));
|
|
6865
|
+
for (const requiredDigest of preconditions?.requiredApprovalDigests ?? []) {
|
|
6866
|
+
if (!approvalDigests.has(requiredDigest)) {
|
|
6867
|
+
issues.push({
|
|
6868
|
+
code: "missing-approval-digest",
|
|
6869
|
+
path: ["approvals"],
|
|
6870
|
+
message: `Required active approval digest is not recorded: ${requiredDigest}.`,
|
|
6871
|
+
expected: requiredDigest
|
|
6872
|
+
});
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
const approvedScopes = new Set(activeApprovals.flatMap((approval) => approval.scopes));
|
|
6876
|
+
const requiredScopes = [...preconditions?.requiredApprovalScopes ?? []].sort();
|
|
6877
|
+
const missingScopes = requiredScopes.filter((scope) => !approvedScopes.has(scope));
|
|
6878
|
+
for (const scope of missingScopes) {
|
|
6879
|
+
issues.push({
|
|
6880
|
+
code: "missing-approval-scope",
|
|
6881
|
+
path: ["approvals"],
|
|
6882
|
+
message: `Required active approval scope is not recorded: ${scope}.`,
|
|
6883
|
+
expected: scope
|
|
6884
|
+
});
|
|
6885
|
+
}
|
|
6886
|
+
const attestationDigests = new Set((envelope.externalAttestations ?? []).map((attestation) => attestation.bundleDigest));
|
|
6887
|
+
const requiredAttestationDigests = [...preconditions?.requiredAttestationDigests ?? []].sort();
|
|
6888
|
+
const missingAttestationDigests = requiredAttestationDigests.filter((digest3) => !attestationDigests.has(digest3));
|
|
6889
|
+
for (const requiredDigest of missingAttestationDigests) {
|
|
6890
|
+
issues.push({
|
|
6891
|
+
code: "missing-attestation-digest",
|
|
6892
|
+
path: ["externalAttestations"],
|
|
6893
|
+
message: `Required external attestation digest is not referenced: ${requiredDigest}.`,
|
|
6894
|
+
expected: requiredDigest
|
|
6895
|
+
});
|
|
6896
|
+
}
|
|
6897
|
+
return {
|
|
6898
|
+
valid: issues.length === 0,
|
|
6899
|
+
verificationScope: "integrity-expiration-and-preconditions-only",
|
|
6900
|
+
envelope,
|
|
6901
|
+
issues,
|
|
6902
|
+
integrity: { envelopeDigestValid, evidenceDigestValid },
|
|
6903
|
+
expiration: { checkedAt, envelopeNotYetValid, envelopeExpired, expiredApprovalIds },
|
|
6904
|
+
checks: {
|
|
6905
|
+
recorded: envelope.evidence.checks.length,
|
|
6906
|
+
passed: passedChecks,
|
|
6907
|
+
failed: failedChecks,
|
|
6908
|
+
skipped: skippedChecks,
|
|
6909
|
+
requiredPassed,
|
|
6910
|
+
missingPassed
|
|
6911
|
+
},
|
|
6912
|
+
approvals: {
|
|
6913
|
+
recorded: envelope.approvals?.length ?? 0,
|
|
6914
|
+
requiredScopes,
|
|
6915
|
+
missingScopes,
|
|
6916
|
+
authenticity: "not-verified"
|
|
6917
|
+
},
|
|
6918
|
+
externalAttestations: {
|
|
6919
|
+
referenced: envelope.externalAttestations?.length ?? 0,
|
|
6920
|
+
requiredDigests: requiredAttestationDigests,
|
|
6921
|
+
missingDigests: missingAttestationDigests,
|
|
6922
|
+
authenticity: "not-verified"
|
|
6923
|
+
}
|
|
6924
|
+
};
|
|
6925
|
+
};
|
|
6926
|
+
|
|
4904
6927
|
// src/cli.ts
|
|
4905
6928
|
var HARNESS_RESUME_METADATA_KEY = "zhivexHarnessResume";
|
|
4906
6929
|
var HARNESS_RESUME_METADATA_SCHEMA_VERSION = 1;
|
|
@@ -4910,9 +6933,11 @@ var CLI_EXIT_CODES = {
|
|
|
4910
6933
|
usageError: 2,
|
|
4911
6934
|
doctorFailed: 3
|
|
4912
6935
|
};
|
|
4913
|
-
var COMMANDS = new Set(["run", "review", "chat", "providers", "doctor", "resume", "runs", "sessions", "help", "version"]);
|
|
6936
|
+
var COMMANDS = new Set(["run", "review", "chat", "providers", "doctor", "resume", "runs", "sessions", "changes", "help", "version"]);
|
|
4914
6937
|
var RUNS_COMMANDS = new Set(["list", "inspect", "cancel", "cleanup", "export"]);
|
|
4915
6938
|
var SESSIONS_COMMANDS = new Set(["list", "inspect", "rename", "fork", "archive"]);
|
|
6939
|
+
var CHANGES_COMMANDS = new Set(["create", "verify"]);
|
|
6940
|
+
var CLI_TIMESTAMP_SCHEMA = z5.iso.datetime({ precision: 3 });
|
|
4916
6941
|
var RUN_STATUSES = new Set([
|
|
4917
6942
|
"queued",
|
|
4918
6943
|
"running",
|
|
@@ -5132,6 +7157,23 @@ var parseCliArgs = (argv) => {
|
|
|
5132
7157
|
options.mcpConfigPath = optionValue(argv, index, argument);
|
|
5133
7158
|
index += 1;
|
|
5134
7159
|
break;
|
|
7160
|
+
case "--patch":
|
|
7161
|
+
options.patchPath = optionValue(argv, index, argument);
|
|
7162
|
+
index += 1;
|
|
7163
|
+
break;
|
|
7164
|
+
case "--preconditions":
|
|
7165
|
+
options.preconditionsPath = optionValue(argv, index, argument);
|
|
7166
|
+
index += 1;
|
|
7167
|
+
break;
|
|
7168
|
+
case "--now": {
|
|
7169
|
+
const value = optionValue(argv, index, argument);
|
|
7170
|
+
if (!CLI_TIMESTAMP_SCHEMA.safeParse(value).success) {
|
|
7171
|
+
throw new CliUsageError("--now must be a millisecond-precision ISO-8601 UTC timestamp.");
|
|
7172
|
+
}
|
|
7173
|
+
options.verificationTime = value;
|
|
7174
|
+
index += 1;
|
|
7175
|
+
break;
|
|
7176
|
+
}
|
|
5135
7177
|
case "--execution":
|
|
5136
7178
|
options.executionBackend = optionValue(argv, index, argument);
|
|
5137
7179
|
index += 1;
|
|
@@ -5424,6 +7466,9 @@ var parseCliArgs = (argv) => {
|
|
|
5424
7466
|
if (options.jsonl && options.command !== "run" && options.command !== "resume") {
|
|
5425
7467
|
throw new CliUsageError("--jsonl is supported by run and resume.");
|
|
5426
7468
|
}
|
|
7469
|
+
if (options.command !== "changes" && (options.patchPath || options.preconditionsPath || options.verificationTime)) {
|
|
7470
|
+
throw new CliUsageError("--patch, --preconditions, and --now are supported only by changes.");
|
|
7471
|
+
}
|
|
5427
7472
|
if (options.command === "resume") {
|
|
5428
7473
|
const runId = positional.shift();
|
|
5429
7474
|
if (runId) {
|
|
@@ -5473,6 +7518,26 @@ var parseCliArgs = (argv) => {
|
|
|
5473
7518
|
if (positional.length > 0) {
|
|
5474
7519
|
throw new CliUsageError(`sessions ${sessionsCommand} received unexpected positional arguments.`);
|
|
5475
7520
|
}
|
|
7521
|
+
} else if (options.command === "changes") {
|
|
7522
|
+
const changesCommand = positional.shift();
|
|
7523
|
+
if (!changesCommand || !CHANGES_COMMANDS.has(changesCommand)) {
|
|
7524
|
+
throw new CliUsageError("changes requires one of: create, verify.");
|
|
7525
|
+
}
|
|
7526
|
+
options.changesCommand = changesCommand;
|
|
7527
|
+
const artifactPath = positional.shift();
|
|
7528
|
+
if (!artifactPath) {
|
|
7529
|
+
throw new CliUsageError(`changes ${changesCommand} requires an input JSON file.`);
|
|
7530
|
+
}
|
|
7531
|
+
options.artifactPath = artifactPath;
|
|
7532
|
+
if (!options.patchPath) {
|
|
7533
|
+
throw new CliUsageError(`changes ${changesCommand} requires --patch <file>.`);
|
|
7534
|
+
}
|
|
7535
|
+
if (changesCommand === "create" && (options.preconditionsPath || options.verificationTime)) {
|
|
7536
|
+
throw new CliUsageError("--preconditions and --now are only supported by changes verify.");
|
|
7537
|
+
}
|
|
7538
|
+
if (positional.length > 0) {
|
|
7539
|
+
throw new CliUsageError(`changes ${changesCommand} received unexpected positional arguments.`);
|
|
7540
|
+
}
|
|
5476
7541
|
} else if (options.command === "run" || options.command === "review") {
|
|
5477
7542
|
const prompt = positional.join(" ").trim();
|
|
5478
7543
|
if (prompt) {
|
|
@@ -5495,6 +7560,8 @@ Usage:
|
|
|
5495
7560
|
zhx resume [options] <runId> --approve|--deny
|
|
5496
7561
|
zhx runs list [--status <status>] [--limit <n>] [--json]
|
|
5497
7562
|
zhx sessions list|inspect|rename|fork|archive
|
|
7563
|
+
zhx changes create <input.json> --patch <artifact>
|
|
7564
|
+
zhx changes verify <envelope.json> --patch <artifact> [--preconditions <file>]
|
|
5498
7565
|
|
|
5499
7566
|
The long command zhivex-harness remains supported for compatibility.
|
|
5500
7567
|
|
|
@@ -5507,10 +7574,13 @@ Options:
|
|
|
5507
7574
|
--workspace <path> Target workspace (default: cwd)
|
|
5508
7575
|
--state-dir <path> Durable run-state directory
|
|
5509
7576
|
--mcp-config <path> Declarative governed MCP JSON configuration
|
|
7577
|
+
--patch <path> Exact patch/artifact bytes bound to a change envelope
|
|
7578
|
+
--preconditions <path> Verification preconditions JSON for changes verify
|
|
7579
|
+
--now <ISO-8601 UTC> Explicit millisecond verification time (default: current time)
|
|
5510
7580
|
--execution <none|oci> Enforced execution backend (default: none)
|
|
5511
7581
|
--oci-runtime <docker|podman> Local OCI runtime (default: docker)
|
|
5512
7582
|
--oci-image <reference> Preloaded immutable-capable OCI image
|
|
5513
|
-
--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
|
|
5514
7584
|
--oci-max-process-runtime-ms <n> Per-command timeout (default: 120000)
|
|
5515
7585
|
--oci-max-process-output-bytes <n> Combined output ceiling (default: 20000)
|
|
5516
7586
|
--oci-max-memory-mb <n> Container memory ceiling (default: 1024)
|
|
@@ -5562,7 +7632,7 @@ Credentials:
|
|
|
5562
7632
|
Gemini: GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY`;
|
|
5563
7633
|
var summarizeApproval = (approval) => {
|
|
5564
7634
|
const mustShowCompleteProposal = approval.name === "apply_patch";
|
|
5565
|
-
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;
|
|
5566
7636
|
return `[${approval.kind ?? "provider"}] ${approval.name}
|
|
5567
7637
|
${argumentsText}`;
|
|
5568
7638
|
};
|
|
@@ -5684,7 +7754,7 @@ var printTerminalResult = (result, harness, output, tracker) => {
|
|
|
5684
7754
|
`);
|
|
5685
7755
|
}
|
|
5686
7756
|
process.stderr.write(`
|
|
5687
|
-
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
|
|
5688
7758
|
`);
|
|
5689
7759
|
if (result.status === "waiting_approval") {
|
|
5690
7760
|
for (const approval of result.state.pendingApprovals) {
|
|
@@ -5714,11 +7784,11 @@ var orchestrationObserver = (json) => async (event) => {
|
|
|
5714
7784
|
return;
|
|
5715
7785
|
if (event.type === "subagent-start") {
|
|
5716
7786
|
process.stderr.write(`
|
|
5717
|
-
subagent start
|
|
7787
|
+
subagent start · ${event.childAgentId ?? event.toolName}
|
|
5718
7788
|
`);
|
|
5719
7789
|
} else if (event.type === "subagent-finish") {
|
|
5720
7790
|
process.stderr.write(`
|
|
5721
|
-
subagent finish
|
|
7791
|
+
subagent finish · ${event.childRun.agentId ?? event.childRun.toolName} · ${event.childRun.status}
|
|
5722
7792
|
`);
|
|
5723
7793
|
}
|
|
5724
7794
|
};
|
|
@@ -5984,9 +8054,9 @@ var chat = async (options) => {
|
|
|
5984
8054
|
const current = await refreshSession();
|
|
5985
8055
|
const latest = current.runs.at(-1);
|
|
5986
8056
|
const routeText = routes.size === 0 ? "default" : [...routes.values()].map((route) => `${route.profile}=${route.provider}:${route.model}`).join(", ");
|
|
5987
|
-
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})` : ""}`;
|
|
5988
8058
|
};
|
|
5989
|
-
process.stderr.write(`Zhivex Harness ${HARNESS_VERSION}
|
|
8059
|
+
process.stderr.write(`Zhivex Harness ${HARNESS_VERSION} · ${harness.config.provider}/${harness.config.model}
|
|
5990
8060
|
` + `session ${session.sessionId}
|
|
5991
8061
|
` + `Type /help for console commands.
|
|
5992
8062
|
`);
|
|
@@ -6001,9 +8071,9 @@ var chat = async (options) => {
|
|
|
6001
8071
|
break;
|
|
6002
8072
|
}
|
|
6003
8073
|
if (prompt === "/help") {
|
|
6004
|
-
process.stderr.write(`/provider [id]
|
|
6005
|
-
` + `/diff
|
|
6006
|
-
` + `/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
|
|
6007
8077
|
`);
|
|
6008
8078
|
continue;
|
|
6009
8079
|
}
|
|
@@ -6282,7 +8352,7 @@ var parseNumericVersion = (version) => {
|
|
|
6282
8352
|
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
|
|
6283
8353
|
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;
|
|
6284
8354
|
};
|
|
6285
|
-
var
|
|
8355
|
+
var satisfiesEngine = (version, range) => {
|
|
6286
8356
|
const minimumMatch = /^>=(\d+\.\d+\.\d+)$/.exec(range.trim());
|
|
6287
8357
|
const actual = parseNumericVersion(version);
|
|
6288
8358
|
const minimum = minimumMatch ? parseNumericVersion(minimumMatch[1] ?? "") : undefined;
|
|
@@ -6305,7 +8375,7 @@ var inspectWorkspace = async (workspace) => {
|
|
|
6305
8375
|
if (!workspaceStat.isDirectory()) {
|
|
6306
8376
|
return diagnostic("workspace", "fail", "Workspace is not a directory.", { path: workspace });
|
|
6307
8377
|
}
|
|
6308
|
-
await access(workspace,
|
|
8378
|
+
await access(workspace, fsConstants2.R_OK);
|
|
6309
8379
|
return diagnostic("workspace", "pass", "Workspace exists and is readable.", { path: workspace });
|
|
6310
8380
|
} catch (error) {
|
|
6311
8381
|
const code = error.code ?? "UNKNOWN";
|
|
@@ -6314,36 +8384,28 @@ var inspectWorkspace = async (workspace) => {
|
|
|
6314
8384
|
};
|
|
6315
8385
|
var inspectGit = async (workspace) => {
|
|
6316
8386
|
try {
|
|
6317
|
-
const versionProcess =
|
|
6318
|
-
|
|
6319
|
-
|
|
8387
|
+
const versionProcess = await runPortableProcess(["git", "--version"], {
|
|
8388
|
+
timeoutMs: 15000,
|
|
8389
|
+
maxOutputCharacters: 4096,
|
|
6320
8390
|
stderr: "ignore"
|
|
6321
8391
|
});
|
|
6322
|
-
|
|
6323
|
-
new Response(versionProcess.stdout).text(),
|
|
6324
|
-
versionProcess.exited
|
|
6325
|
-
]);
|
|
6326
|
-
if (versionExitCode !== 0) {
|
|
8392
|
+
if (versionProcess.exitCode !== 0) {
|
|
6327
8393
|
return diagnostic("git", "warn", "Git is unavailable.", { installed: false });
|
|
6328
8394
|
}
|
|
6329
|
-
const repositoryProcess =
|
|
6330
|
-
env: {
|
|
6331
|
-
|
|
6332
|
-
|
|
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,
|
|
6333
8399
|
stderr: "ignore"
|
|
6334
8400
|
});
|
|
6335
|
-
const
|
|
6336
|
-
|
|
6337
|
-
repositoryProcess.exited
|
|
6338
|
-
]);
|
|
6339
|
-
const repository = repositoryExitCode === 0 && repositoryOutput.trim() === "true";
|
|
6340
|
-
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() });
|
|
6341
8403
|
} catch {
|
|
6342
8404
|
return diagnostic("git", "warn", "Git is unavailable.", { installed: false });
|
|
6343
8405
|
}
|
|
6344
8406
|
};
|
|
6345
8407
|
var inspectScripts = async (workspace, allowedChecks) => {
|
|
6346
|
-
const packagePath =
|
|
8408
|
+
const packagePath = path10.join(workspace, "package.json");
|
|
6347
8409
|
try {
|
|
6348
8410
|
const packageStat = await stat4(packagePath);
|
|
6349
8411
|
if (!packageStat.isFile() || packageStat.size > 1024 * 1024) {
|
|
@@ -6356,7 +8418,15 @@ var inspectScripts = async (workspace, allowedChecks) => {
|
|
|
6356
8418
|
const scripts = packageJson.scripts && typeof packageJson.scripts === "object" ? packageJson.scripts : {};
|
|
6357
8419
|
const available = allowedChecks.filter((name) => typeof scripts[name] === "string");
|
|
6358
8420
|
const missing = allowedChecks.filter((name) => typeof scripts[name] !== "string");
|
|
6359
|
-
|
|
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
|
+
});
|
|
6360
8430
|
} catch (error) {
|
|
6361
8431
|
const code = error instanceof SyntaxError ? "INVALID_JSON" : error.code ?? "UNKNOWN";
|
|
6362
8432
|
return diagnostic("scripts", "warn", "package.json could not be inspected.", {
|
|
@@ -6367,17 +8437,17 @@ var inspectScripts = async (workspace, allowedChecks) => {
|
|
|
6367
8437
|
}
|
|
6368
8438
|
};
|
|
6369
8439
|
var isInsidePath2 = (root, candidate) => {
|
|
6370
|
-
const relative =
|
|
6371
|
-
return relative === "" || !relative.startsWith(`..${
|
|
8440
|
+
const relative = path10.relative(root, candidate);
|
|
8441
|
+
return relative === "" || !relative.startsWith(`..${path10.sep}`) && relative !== ".." && !path10.isAbsolute(relative);
|
|
6372
8442
|
};
|
|
6373
8443
|
var containsSymlink = async (candidate, trustedPrefix) => {
|
|
6374
|
-
const parsed =
|
|
6375
|
-
const parts = trustedPrefix ?
|
|
6376
|
-
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] ?? "");
|
|
6377
8447
|
for (const part of parts) {
|
|
6378
|
-
current =
|
|
8448
|
+
current = path10.join(current, part);
|
|
6379
8449
|
try {
|
|
6380
|
-
if ((await
|
|
8450
|
+
if ((await lstat8(current)).isSymbolicLink()) {
|
|
6381
8451
|
return true;
|
|
6382
8452
|
}
|
|
6383
8453
|
} catch (error) {
|
|
@@ -6398,7 +8468,7 @@ var nearestExistingPath = async (candidate) => {
|
|
|
6398
8468
|
if (error.code !== "ENOENT") {
|
|
6399
8469
|
throw error;
|
|
6400
8470
|
}
|
|
6401
|
-
const parent =
|
|
8471
|
+
const parent = path10.dirname(current);
|
|
6402
8472
|
if (parent === current) {
|
|
6403
8473
|
throw error;
|
|
6404
8474
|
}
|
|
@@ -6414,7 +8484,7 @@ var inspectStateDirectory = async (workspace, stateDirectory) => {
|
|
|
6414
8484
|
});
|
|
6415
8485
|
}
|
|
6416
8486
|
if (isInsidePath2(workspace, stateDirectory)) {
|
|
6417
|
-
const segments =
|
|
8487
|
+
const segments = path10.relative(workspace, stateDirectory).split(path10.sep).filter(Boolean);
|
|
6418
8488
|
const sensitiveSegment = segments.find(isSensitiveStateSegment);
|
|
6419
8489
|
if (sensitiveSegment) {
|
|
6420
8490
|
return diagnostic("state-directory", "fail", "State directory is inside a sensitive workspace path.", {
|
|
@@ -6439,7 +8509,7 @@ var inspectStateDirectory = async (workspace, stateDirectory) => {
|
|
|
6439
8509
|
writable: false
|
|
6440
8510
|
});
|
|
6441
8511
|
}
|
|
6442
|
-
await access(existing.path,
|
|
8512
|
+
await access(existing.path, fsConstants2.R_OK | fsConstants2.W_OK);
|
|
6443
8513
|
return diagnostic("state-directory", "pass", "State directory is safe and writable.", {
|
|
6444
8514
|
path: stateDirectory,
|
|
6445
8515
|
exists: existing.path === stateDirectory,
|
|
@@ -6461,9 +8531,9 @@ var inspectOperationsStore = async (stateDirectory, storeBackend) => {
|
|
|
6461
8531
|
migrationAvailable: true
|
|
6462
8532
|
});
|
|
6463
8533
|
}
|
|
6464
|
-
const databasePath =
|
|
8534
|
+
const databasePath = path10.join(stateDirectory, "operations.sqlite");
|
|
6465
8535
|
try {
|
|
6466
|
-
const entry = await
|
|
8536
|
+
const entry = await lstat8(databasePath);
|
|
6467
8537
|
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
6468
8538
|
return diagnostic("operations-store", "fail", "SQLite state path is not a regular file.", {
|
|
6469
8539
|
backend: storeBackend,
|
|
@@ -6591,11 +8661,11 @@ var createDoctorReport = async (options = {}, context = {}) => {
|
|
|
6591
8661
|
...env.ZHIVEX_HARNESS_MCP_CONFIG ? { mcpConfigPath: env.ZHIVEX_HARNESS_MCP_CONFIG } : {},
|
|
6592
8662
|
...options
|
|
6593
8663
|
});
|
|
6594
|
-
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 };
|
|
6595
8665
|
const providers = providerAvailability(env);
|
|
6596
8666
|
const selectedProvider = providers.find((provider) => provider.id === config.provider);
|
|
6597
8667
|
const checks = [];
|
|
6598
|
-
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" }));
|
|
6599
8669
|
checks.push(await inspectWorkspace(config.workspace));
|
|
6600
8670
|
checks.push(await inspectGit(config.workspace));
|
|
6601
8671
|
checks.push(await inspectScripts(config.workspace, config.allowedChecks));
|
|
@@ -6655,7 +8725,7 @@ var createDoctorReport = async (options = {}, context = {}) => {
|
|
|
6655
8725
|
};
|
|
6656
8726
|
};
|
|
6657
8727
|
var formatDoctorReport = (report) => {
|
|
6658
|
-
const symbols = { pass: "
|
|
8728
|
+
const symbols = { pass: "✓", warn: "!", fail: "✗" };
|
|
6659
8729
|
const lines = [
|
|
6660
8730
|
`Zhivex Harness doctor v${report.harnessVersion}`,
|
|
6661
8731
|
...report.checks.map((check) => `${symbols[check.status]} ${check.id}: ${check.message}`),
|
|
@@ -6693,7 +8763,7 @@ var printRunsDocument = (document, json) => {
|
|
|
6693
8763
|
}
|
|
6694
8764
|
if (record.kind === "run-inspection") {
|
|
6695
8765
|
const run = record.run;
|
|
6696
|
-
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
|
|
6697
8767
|
`);
|
|
6698
8768
|
return;
|
|
6699
8769
|
}
|
|
@@ -6766,14 +8836,14 @@ var printSessionDocument = (document, json) => {
|
|
|
6766
8836
|
if (record.kind === "session-list") {
|
|
6767
8837
|
for (const session2 of record.sessions ?? []) {
|
|
6768
8838
|
const latest = session2.runs.at(-1);
|
|
6769
|
-
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}` : ""}
|
|
6770
8840
|
`);
|
|
6771
8841
|
}
|
|
6772
8842
|
return;
|
|
6773
8843
|
}
|
|
6774
8844
|
const session = record.session;
|
|
6775
8845
|
if (session) {
|
|
6776
|
-
process.stdout.write(`${session.sessionId}
|
|
8846
|
+
process.stdout.write(`${session.sessionId} · ${session.title ?? "(untitled)"} · ${session.runs.length} runs` + `${session.archivedAt ? " · archived" : ""}
|
|
6777
8847
|
`);
|
|
6778
8848
|
return;
|
|
6779
8849
|
}
|
|
@@ -6841,6 +8911,90 @@ var updateIndexedSessionRun = async (config, runId, status) => {
|
|
|
6841
8911
|
}
|
|
6842
8912
|
};
|
|
6843
8913
|
var cliExitCodeForError = (error) => error instanceof CliUsageError ? CLI_EXIT_CODES.usageError : CLI_EXIT_CODES.runtimeError;
|
|
8914
|
+
var MAX_CHANGE_ENVELOPE_JSON_BYTES = 4 * 1024 * 1024;
|
|
8915
|
+
var MAX_CHANGE_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
8916
|
+
var readStableCliArtifact = async (filePath, maxBytes) => {
|
|
8917
|
+
const absolute = path10.resolve(process.cwd(), filePath);
|
|
8918
|
+
try {
|
|
8919
|
+
const before = await lstat8(absolute);
|
|
8920
|
+
if (before.isSymbolicLink() || !before.isFile()) {
|
|
8921
|
+
throw new CliUsageError(`Change artifact must be a regular non-symlink file: ${filePath}`);
|
|
8922
|
+
}
|
|
8923
|
+
if (before.size > maxBytes) {
|
|
8924
|
+
throw new CliUsageError(`Change artifact exceeds the ${maxBytes}-byte limit: ${filePath}`);
|
|
8925
|
+
}
|
|
8926
|
+
const contents = await readFile4(absolute);
|
|
8927
|
+
const after = await lstat8(absolute);
|
|
8928
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
|
|
8929
|
+
throw new CliUsageError(`Change artifact changed while it was being read: ${filePath}`);
|
|
8930
|
+
}
|
|
8931
|
+
return contents;
|
|
8932
|
+
} catch (error) {
|
|
8933
|
+
if (error instanceof CliUsageError)
|
|
8934
|
+
throw error;
|
|
8935
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
8936
|
+
if (typeof code === "string") {
|
|
8937
|
+
throw new CliUsageError(`Change artifact cannot be read (${code}): ${filePath}`);
|
|
8938
|
+
}
|
|
8939
|
+
throw error;
|
|
8940
|
+
}
|
|
8941
|
+
};
|
|
8942
|
+
var readCliJsonArtifact = async (filePath) => {
|
|
8943
|
+
const contents = await readStableCliArtifact(filePath, MAX_CHANGE_ENVELOPE_JSON_BYTES);
|
|
8944
|
+
try {
|
|
8945
|
+
return JSON.parse(contents.toString("utf8"));
|
|
8946
|
+
} catch {
|
|
8947
|
+
throw new CliUsageError(`Change artifact is not valid JSON: ${filePath}`);
|
|
8948
|
+
}
|
|
8949
|
+
};
|
|
8950
|
+
var objectValue = (value, label) => {
|
|
8951
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
8952
|
+
throw new CliUsageError(`${label} must be a JSON object.`);
|
|
8953
|
+
}
|
|
8954
|
+
return value;
|
|
8955
|
+
};
|
|
8956
|
+
var manageChanges = async (options) => {
|
|
8957
|
+
if (!options.changesCommand || !options.artifactPath || !options.patchPath) {
|
|
8958
|
+
throw new CliUsageError("Incomplete changes command.");
|
|
8959
|
+
}
|
|
8960
|
+
const patchBytes = await readStableCliArtifact(options.patchPath, MAX_CHANGE_ARTIFACT_BYTES);
|
|
8961
|
+
const patchDigest = digestChangeEnvelopeArtifact(patchBytes);
|
|
8962
|
+
if (options.changesCommand === "create") {
|
|
8963
|
+
const input = objectValue(await readCliJsonArtifact(options.artifactPath), "Change envelope input");
|
|
8964
|
+
const patchInput = objectValue(input.patch, "Change envelope patch");
|
|
8965
|
+
if (patchInput.patchDigest !== undefined && patchInput.patchDigest !== patchDigest) {
|
|
8966
|
+
throw new CliUsageError("The declared patch.patchDigest does not match the exact --patch bytes.");
|
|
8967
|
+
}
|
|
8968
|
+
const envelope2 = createChangeEnvelope({
|
|
8969
|
+
...input,
|
|
8970
|
+
patch: { ...patchInput, patchDigest }
|
|
8971
|
+
});
|
|
8972
|
+
process.stdout.write(`${canonicalizeChangeEnvelope(envelope2)}
|
|
8973
|
+
`);
|
|
8974
|
+
return;
|
|
8975
|
+
}
|
|
8976
|
+
const envelope = await readCliJsonArtifact(options.artifactPath);
|
|
8977
|
+
const suppliedPreconditions = options.preconditionsPath ? objectValue(await readCliJsonArtifact(options.preconditionsPath), "Change envelope preconditions") : {};
|
|
8978
|
+
if (suppliedPreconditions.patchDigest !== undefined && suppliedPreconditions.patchDigest !== patchDigest) {
|
|
8979
|
+
throw new CliUsageError("The precondition patchDigest does not match the exact --patch bytes.");
|
|
8980
|
+
}
|
|
8981
|
+
const preconditions = changeEnvelopePreconditionsSchema.parse({
|
|
8982
|
+
...suppliedPreconditions,
|
|
8983
|
+
patchDigest
|
|
8984
|
+
});
|
|
8985
|
+
const result = verifyChangeEnvelope(envelope, {
|
|
8986
|
+
...options.verificationTime ? { now: options.verificationTime } : {},
|
|
8987
|
+
preconditions
|
|
8988
|
+
});
|
|
8989
|
+
process.stdout.write(`${JSON.stringify({
|
|
8990
|
+
schemaVersion: 1,
|
|
8991
|
+
kind: "change-envelope-verification",
|
|
8992
|
+
...result
|
|
8993
|
+
})}
|
|
8994
|
+
`);
|
|
8995
|
+
if (!result.valid)
|
|
8996
|
+
process.exitCode = CLI_EXIT_CODES.runtimeError;
|
|
8997
|
+
};
|
|
6844
8998
|
var main = async (argv = process.argv.slice(2)) => {
|
|
6845
8999
|
const options = parseCliArgs(argv);
|
|
6846
9000
|
switch (options.command) {
|
|
@@ -6870,6 +9024,9 @@ var main = async (argv = process.argv.slice(2)) => {
|
|
|
6870
9024
|
case "sessions":
|
|
6871
9025
|
await manageSessions(options);
|
|
6872
9026
|
return;
|
|
9027
|
+
case "changes":
|
|
9028
|
+
await manageChanges(options);
|
|
9029
|
+
return;
|
|
6873
9030
|
case "run":
|
|
6874
9031
|
if (options.implicitCommand && !options.prompt && process.stdin.isTTY && process.stdout.isTTY) {
|
|
6875
9032
|
await chat(options);
|
|
@@ -6881,7 +9038,16 @@ var main = async (argv = process.argv.slice(2)) => {
|
|
|
6881
9038
|
await reviewOnce(options);
|
|
6882
9039
|
}
|
|
6883
9040
|
};
|
|
6884
|
-
|
|
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) {
|
|
6885
9051
|
main().catch((error) => {
|
|
6886
9052
|
const message = error instanceof Error ? error.message : String(error);
|
|
6887
9053
|
process.stderr.write(`Error: ${message}
|
|
@@ -6890,22 +9056,22 @@ if (import.meta.main) {
|
|
|
6890
9056
|
});
|
|
6891
9057
|
}
|
|
6892
9058
|
export {
|
|
6893
|
-
|
|
6894
|
-
summarizeApproval,
|
|
6895
|
-
runResultDocument,
|
|
6896
|
-
resumeCommand,
|
|
6897
|
-
readHarnessResumeRoutes,
|
|
6898
|
-
readHarnessResumeConfig,
|
|
6899
|
-
providersDocument,
|
|
6900
|
-
parseCliArgs,
|
|
6901
|
-
main,
|
|
6902
|
-
formatDoctorReport,
|
|
6903
|
-
createHarnessResumeMetadata,
|
|
6904
|
-
createDoctorReport,
|
|
6905
|
-
cliExitCodeForError,
|
|
6906
|
-
HARNESS_RESUME_METADATA_SCHEMA_VERSION,
|
|
6907
|
-
HARNESS_RESUME_METADATA_KEY,
|
|
6908
|
-
CliUsageError,
|
|
9059
|
+
CLI_EXIT_CODES,
|
|
6909
9060
|
CLI_JSON_SCHEMA_VERSION,
|
|
6910
|
-
|
|
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
|
|
6911
9077
|
};
|