@zhivex-ai/harness 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +24 -6
- package/ROADMAP.md +34 -9
- package/SECURITY.md +7 -1
- package/SUPPORT.md +1 -1
- package/dist/change-envelope.d.ts +271 -0
- package/dist/cli-stream.d.ts +14 -14
- package/dist/cli.d.ts +7 -1
- package/dist/cli.js +1454 -162
- package/dist/config.d.ts +1 -1
- package/dist/execution-environment.d.ts +49 -3
- package/dist/harness.d.ts +16 -1
- package/dist/hostile-repository-demo.js +809 -158
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1364 -186
- package/dist/workspace.d.ts +53 -0
- package/dist/zhx.js +1454 -162
- package/docs/CHANGE_ENVELOPES.md +116 -0
- package/docs/CLI.md +14 -4
- package/docs/DURABLE_OPERATIONS.md +8 -2
- package/docs/EXECUTION_ENVIRONMENTS.md +10 -4
- package/docs/EXTENSIBILITY.md +2 -2
- package/docs/RELEASE.md +4 -4
- package/docs/REPOSITORY_EDITING.md +4 -0
- package/examples/change-envelope-input.json +26 -0
- package/examples/change.patch +6 -0
- package/package.json +8 -2
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
import { createInterface } from "readline/promises";
|
|
6
6
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
7
|
-
import { constants as
|
|
7
|
+
import { constants as fsConstants2 } from "fs";
|
|
8
8
|
import { access, lstat as lstat7, readFile as readFile4, stat as stat4 } from "fs/promises";
|
|
9
9
|
import path9 from "path";
|
|
10
|
+
import { z as z5 } from "zod";
|
|
10
11
|
import {
|
|
11
12
|
getAgentBudgetStatus as getAgentBudgetStatus3
|
|
12
13
|
} from "@zhivex-ai/agents";
|
|
@@ -371,7 +372,7 @@ 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-20-v2";
|
|
375
376
|
var DEFAULT_OCI_EXECUTION = {
|
|
376
377
|
runtime: "docker",
|
|
377
378
|
image: "oven/bun:1.3.7-slim",
|
|
@@ -869,6 +870,10 @@ var IGNORE_FILES = [".gitignore", ".zhivex-harnessignore"];
|
|
|
869
870
|
var DEFAULT_CHECKS = ["test", "typecheck", "lint", "build"];
|
|
870
871
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
871
872
|
var MAX_TOOL_OUTPUT = 20000;
|
|
873
|
+
var MAX_READ_BATCH_FILES = 20;
|
|
874
|
+
var MAX_READ_BATCH_BYTES = 2 * MAX_FILE_BYTES;
|
|
875
|
+
var MAX_SEARCH_MANY_QUERIES = 10;
|
|
876
|
+
var MAX_SEARCH_MANY_MATCHES = 500;
|
|
872
877
|
var isSensitiveName = (name) => {
|
|
873
878
|
const normalized = name.toLocaleLowerCase();
|
|
874
879
|
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");
|
|
@@ -890,6 +895,30 @@ var cursorDecode = (value) => {
|
|
|
890
895
|
throw new Error("The pagination cursor is invalid.");
|
|
891
896
|
}
|
|
892
897
|
};
|
|
898
|
+
var firstPathAtLeast = (files, target) => {
|
|
899
|
+
let low = 0;
|
|
900
|
+
let high = files.length;
|
|
901
|
+
while (low < high) {
|
|
902
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
903
|
+
if ((files[middle]?.path ?? "") < target)
|
|
904
|
+
low = middle + 1;
|
|
905
|
+
else
|
|
906
|
+
high = middle;
|
|
907
|
+
}
|
|
908
|
+
return low;
|
|
909
|
+
};
|
|
910
|
+
var firstPathAfter = (files, target) => {
|
|
911
|
+
let low = 0;
|
|
912
|
+
let high = files.length;
|
|
913
|
+
while (low < high) {
|
|
914
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
915
|
+
if ((files[middle]?.path ?? "") <= target)
|
|
916
|
+
low = middle + 1;
|
|
917
|
+
else
|
|
918
|
+
high = middle;
|
|
919
|
+
}
|
|
920
|
+
return low;
|
|
921
|
+
};
|
|
893
922
|
var escapeRegex = (value) => value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
894
923
|
var globRegex = (pattern) => {
|
|
895
924
|
let source = "";
|
|
@@ -981,6 +1010,11 @@ var spawnBounded = async (command, cwd, timeoutMs = 120000) => {
|
|
|
981
1010
|
class Workspace {
|
|
982
1011
|
root;
|
|
983
1012
|
auditEntries = [];
|
|
1013
|
+
indexIdentity = randomUUID();
|
|
1014
|
+
indexRevision = 0;
|
|
1015
|
+
workspaceIndex;
|
|
1016
|
+
indexBuild;
|
|
1017
|
+
indexMetrics = { builds: 0, reuses: 0, stableFileReads: 0 };
|
|
984
1018
|
constructor(root) {
|
|
985
1019
|
this.root = root;
|
|
986
1020
|
}
|
|
@@ -1032,6 +1066,7 @@ class Workspace {
|
|
|
1032
1066
|
return { path: candidate, exists: true };
|
|
1033
1067
|
}
|
|
1034
1068
|
async readStableFile(relativePath, allowBinary = true) {
|
|
1069
|
+
this.indexMetrics.stableFileReads += 1;
|
|
1035
1070
|
const safe = await this.safePath(relativePath, { requireFile: true });
|
|
1036
1071
|
const before = await lstat(safe.path);
|
|
1037
1072
|
if (before.size > MAX_FILE_BYTES)
|
|
@@ -1051,14 +1086,40 @@ class Workspace {
|
|
|
1051
1086
|
mode: before.mode & 511
|
|
1052
1087
|
};
|
|
1053
1088
|
}
|
|
1054
|
-
|
|
1089
|
+
fingerprint(relativePath, kind, entry, digest2) {
|
|
1090
|
+
return {
|
|
1091
|
+
path: relativePath || ".",
|
|
1092
|
+
kind,
|
|
1093
|
+
dev: entry.dev,
|
|
1094
|
+
ino: entry.ino,
|
|
1095
|
+
size: entry.size,
|
|
1096
|
+
mtimeMs: entry.mtimeMs,
|
|
1097
|
+
ctimeMs: entry.ctimeMs,
|
|
1098
|
+
...digest2 ? { digest: digest2 } : {}
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
sameFingerprint(entry, fingerprint) {
|
|
1102
|
+
return entry.dev === fingerprint.dev && entry.ino === fingerprint.ino && entry.size === fingerprint.size && entry.mtimeMs === fingerprint.mtimeMs && entry.ctimeMs === fingerprint.ctimeMs;
|
|
1103
|
+
}
|
|
1104
|
+
async rulesForDirectory(directory, base, inherited, ignoreFiles) {
|
|
1055
1105
|
const rules = [...inherited];
|
|
1056
1106
|
for (const filename of IGNORE_FILES) {
|
|
1057
1107
|
try {
|
|
1058
1108
|
const ignorePath = path3.join(directory, filename);
|
|
1059
|
-
const
|
|
1060
|
-
if (
|
|
1061
|
-
|
|
1109
|
+
const before = await lstat(ignorePath);
|
|
1110
|
+
if (before.isSymbolicLink() || !before.isFile())
|
|
1111
|
+
continue;
|
|
1112
|
+
if (before.size <= MAX_FILE_BYTES) {
|
|
1113
|
+
const contents = await readFile(ignorePath);
|
|
1114
|
+
const after = await lstat(ignorePath);
|
|
1115
|
+
if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, this.fingerprint("", "ignore-file", before))) {
|
|
1116
|
+
throw new Error(`The ignore file changed while the workspace was being indexed: ${wirePath(path3.relative(this.root, ignorePath))}`);
|
|
1117
|
+
}
|
|
1118
|
+
const digest2 = digestBytes(contents);
|
|
1119
|
+
ignoreFiles.push(this.fingerprint(wirePath(path3.relative(this.root, ignorePath)), "ignore-file", after, digest2));
|
|
1120
|
+
rules.push(...parseIgnoreRules(contents.toString("utf8"), base));
|
|
1121
|
+
} else {
|
|
1122
|
+
ignoreFiles.push(this.fingerprint(wirePath(path3.relative(this.root, ignorePath)), "ignore-file", before));
|
|
1062
1123
|
}
|
|
1063
1124
|
} catch (error) {
|
|
1064
1125
|
if (error.code !== "ENOENT")
|
|
@@ -1067,10 +1128,17 @@ class Workspace {
|
|
|
1067
1128
|
}
|
|
1068
1129
|
return rules;
|
|
1069
1130
|
}
|
|
1070
|
-
async collectFiles() {
|
|
1131
|
+
async collectFiles(version) {
|
|
1071
1132
|
const files = [];
|
|
1133
|
+
const directories = [];
|
|
1134
|
+
const ignoreFiles = [];
|
|
1072
1135
|
const walk = async (directory, base, inherited) => {
|
|
1073
|
-
const
|
|
1136
|
+
const directoryEntry = await lstat(directory);
|
|
1137
|
+
if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
|
|
1138
|
+
throw new Error(`A workspace directory changed while it was being indexed: ${base || "."}`);
|
|
1139
|
+
}
|
|
1140
|
+
directories.push(this.fingerprint(base, "directory", directoryEntry));
|
|
1141
|
+
const rules = await this.rulesForDirectory(directory, base, inherited, ignoreFiles);
|
|
1074
1142
|
const entries = (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
1075
1143
|
for (const entry of entries) {
|
|
1076
1144
|
const relative = base ? `${base}/${entry.name}` : entry.name;
|
|
@@ -1080,11 +1148,99 @@ class Workspace {
|
|
|
1080
1148
|
if (entry.isDirectory())
|
|
1081
1149
|
await walk(absolute, relative, rules);
|
|
1082
1150
|
else if (entry.isFile())
|
|
1083
|
-
files.push({ path: relative
|
|
1151
|
+
files.push({ path: relative });
|
|
1084
1152
|
}
|
|
1085
1153
|
};
|
|
1086
1154
|
await walk(this.root, "", []);
|
|
1087
|
-
|
|
1155
|
+
files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
1156
|
+
return { version, files, directories, ignoreFiles };
|
|
1157
|
+
}
|
|
1158
|
+
async isWorkspaceIndexFresh(index) {
|
|
1159
|
+
try {
|
|
1160
|
+
for (const fingerprint of index.directories) {
|
|
1161
|
+
const absolute = fingerprint.path === "." ? this.root : path3.join(this.root, fingerprint.path);
|
|
1162
|
+
const entry = await lstat(absolute);
|
|
1163
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() || !this.sameFingerprint(entry, fingerprint))
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
for (const fingerprint of index.ignoreFiles) {
|
|
1167
|
+
const absolute = path3.join(this.root, fingerprint.path);
|
|
1168
|
+
const before = await lstat(absolute);
|
|
1169
|
+
if (before.isSymbolicLink() || !before.isFile() || !this.sameFingerprint(before, fingerprint))
|
|
1170
|
+
return false;
|
|
1171
|
+
if (fingerprint.digest) {
|
|
1172
|
+
const contents = await readFile(absolute);
|
|
1173
|
+
const after = await lstat(absolute);
|
|
1174
|
+
if (after.isSymbolicLink() || !after.isFile() || !this.sameFingerprint(after, fingerprint) || digestBytes(contents) !== fingerprint.digest)
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return true;
|
|
1179
|
+
} catch {
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
invalidateWorkspaceIndex() {
|
|
1184
|
+
this.indexRevision += 1;
|
|
1185
|
+
this.workspaceIndex = undefined;
|
|
1186
|
+
}
|
|
1187
|
+
async buildWorkspaceIndex(revision) {
|
|
1188
|
+
const version = `${this.indexIdentity}:${revision}`;
|
|
1189
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
1190
|
+
try {
|
|
1191
|
+
const index = await this.collectFiles(version);
|
|
1192
|
+
if (await this.isWorkspaceIndexFresh(index))
|
|
1193
|
+
return index;
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
const code = error.code;
|
|
1196
|
+
const changedDuringIndexing = error instanceof Error && error.message.includes("while the workspace was being indexed");
|
|
1197
|
+
if (attempt === 2 || code !== "ENOENT" && !changedDuringIndexing)
|
|
1198
|
+
throw error;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
throw new Error("The workspace kept changing while its file index was being built.");
|
|
1202
|
+
}
|
|
1203
|
+
async getWorkspaceIndex() {
|
|
1204
|
+
const cached = this.workspaceIndex;
|
|
1205
|
+
if (cached) {
|
|
1206
|
+
if (await this.isWorkspaceIndexFresh(cached)) {
|
|
1207
|
+
this.indexMetrics.reuses += 1;
|
|
1208
|
+
return cached;
|
|
1209
|
+
}
|
|
1210
|
+
this.invalidateWorkspaceIndex();
|
|
1211
|
+
}
|
|
1212
|
+
const revision = this.indexRevision;
|
|
1213
|
+
if (!this.indexBuild || this.indexBuild.revision !== revision) {
|
|
1214
|
+
const promise = this.buildWorkspaceIndex(revision);
|
|
1215
|
+
this.indexBuild = { revision, promise };
|
|
1216
|
+
}
|
|
1217
|
+
const build = this.indexBuild;
|
|
1218
|
+
let index;
|
|
1219
|
+
try {
|
|
1220
|
+
index = await build.promise;
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
if (this.indexBuild === build)
|
|
1223
|
+
this.indexBuild = undefined;
|
|
1224
|
+
throw error;
|
|
1225
|
+
}
|
|
1226
|
+
if (revision !== this.indexRevision)
|
|
1227
|
+
return this.getWorkspaceIndex();
|
|
1228
|
+
if (this.indexBuild === build)
|
|
1229
|
+
this.indexBuild = undefined;
|
|
1230
|
+
if (this.workspaceIndex?.version !== index.version) {
|
|
1231
|
+
this.workspaceIndex = index;
|
|
1232
|
+
this.indexMetrics.builds += 1;
|
|
1233
|
+
}
|
|
1234
|
+
return this.workspaceIndex;
|
|
1235
|
+
}
|
|
1236
|
+
workspaceIndexDiagnostics() {
|
|
1237
|
+
const index = this.workspaceIndex;
|
|
1238
|
+
return {
|
|
1239
|
+
...this.indexMetrics,
|
|
1240
|
+
...index ? { version: index.version } : {},
|
|
1241
|
+
files: index?.files.length ?? 0,
|
|
1242
|
+
directories: index?.directories.length ?? 0
|
|
1243
|
+
};
|
|
1088
1244
|
}
|
|
1089
1245
|
async listFiles(relativePath = ".", options = {}) {
|
|
1090
1246
|
const start = await this.safePath(relativePath);
|
|
@@ -1096,36 +1252,52 @@ class Workspace {
|
|
|
1096
1252
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 5000)
|
|
1097
1253
|
throw new Error("limit must be between 1 and 5000.");
|
|
1098
1254
|
let after = "";
|
|
1255
|
+
let cursorIndexVersion;
|
|
1099
1256
|
if (input.cursor) {
|
|
1100
1257
|
const parsed = cursorDecode(input.cursor);
|
|
1101
|
-
if (parsed.v !==
|
|
1258
|
+
if (parsed.v !== 2 || parsed.kind !== "list" || parsed.path !== requestPath || parsed.limit !== limit || typeof parsed.indexVersion !== "string" || typeof parsed.after !== "string") {
|
|
1102
1259
|
throw new Error("The pagination cursor does not match this list request.");
|
|
1103
1260
|
}
|
|
1104
1261
|
after = parsed.after;
|
|
1262
|
+
cursorIndexVersion = parsed.indexVersion;
|
|
1263
|
+
}
|
|
1264
|
+
const index = await this.getWorkspaceIndex();
|
|
1265
|
+
if (cursorIndexVersion && cursorIndexVersion !== index.version) {
|
|
1266
|
+
throw new Error("The pagination cursor is stale because the workspace changed.");
|
|
1105
1267
|
}
|
|
1106
1268
|
const prefix = requestPath === "." ? "" : `${requestPath}/`;
|
|
1107
|
-
|
|
1108
|
-
const selected =
|
|
1269
|
+
let candidateIndex = after ? firstPathAfter(index.files, after) : requestPath === "." ? 0 : firstPathAtLeast(index.files, prefix);
|
|
1270
|
+
const selected = [];
|
|
1271
|
+
while (candidateIndex < index.files.length && selected.length <= limit) {
|
|
1272
|
+
const candidate = index.files[candidateIndex];
|
|
1273
|
+
if (!candidate || requestPath !== "." && !candidate.path.startsWith(prefix))
|
|
1274
|
+
break;
|
|
1275
|
+
selected.push(candidate);
|
|
1276
|
+
candidateIndex += 1;
|
|
1277
|
+
}
|
|
1278
|
+
const truncated = selected.length > limit;
|
|
1279
|
+
if (truncated)
|
|
1280
|
+
selected.pop();
|
|
1109
1281
|
const files = [];
|
|
1110
1282
|
for (const candidate of selected) {
|
|
1111
1283
|
const file = await this.readStableFile(candidate.path);
|
|
1112
1284
|
files.push({ path: file.path, size: file.contents.byteLength, digest: file.digest });
|
|
1113
1285
|
}
|
|
1114
|
-
const truncated = candidates.length > selected.length;
|
|
1115
1286
|
const last = selected.at(-1);
|
|
1116
1287
|
return {
|
|
1117
1288
|
files,
|
|
1118
1289
|
truncated,
|
|
1119
|
-
...truncated && last ? {
|
|
1290
|
+
...truncated && last ? {
|
|
1291
|
+
nextCursor: cursorEncode({ v: 2, kind: "list", path: requestPath, limit, after: last.path, indexVersion: index.version })
|
|
1292
|
+
} : {}
|
|
1120
1293
|
};
|
|
1121
1294
|
}
|
|
1122
|
-
|
|
1295
|
+
renderReadFile(file, startLine = 1, endLine) {
|
|
1123
1296
|
if (!Number.isSafeInteger(startLine) || startLine < 1)
|
|
1124
1297
|
throw new Error("startLine must be a positive integer.");
|
|
1125
1298
|
if (endLine !== undefined && (!Number.isSafeInteger(endLine) || endLine < startLine)) {
|
|
1126
1299
|
throw new Error("endLine must be greater than or equal to startLine.");
|
|
1127
1300
|
}
|
|
1128
|
-
const file = await this.readStableFile(relativePath, false);
|
|
1129
1301
|
const lines = file.contents.toString("utf8").split(/\r?\n/);
|
|
1130
1302
|
const boundedEnd = Math.min(endLine ?? startLine + 399, startLine + 1999, lines.length);
|
|
1131
1303
|
return {
|
|
@@ -1139,6 +1311,37 @@ class Workspace {
|
|
|
1139
1311
|
truncated: boundedEnd < lines.length
|
|
1140
1312
|
};
|
|
1141
1313
|
}
|
|
1314
|
+
async readFile(relativePath, startLine = 1, endLine) {
|
|
1315
|
+
return this.renderReadFile(await this.readStableFile(relativePath, false), startLine, endLine);
|
|
1316
|
+
}
|
|
1317
|
+
async readFiles(requests) {
|
|
1318
|
+
if (!Array.isArray(requests) || requests.length < 1 || requests.length > MAX_READ_BATCH_FILES) {
|
|
1319
|
+
throw new Error(`readFiles requires between 1 and ${MAX_READ_BATCH_FILES} file requests.`);
|
|
1320
|
+
}
|
|
1321
|
+
const ordered = requests.map((request) => ({
|
|
1322
|
+
...request,
|
|
1323
|
+
path: wirePath(path3.relative(this.root, this.lexicalPath(request.path)))
|
|
1324
|
+
})).sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0) || (a.startLine ?? 1) - (b.startLine ?? 1) || (a.endLine ?? 0) - (b.endLine ?? 0));
|
|
1325
|
+
const stableFiles = new Map;
|
|
1326
|
+
let totalBytes = 0;
|
|
1327
|
+
for (const request of ordered) {
|
|
1328
|
+
if (!Number.isSafeInteger(request.startLine ?? 1) || (request.startLine ?? 1) < 1 || request.endLine !== undefined && (!Number.isSafeInteger(request.endLine) || request.endLine < (request.startLine ?? 1))) {
|
|
1329
|
+
throw new Error(`Invalid line range for ${request.path}.`);
|
|
1330
|
+
}
|
|
1331
|
+
if (stableFiles.has(request.path))
|
|
1332
|
+
continue;
|
|
1333
|
+
const file = await this.readStableFile(request.path, false);
|
|
1334
|
+
totalBytes += file.contents.byteLength;
|
|
1335
|
+
if (totalBytes > MAX_READ_BATCH_BYTES) {
|
|
1336
|
+
throw new Error(`readFiles exceeds the aggregate ${MAX_READ_BATCH_BYTES}-byte source limit.`);
|
|
1337
|
+
}
|
|
1338
|
+
stableFiles.set(request.path, file);
|
|
1339
|
+
}
|
|
1340
|
+
return {
|
|
1341
|
+
files: ordered.map((request) => this.renderReadFile(stableFiles.get(request.path), request.startLine ?? 1, request.endLine)),
|
|
1342
|
+
sourceBytes: totalBytes
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1142
1345
|
async inspectFile(relativePath) {
|
|
1143
1346
|
const file = await this.readStableFile(relativePath);
|
|
1144
1347
|
return {
|
|
@@ -1161,21 +1364,29 @@ class Workspace {
|
|
|
1161
1364
|
const caseSensitive = options.caseSensitive ?? false;
|
|
1162
1365
|
let afterPath = "";
|
|
1163
1366
|
let afterLine = 0;
|
|
1367
|
+
let cursorIndexVersion;
|
|
1164
1368
|
if (options.cursor) {
|
|
1165
1369
|
const parsed = cursorDecode(options.cursor);
|
|
1166
|
-
if (parsed.v !==
|
|
1370
|
+
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
1371
|
throw new Error("The pagination cursor does not match this search request.");
|
|
1168
1372
|
}
|
|
1169
1373
|
afterPath = parsed.afterPath;
|
|
1170
1374
|
afterLine = parsed.afterLine;
|
|
1375
|
+
cursorIndexVersion = parsed.indexVersion;
|
|
1376
|
+
}
|
|
1377
|
+
const index = await this.getWorkspaceIndex();
|
|
1378
|
+
if (cursorIndexVersion && cursorIndexVersion !== index.version) {
|
|
1379
|
+
throw new Error("The pagination cursor is stale because the workspace changed.");
|
|
1171
1380
|
}
|
|
1172
1381
|
const prefix = requestPath === "." ? "" : `${requestPath}/`;
|
|
1173
1382
|
const needle = caseSensitive ? query : query.toLocaleLowerCase();
|
|
1174
1383
|
const matches = [];
|
|
1175
1384
|
let hasMore = false;
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1385
|
+
let candidateIndex = afterPath ? firstPathAtLeast(index.files, afterPath) : requestPath === "." ? 0 : firstPathAtLeast(index.files, prefix);
|
|
1386
|
+
for (;candidateIndex < index.files.length; candidateIndex += 1) {
|
|
1387
|
+
const candidate = index.files[candidateIndex];
|
|
1388
|
+
if (!candidate || requestPath !== "." && !candidate.path.startsWith(prefix))
|
|
1389
|
+
break;
|
|
1179
1390
|
let file;
|
|
1180
1391
|
try {
|
|
1181
1392
|
file = await this.readStableFile(candidate.path, false);
|
|
@@ -1183,11 +1394,11 @@ class Workspace {
|
|
|
1183
1394
|
continue;
|
|
1184
1395
|
}
|
|
1185
1396
|
const lines = file.contents.toString("utf8").split(/\r?\n/);
|
|
1186
|
-
for (let
|
|
1187
|
-
const lineNumber =
|
|
1397
|
+
for (let index2 = 0;index2 < lines.length; index2 += 1) {
|
|
1398
|
+
const lineNumber = index2 + 1;
|
|
1188
1399
|
if (candidate.path === afterPath && lineNumber <= afterLine)
|
|
1189
1400
|
continue;
|
|
1190
|
-
const line = lines[
|
|
1401
|
+
const line = lines[index2] ?? "";
|
|
1191
1402
|
if (!(caseSensitive ? line : line.toLocaleLowerCase()).includes(needle))
|
|
1192
1403
|
continue;
|
|
1193
1404
|
if (matches.length >= limit) {
|
|
@@ -1204,10 +1415,88 @@ class Workspace {
|
|
|
1204
1415
|
matches,
|
|
1205
1416
|
truncated: hasMore,
|
|
1206
1417
|
...hasMore && last ? {
|
|
1207
|
-
nextCursor: cursorEncode({
|
|
1418
|
+
nextCursor: cursorEncode({
|
|
1419
|
+
v: 2,
|
|
1420
|
+
kind: "search",
|
|
1421
|
+
path: requestPath,
|
|
1422
|
+
query,
|
|
1423
|
+
caseSensitive,
|
|
1424
|
+
limit,
|
|
1425
|
+
afterPath: last.path,
|
|
1426
|
+
afterLine: last.line,
|
|
1427
|
+
indexVersion: index.version
|
|
1428
|
+
})
|
|
1208
1429
|
} : {}
|
|
1209
1430
|
};
|
|
1210
1431
|
}
|
|
1432
|
+
async searchMany(queries, relativePath = ".", options = {}) {
|
|
1433
|
+
if (!Array.isArray(queries) || queries.length < 1 || queries.length > MAX_SEARCH_MANY_QUERIES) {
|
|
1434
|
+
throw new Error(`searchMany requires between 1 and ${MAX_SEARCH_MANY_QUERIES} queries.`);
|
|
1435
|
+
}
|
|
1436
|
+
const limitPerQuery = options.limitPerQuery ?? 50;
|
|
1437
|
+
if (!Number.isSafeInteger(limitPerQuery) || limitPerQuery < 1 || limitPerQuery > MAX_SEARCH_MANY_MATCHES || limitPerQuery * queries.length > MAX_SEARCH_MANY_MATCHES) {
|
|
1438
|
+
throw new Error(`searchMany allows at most ${MAX_SEARCH_MANY_MATCHES} aggregate matches.`);
|
|
1439
|
+
}
|
|
1440
|
+
const seen = new Set;
|
|
1441
|
+
const states = queries.map((input) => {
|
|
1442
|
+
if (!input.query || input.query.length > 200)
|
|
1443
|
+
throw new Error("Each search query must be between 1 and 200 characters.");
|
|
1444
|
+
const caseSensitive = input.caseSensitive ?? false;
|
|
1445
|
+
const key = `${caseSensitive ? "1" : "0"}\x00${input.query}`;
|
|
1446
|
+
if (seen.has(key))
|
|
1447
|
+
throw new Error("searchMany queries must be unique.");
|
|
1448
|
+
seen.add(key);
|
|
1449
|
+
return {
|
|
1450
|
+
query: input.query,
|
|
1451
|
+
caseSensitive,
|
|
1452
|
+
needle: caseSensitive ? input.query : input.query.toLocaleLowerCase(),
|
|
1453
|
+
matches: [],
|
|
1454
|
+
truncated: false
|
|
1455
|
+
};
|
|
1456
|
+
});
|
|
1457
|
+
const start = await this.safePath(relativePath);
|
|
1458
|
+
if (!(await lstat(start.path)).isDirectory())
|
|
1459
|
+
throw new Error("searchMany requires a directory.");
|
|
1460
|
+
const requestPath = wirePath(path3.relative(this.root, start.path)) || ".";
|
|
1461
|
+
const prefix = requestPath === "." ? "" : `${requestPath}/`;
|
|
1462
|
+
const index = await this.getWorkspaceIndex();
|
|
1463
|
+
let candidateIndex = requestPath === "." ? 0 : firstPathAtLeast(index.files, prefix);
|
|
1464
|
+
for (;candidateIndex < index.files.length; candidateIndex += 1) {
|
|
1465
|
+
const candidate = index.files[candidateIndex];
|
|
1466
|
+
if (!candidate || requestPath !== "." && !candidate.path.startsWith(prefix))
|
|
1467
|
+
break;
|
|
1468
|
+
let file;
|
|
1469
|
+
try {
|
|
1470
|
+
file = await this.readStableFile(candidate.path, false);
|
|
1471
|
+
} catch {
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
const lines = file.contents.toString("utf8").split(/\r?\n/);
|
|
1475
|
+
for (let lineIndex = 0;lineIndex < lines.length; lineIndex += 1) {
|
|
1476
|
+
const line = lines[lineIndex] ?? "";
|
|
1477
|
+
const foldedLine = line.toLocaleLowerCase();
|
|
1478
|
+
for (const state of states) {
|
|
1479
|
+
if (state.truncated || !(state.caseSensitive ? line : foldedLine).includes(state.needle))
|
|
1480
|
+
continue;
|
|
1481
|
+
if (state.matches.length >= limitPerQuery) {
|
|
1482
|
+
state.truncated = true;
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
state.matches.push({
|
|
1486
|
+
path: candidate.path,
|
|
1487
|
+
line: lineIndex + 1,
|
|
1488
|
+
text: truncate(line, 500),
|
|
1489
|
+
digest: file.digest
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (states.every((state) => state.truncated))
|
|
1494
|
+
break;
|
|
1495
|
+
}
|
|
1496
|
+
return {
|
|
1497
|
+
results: states.map(({ needle: _needle, ...state }) => state)
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1211
1500
|
audit(entry) {
|
|
1212
1501
|
const record = { id: randomUUID(), timestamp: new Date().toISOString(), ...entry };
|
|
1213
1502
|
this.auditEntries.push(record);
|
|
@@ -1371,6 +1660,7 @@ class Workspace {
|
|
|
1371
1660
|
throw new AggregateError([error, ...rollbackErrors], "Patch failed and rollback was incomplete.");
|
|
1372
1661
|
throw error;
|
|
1373
1662
|
}
|
|
1663
|
+
this.invalidateWorkspaceIndex();
|
|
1374
1664
|
const changes = prepared.map((item) => this.audit({
|
|
1375
1665
|
operation: item.before ? "update" : "create",
|
|
1376
1666
|
path: item.change.path,
|
|
@@ -1431,6 +1721,7 @@ class Workspace {
|
|
|
1431
1721
|
await unlink(destination.path).catch(() => {});
|
|
1432
1722
|
throw error;
|
|
1433
1723
|
}
|
|
1724
|
+
this.invalidateWorkspaceIndex();
|
|
1434
1725
|
const audit = this.audit({ operation: "move", path: parsed.source, destination: parsed.destination, beforeDigest: source.digest, afterDigest: source.digest });
|
|
1435
1726
|
return { source: parsed.source, destination: parsed.destination, digest: source.digest, audit };
|
|
1436
1727
|
}
|
|
@@ -1519,6 +1810,7 @@ class Workspace {
|
|
|
1519
1810
|
await unlink(path3.join(directory, `${quarantineId}.json`)).catch(() => {});
|
|
1520
1811
|
throw error;
|
|
1521
1812
|
}
|
|
1813
|
+
this.invalidateWorkspaceIndex();
|
|
1522
1814
|
const audit = this.audit({ operation: "quarantine", path: parsed.path, beforeDigest: source.digest, quarantineId });
|
|
1523
1815
|
return { quarantineId, path: parsed.path, digest: source.digest, audit };
|
|
1524
1816
|
}
|
|
@@ -1587,6 +1879,7 @@ class Workspace {
|
|
|
1587
1879
|
}
|
|
1588
1880
|
throw rollbackErrors.length ? new AggregateError([error, ...rollbackErrors], "Restore failed and rollback was incomplete.") : error;
|
|
1589
1881
|
}
|
|
1882
|
+
this.invalidateWorkspaceIndex();
|
|
1590
1883
|
const audit = this.audit({ operation: "restore", path: destinationPath, afterDigest: digest2, quarantineId: parsed.quarantineId });
|
|
1591
1884
|
return { quarantineId: parsed.quarantineId, path: destinationPath, digest: digest2, audit };
|
|
1592
1885
|
}
|
|
@@ -1607,7 +1900,11 @@ class Workspace {
|
|
|
1607
1900
|
throw new Error(`package.json does not define the "${check}" script.`);
|
|
1608
1901
|
if (scripts[check] !== expectedScript)
|
|
1609
1902
|
throw new Error(`The "${check}" script changed or does not match expectedScript.`);
|
|
1610
|
-
|
|
1903
|
+
try {
|
|
1904
|
+
return await spawnBounded(["bun", "--no-env-file", "run", check], this.root);
|
|
1905
|
+
} finally {
|
|
1906
|
+
this.invalidateWorkspaceIndex();
|
|
1907
|
+
}
|
|
1611
1908
|
}
|
|
1612
1909
|
async gitDiff() {
|
|
1613
1910
|
const [statusResult, diffResult, stagedResult] = await Promise.all([
|
|
@@ -2389,7 +2686,7 @@ var HARNESS_SUBAGENT_PROFILE_DESCRIPTORS = [
|
|
|
2389
2686
|
toolName: "delegate_explorer",
|
|
2390
2687
|
description: "Delegate bounded repository discovery to a read-only explorer.",
|
|
2391
2688
|
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"]
|
|
2689
|
+
toolNames: ["list_files", "read_file", "read_files", "search_files", "search_many", "git_diff"]
|
|
2393
2690
|
},
|
|
2394
2691
|
{
|
|
2395
2692
|
id: "implementer",
|
|
@@ -2403,14 +2700,14 @@ var HARNESS_SUBAGENT_PROFILE_DESCRIPTORS = [
|
|
|
2403
2700
|
toolName: "delegate_tester",
|
|
2404
2701
|
description: "Delegate focused verification to a tester with read-only inspection and approved Bun checks.",
|
|
2405
2702
|
instructions: "Verify the delegated behavior with repository evidence and declared Bun checks. Do not edit files. Report exact failures and limits.",
|
|
2406
|
-
toolNames: ["list_files", "read_file", "search_files", "run_check", "git_diff"]
|
|
2703
|
+
toolNames: ["list_files", "read_file", "read_files", "search_files", "search_many", "run_check", "git_diff"]
|
|
2407
2704
|
},
|
|
2408
2705
|
{
|
|
2409
2706
|
id: "reviewer",
|
|
2410
2707
|
toolName: "delegate_reviewer",
|
|
2411
2708
|
description: "Delegate an independent read-only review of correctness, safety, and regressions.",
|
|
2412
2709
|
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"]
|
|
2710
|
+
toolNames: ["list_files", "read_file", "read_files", "search_files", "search_many", "mutation_audit", "git_diff"]
|
|
2414
2711
|
}
|
|
2415
2712
|
];
|
|
2416
2713
|
var pickTools = (tools, names) => {
|
|
@@ -2548,8 +2845,10 @@ var runHarnessReviewGroup = async (runtime, input, profiles = ["explorer", "revi
|
|
|
2548
2845
|
|
|
2549
2846
|
// src/execution-environment.ts
|
|
2550
2847
|
import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
|
|
2848
|
+
import { constants as fsConstants } from "fs";
|
|
2551
2849
|
import {
|
|
2552
2850
|
chmod as chmod2,
|
|
2851
|
+
copyFile,
|
|
2553
2852
|
lstat as lstat5,
|
|
2554
2853
|
mkdir as mkdir3,
|
|
2555
2854
|
readFile as readFile3,
|
|
@@ -2573,7 +2872,9 @@ var TERMINAL_STATUSES = new Set([
|
|
|
2573
2872
|
var BUILT_IN_TOOL_NAMES = new Set([
|
|
2574
2873
|
"list_files",
|
|
2575
2874
|
"read_file",
|
|
2875
|
+
"read_files",
|
|
2576
2876
|
"search_files",
|
|
2877
|
+
"search_many",
|
|
2577
2878
|
"propose_edits",
|
|
2578
2879
|
"apply_patch",
|
|
2579
2880
|
"move_file",
|
|
@@ -2698,11 +2999,26 @@ var parseRuntimeVersion = (stdout, runtime) => {
|
|
|
2698
2999
|
var validContainerIds = (stdout) => stdout.split(/\r?\n/).map((value) => value.trim()).filter((value) => /^[a-f0-9]{12,64}$/.test(value));
|
|
2699
3000
|
var validVolumeNames = (stdout) => stdout.split(/\r?\n/).map((value) => value.trim()).filter((value) => /^zhivex-harness-[A-Za-z0-9_.-]+$/.test(value));
|
|
2700
3001
|
var MAX_SYNC_ENTRIES = 1e4;
|
|
3002
|
+
var updateSnapshotMetadata = (target, kind, relative, entry) => {
|
|
3003
|
+
target.update([
|
|
3004
|
+
kind,
|
|
3005
|
+
relative,
|
|
3006
|
+
entry.dev,
|
|
3007
|
+
entry.ino,
|
|
3008
|
+
entry.size,
|
|
3009
|
+
entry.mtimeMs,
|
|
3010
|
+
entry.ctimeMs,
|
|
3011
|
+
entry.mode & 511
|
|
3012
|
+
].join("\x00"));
|
|
3013
|
+
target.update("\x00");
|
|
3014
|
+
};
|
|
2701
3015
|
var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspaceBytes, maxFileWriteBytes) => {
|
|
2702
3016
|
let entries = 0;
|
|
2703
3017
|
let totalBytes = 0;
|
|
3018
|
+
const seal = createHash6("sha256");
|
|
3019
|
+
const metadata = createHash6("sha256");
|
|
2704
3020
|
const visit = async (directory, relativeDirectory = "") => {
|
|
2705
|
-
for (const name of await readdir2(directory)) {
|
|
3021
|
+
for (const name of (await readdir2(directory)).sort()) {
|
|
2706
3022
|
const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
|
2707
3023
|
const absolute = path7.join(directory, name);
|
|
2708
3024
|
if (!isInside3(stagedRoot, absolute) || relative.length > 1024) {
|
|
@@ -2721,22 +3037,32 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
2721
3037
|
throw new Error(`OCI workspace export contains a symbolic link: ${relative}.`);
|
|
2722
3038
|
}
|
|
2723
3039
|
if (entry.isDirectory()) {
|
|
3040
|
+
updateSnapshotMetadata(metadata, "directory", relative, entry);
|
|
3041
|
+
seal.update(`directory\x00${relative}\x00${entry.mode & 511}\x00`);
|
|
2724
3042
|
await visit(absolute, relative);
|
|
2725
3043
|
continue;
|
|
2726
3044
|
}
|
|
2727
3045
|
if (!entry.isFile() || entry.nlink !== 1) {
|
|
2728
3046
|
throw new Error(`OCI workspace export contains a special or linked file: ${relative}.`);
|
|
2729
3047
|
}
|
|
3048
|
+
const contents = await readFile3(absolute);
|
|
3049
|
+
const after = await lstat5(absolute);
|
|
3050
|
+
if (after.isSymbolicLink() || !after.isFile() || after.nlink !== 1 || entry.dev !== after.dev || entry.ino !== after.ino || entry.size !== after.size || entry.mtimeMs !== after.mtimeMs || entry.ctimeMs !== after.ctimeMs || (entry.mode & 511) !== (after.mode & 511)) {
|
|
3051
|
+
throw new Error(`OCI workspace export changed while it was being validated: ${relative}.`);
|
|
3052
|
+
}
|
|
2730
3053
|
totalBytes += entry.size;
|
|
2731
3054
|
if (totalBytes > maxWorkspaceBytes) {
|
|
2732
3055
|
throw new Error(`OCI workspace export exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
|
|
2733
3056
|
}
|
|
3057
|
+
const contentDigest = digest2(contents);
|
|
3058
|
+
updateSnapshotMetadata(metadata, "file", relative, after);
|
|
3059
|
+
seal.update(`file\x00${relative}\x00${entry.mode & 511}\x00${entry.size}\x00${contentDigest}\x00`);
|
|
2734
3060
|
if (entry.size > maxFileWriteBytes) {
|
|
2735
3061
|
const previous = path7.join(previousRoot, ...relative.split("/"));
|
|
2736
3062
|
let unchanged = false;
|
|
2737
3063
|
try {
|
|
2738
3064
|
const previousEntry = await lstat5(previous);
|
|
2739
|
-
unchanged = previousEntry.isFile() && !previousEntry.isSymbolicLink() && previousEntry.size === entry.size && digest2(await readFile3(previous)) ===
|
|
3065
|
+
unchanged = previousEntry.isFile() && !previousEntry.isSymbolicLink() && previousEntry.size === entry.size && digest2(await readFile3(previous)) === contentDigest;
|
|
2740
3066
|
} catch (error) {
|
|
2741
3067
|
if (error.code !== "ENOENT")
|
|
2742
3068
|
throw error;
|
|
@@ -2748,6 +3074,51 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
2748
3074
|
}
|
|
2749
3075
|
};
|
|
2750
3076
|
await visit(stagedRoot);
|
|
3077
|
+
return {
|
|
3078
|
+
entries,
|
|
3079
|
+
totalBytes,
|
|
3080
|
+
seal: `sha256:${seal.digest("hex")}`,
|
|
3081
|
+
metadataFingerprint: `sha256:${metadata.digest("hex")}`
|
|
3082
|
+
};
|
|
3083
|
+
};
|
|
3084
|
+
var snapshotMetadataFingerprint = async (root, maxWorkspaceBytes) => {
|
|
3085
|
+
let entries = 0;
|
|
3086
|
+
let totalBytes = 0;
|
|
3087
|
+
const metadata = createHash6("sha256");
|
|
3088
|
+
const visit = async (directory, relativeDirectory = "") => {
|
|
3089
|
+
for (const name of (await readdir2(directory)).sort()) {
|
|
3090
|
+
const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
|
3091
|
+
if (relative === "node_modules")
|
|
3092
|
+
continue;
|
|
3093
|
+
const absolute = path7.join(directory, name);
|
|
3094
|
+
if (!isInside3(root, absolute) || relative.length > 1024) {
|
|
3095
|
+
throw new Error(`OCI workspace snapshot contains an invalid path: ${relative}.`);
|
|
3096
|
+
}
|
|
3097
|
+
entries += 1;
|
|
3098
|
+
if (entries > MAX_SYNC_ENTRIES) {
|
|
3099
|
+
throw new Error(`OCI workspace snapshot exceeds the ${MAX_SYNC_ENTRIES}-entry limit.`);
|
|
3100
|
+
}
|
|
3101
|
+
const entry = await lstat5(absolute);
|
|
3102
|
+
if (entry.isSymbolicLink()) {
|
|
3103
|
+
throw new Error(`OCI workspace snapshot contains a symbolic link: ${relative}.`);
|
|
3104
|
+
}
|
|
3105
|
+
if (entry.isDirectory()) {
|
|
3106
|
+
updateSnapshotMetadata(metadata, "directory", relative, entry);
|
|
3107
|
+
await visit(absolute, relative);
|
|
3108
|
+
continue;
|
|
3109
|
+
}
|
|
3110
|
+
if (!entry.isFile() || entry.nlink !== 1) {
|
|
3111
|
+
throw new Error(`OCI workspace snapshot contains a special or linked file: ${relative}.`);
|
|
3112
|
+
}
|
|
3113
|
+
totalBytes += entry.size;
|
|
3114
|
+
if (totalBytes > maxWorkspaceBytes) {
|
|
3115
|
+
throw new Error(`OCI workspace snapshot exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
|
|
3116
|
+
}
|
|
3117
|
+
updateSnapshotMetadata(metadata, "file", relative, entry);
|
|
3118
|
+
}
|
|
3119
|
+
};
|
|
3120
|
+
await visit(root);
|
|
3121
|
+
return `sha256:${metadata.digest("hex")}`;
|
|
2751
3122
|
};
|
|
2752
3123
|
var replaceSnapshot = async (snapshotRoot, stagedRoot) => {
|
|
2753
3124
|
const backupRoot = `${snapshotRoot}.${randomUUID3()}.previous`;
|
|
@@ -2762,10 +3133,42 @@ var replaceSnapshot = async (snapshotRoot, stagedRoot) => {
|
|
|
2762
3133
|
}
|
|
2763
3134
|
await rm(backupRoot, { recursive: true, force: true });
|
|
2764
3135
|
};
|
|
3136
|
+
var OCI_WORKSPACE_SEAL_SCRIPT = [
|
|
3137
|
+
"import { createHash } from 'node:crypto';",
|
|
3138
|
+
"import { lstat, readFile, readdir, readlink, rm } from 'node:fs/promises';",
|
|
3139
|
+
"import path from 'node:path';",
|
|
3140
|
+
"const root='/workspace';",
|
|
3141
|
+
"const seal=createHash('sha256');",
|
|
3142
|
+
"const modules=path.join(root,'node_modules');",
|
|
3143
|
+
"if(process.env.ZHIVEX_HARNESS_HAS_DEPENDENCIES==='1'){const entry=await lstat(modules);if(!entry.isSymbolicLink()||(await readlink(modules))!=='/dependencies')throw new Error('read-only dependency mount was replaced');}else{await rm(modules,{recursive:true,force:true});}",
|
|
3144
|
+
"const digest=(value)=>`sha256:${createHash('sha256').update(value).digest('hex')}`;",
|
|
3145
|
+
"const same=(before,after)=>before.dev===after.dev&&before.ino===after.ino&&before.size===after.size&&before.mtimeMs===after.mtimeMs&&before.ctimeMs===after.ctimeMs&&(before.mode&0o777)===(after.mode&0o777);",
|
|
3146
|
+
"const visit=async(directory,base='')=>{",
|
|
3147
|
+
"for(const name of (await readdir(directory)).sort()){",
|
|
3148
|
+
"const relative=base?`${base}/${name}`:name;",
|
|
3149
|
+
"if(relative==='node_modules')continue;",
|
|
3150
|
+
"const absolute=path.join(directory,name);",
|
|
3151
|
+
"const entry=await lstat(absolute);",
|
|
3152
|
+
"if(entry.isSymbolicLink())throw new Error(`symbolic link: ${relative}`);",
|
|
3153
|
+
"if(entry.isDirectory()){seal.update(`directory\\0${relative}\\0${entry.mode&0o777}\\0`);await visit(absolute,relative);continue;}",
|
|
3154
|
+
"if(!entry.isFile()||entry.nlink!==1)throw new Error(`special or linked file: ${relative}`);",
|
|
3155
|
+
"const contents=await readFile(absolute);",
|
|
3156
|
+
"const after=await lstat(absolute);",
|
|
3157
|
+
"if(!same(entry,after))throw new Error(`changed while sealing: ${relative}`);",
|
|
3158
|
+
"seal.update(`file\\0${relative}\\0${entry.mode&0o777}\\0${entry.size}\\0${digest(contents)}\\0`);",
|
|
3159
|
+
"}",
|
|
3160
|
+
"};",
|
|
3161
|
+
"await visit(root);",
|
|
3162
|
+
"for(const name of await readdir('/tmp'))await rm(path.join('/tmp',name),{recursive:true,force:true});",
|
|
3163
|
+
"console.log(`sha256:${seal.digest('hex')}`);"
|
|
3164
|
+
].join("");
|
|
3165
|
+
var OCI_SESSION_CONTROLLER_SCRIPT = "setInterval(()=>{},2147483647);";
|
|
2765
3166
|
|
|
2766
3167
|
class CliOciRuntimeAdapter {
|
|
2767
3168
|
runtime;
|
|
2768
3169
|
hostEnvironment;
|
|
3170
|
+
sessions = new Map;
|
|
3171
|
+
runQueues = new Map;
|
|
2769
3172
|
constructor(runtime) {
|
|
2770
3173
|
this.runtime = runtime;
|
|
2771
3174
|
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 +3221,44 @@ ${result.stdout}`)) {
|
|
|
2818
3221
|
throw new Error(`${this.runtime} could not remove OCI workspace volume ${name}: ${result.stderr.trim()}`);
|
|
2819
3222
|
}
|
|
2820
3223
|
}
|
|
2821
|
-
|
|
3224
|
+
requestFingerprint(request) {
|
|
3225
|
+
return digest2(JSON.stringify({
|
|
3226
|
+
runId: request.runId,
|
|
3227
|
+
snapshotRoot: request.snapshotRoot,
|
|
3228
|
+
dependencyRoot: request.dependencyRoot ?? null,
|
|
3229
|
+
imageId: request.imageId,
|
|
3230
|
+
limits: request.limits
|
|
3231
|
+
}));
|
|
3232
|
+
}
|
|
3233
|
+
async withRunLock(runId, operation) {
|
|
3234
|
+
const key = runHash(runId);
|
|
3235
|
+
const previous = this.runQueues.get(key) ?? Promise.resolve();
|
|
3236
|
+
let release;
|
|
3237
|
+
const gate = new Promise((resolve) => {
|
|
3238
|
+
release = resolve;
|
|
3239
|
+
});
|
|
3240
|
+
const tail = previous.then(() => gate, () => gate);
|
|
3241
|
+
this.runQueues.set(key, tail);
|
|
3242
|
+
await previous.catch(() => {
|
|
3243
|
+
return;
|
|
3244
|
+
});
|
|
3245
|
+
try {
|
|
3246
|
+
return await operation();
|
|
3247
|
+
} finally {
|
|
3248
|
+
release();
|
|
3249
|
+
if (this.runQueues.get(key) === tail)
|
|
3250
|
+
this.runQueues.delete(key);
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
async destroySession(session) {
|
|
3254
|
+
if (this.sessions.get(session.key) === session)
|
|
3255
|
+
this.sessions.delete(session.key);
|
|
3256
|
+
await this.forceRemove(session.name);
|
|
3257
|
+
await this.removeVolume(session.volumeName);
|
|
3258
|
+
}
|
|
3259
|
+
async createSession(request, requestFingerprint, snapshotSeal, hostFingerprint) {
|
|
2822
3260
|
const resourceName = `zhivex-harness-${runHash(request.runId)}-${randomUUID3().slice(0, 8)}`;
|
|
2823
3261
|
const name = `${resourceName}-job`;
|
|
2824
|
-
const exportName = `${resourceName}-export`;
|
|
2825
3262
|
const volumeName = `${resourceName}-workspace`;
|
|
2826
3263
|
const uid = process.getuid?.() ?? 65532;
|
|
2827
3264
|
const gid = process.getgid?.() ?? 65532;
|
|
@@ -2877,8 +3314,6 @@ ${result.stdout}`)) {
|
|
|
2877
3314
|
"BUN_INSTALL_CACHE_DIR=/tmp/bun-cache",
|
|
2878
3315
|
"--env",
|
|
2879
3316
|
"CI=1",
|
|
2880
|
-
"--volume",
|
|
2881
|
-
`${request.snapshotRoot}:/seed:ro`,
|
|
2882
3317
|
...request.dependencyRoot ? ["--volume", `${request.dependencyRoot}:/dependencies:ro`] : [],
|
|
2883
3318
|
"--workdir",
|
|
2884
3319
|
"/workspace",
|
|
@@ -2886,26 +3321,19 @@ ${result.stdout}`)) {
|
|
|
2886
3321
|
"bun",
|
|
2887
3322
|
request.imageId,
|
|
2888
3323
|
"-e",
|
|
2889
|
-
|
|
3324
|
+
OCI_SESSION_CONTROLLER_SCRIPT
|
|
2890
3325
|
];
|
|
2891
|
-
let
|
|
2892
|
-
const forceRemove = () => {
|
|
2893
|
-
forcedRemoval ??= this.forceRemove(name);
|
|
2894
|
-
};
|
|
3326
|
+
let volumeCreated = false;
|
|
2895
3327
|
try {
|
|
2896
3328
|
await this.cli(volumeArgs);
|
|
3329
|
+
volumeCreated = true;
|
|
2897
3330
|
await this.cli(args);
|
|
2898
3331
|
await this.cli(["start", name]);
|
|
2899
3332
|
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 })"
|
|
3333
|
+
"cp",
|
|
3334
|
+
"-a",
|
|
3335
|
+
`${request.snapshotRoot}${path7.sep}.`,
|
|
3336
|
+
`${name}:/workspace`
|
|
2909
3337
|
], 60000);
|
|
2910
3338
|
if (request.dependencyRoot) {
|
|
2911
3339
|
await this.cli([
|
|
@@ -2920,88 +3348,183 @@ ${result.stdout}`)) {
|
|
|
2920
3348
|
"import { symlink } from 'node:fs/promises'; await symlink('/dependencies', '/workspace/node_modules', 'dir')"
|
|
2921
3349
|
]);
|
|
2922
3350
|
}
|
|
2923
|
-
|
|
2924
|
-
|
|
3351
|
+
await this.cli(["pause", name]);
|
|
3352
|
+
const session = {
|
|
3353
|
+
key: runHash(request.runId),
|
|
3354
|
+
requestFingerprint,
|
|
3355
|
+
name,
|
|
3356
|
+
volumeName,
|
|
3357
|
+
snapshotSeal,
|
|
3358
|
+
hostFingerprint,
|
|
3359
|
+
paused: true
|
|
3360
|
+
};
|
|
3361
|
+
this.sessions.set(session.key, session);
|
|
3362
|
+
return session;
|
|
3363
|
+
} catch (error) {
|
|
3364
|
+
await this.forceRemove(name);
|
|
3365
|
+
if (volumeCreated)
|
|
3366
|
+
await this.removeVolume(volumeName).catch(() => {
|
|
3367
|
+
return;
|
|
3368
|
+
});
|
|
3369
|
+
throw error;
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
async onlyControllerProcessRemains(session) {
|
|
3373
|
+
const result = await this.cli(this.runtime === "podman" ? ["top", session.name, "pid"] : ["top", session.name, "-eo", "pid"]);
|
|
3374
|
+
const processIds = result.stdout.split(/\r?\n/).filter((line) => /^\s*\d+\s*$/.test(line));
|
|
3375
|
+
return processIds.length === 1;
|
|
3376
|
+
}
|
|
3377
|
+
async containerWorkspaceSeal(session, uid, gid, hasDependencies) {
|
|
3378
|
+
if (session.paused) {
|
|
3379
|
+
await this.cli(["unpause", session.name]);
|
|
3380
|
+
session.paused = false;
|
|
3381
|
+
}
|
|
3382
|
+
try {
|
|
3383
|
+
const result = await this.cli([
|
|
2925
3384
|
"exec",
|
|
2926
3385
|
"--workdir",
|
|
2927
3386
|
"/workspace",
|
|
2928
3387
|
"--user",
|
|
2929
3388
|
`${uid}:${gid}`,
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
})
|
|
2939
|
-
|
|
2940
|
-
forceRemove();
|
|
2941
|
-
await forcedRemoval;
|
|
2942
|
-
} else if (result.exitCode === 0) {
|
|
2943
|
-
await this.cli(["pause", name]);
|
|
2944
|
-
const stagedRoot = path7.join(path7.dirname(request.snapshotRoot), `.workspace-sync-${randomUUID3()}`);
|
|
2945
|
-
try {
|
|
2946
|
-
await privateDirectory(stagedRoot);
|
|
2947
|
-
try {
|
|
2948
|
-
await this.cli([
|
|
2949
|
-
"run",
|
|
2950
|
-
"--rm",
|
|
2951
|
-
"--name",
|
|
2952
|
-
exportName,
|
|
2953
|
-
"--label",
|
|
2954
|
-
`${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`,
|
|
2955
|
-
"--label",
|
|
2956
|
-
`com.zhivex.harness.run=${runHash(request.runId)}`,
|
|
2957
|
-
"--network",
|
|
2958
|
-
"none",
|
|
2959
|
-
"--read-only",
|
|
2960
|
-
"--cap-drop",
|
|
2961
|
-
"ALL",
|
|
2962
|
-
"--security-opt",
|
|
2963
|
-
"no-new-privileges",
|
|
2964
|
-
"--pids-limit",
|
|
2965
|
-
"32",
|
|
2966
|
-
"--memory",
|
|
2967
|
-
"256m",
|
|
2968
|
-
"--cpus",
|
|
2969
|
-
"1",
|
|
2970
|
-
"--tmpfs",
|
|
2971
|
-
"/tmp:rw,noexec,nosuid,nodev,size=16m",
|
|
2972
|
-
"--user",
|
|
2973
|
-
`${uid}:${gid}`,
|
|
2974
|
-
"--volume",
|
|
2975
|
-
`${volumeName}:/workspace:ro`,
|
|
2976
|
-
"--volume",
|
|
2977
|
-
`${stagedRoot}:/export:rw`,
|
|
2978
|
-
"--workdir",
|
|
2979
|
-
"/workspace",
|
|
2980
|
-
"--entrypoint",
|
|
2981
|
-
"bun",
|
|
2982
|
-
request.imageId,
|
|
2983
|
-
"-e",
|
|
2984
|
-
"import { cp } from 'node:fs/promises'; await cp('/workspace', '/export', { recursive: true, force: false, errorOnExist: true })"
|
|
2985
|
-
], 60000);
|
|
2986
|
-
} finally {
|
|
2987
|
-
await this.forceRemove(exportName);
|
|
2988
|
-
}
|
|
2989
|
-
await validateSynchronizedSnapshot(stagedRoot, request.snapshotRoot, request.limits.maxWorkspaceBytes, request.limits.maxFileWriteBytes);
|
|
2990
|
-
await replaceSnapshot(request.snapshotRoot, stagedRoot);
|
|
2991
|
-
} finally {
|
|
2992
|
-
await rm(stagedRoot, { recursive: true, force: true });
|
|
2993
|
-
}
|
|
3389
|
+
"--env",
|
|
3390
|
+
`ZHIVEX_HARNESS_HAS_DEPENDENCIES=${hasDependencies ? "1" : "0"}`,
|
|
3391
|
+
session.name,
|
|
3392
|
+
"bun",
|
|
3393
|
+
"-e",
|
|
3394
|
+
OCI_WORKSPACE_SEAL_SCRIPT
|
|
3395
|
+
], 60000, 20000);
|
|
3396
|
+
const seal = result.stdout.trim();
|
|
3397
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(seal)) {
|
|
3398
|
+
throw new Error("OCI workspace seal helper returned an invalid digest.");
|
|
2994
3399
|
}
|
|
2995
|
-
return
|
|
2996
|
-
...result,
|
|
2997
|
-
command: request.command
|
|
2998
|
-
};
|
|
3400
|
+
return seal;
|
|
2999
3401
|
} finally {
|
|
3402
|
+
if (!session.paused) {
|
|
3403
|
+
await this.cli(["pause", session.name]);
|
|
3404
|
+
session.paused = true;
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
async runInSession(request, session, sessionReused) {
|
|
3409
|
+
const uid = process.getuid?.() ?? 65532;
|
|
3410
|
+
const gid = process.getgid?.() ?? 65532;
|
|
3411
|
+
if (session.paused) {
|
|
3412
|
+
await this.cli(["unpause", session.name]);
|
|
3413
|
+
session.paused = false;
|
|
3414
|
+
}
|
|
3415
|
+
let forcedRemoval;
|
|
3416
|
+
const forceRemove = () => {
|
|
3417
|
+
forcedRemoval ??= this.forceRemove(session.name);
|
|
3418
|
+
};
|
|
3419
|
+
const result = await spawnBounded2([
|
|
3420
|
+
this.runtime,
|
|
3421
|
+
"exec",
|
|
3422
|
+
"--workdir",
|
|
3423
|
+
"/workspace",
|
|
3424
|
+
"--user",
|
|
3425
|
+
`${uid}:${gid}`,
|
|
3426
|
+
session.name,
|
|
3427
|
+
...request.command
|
|
3428
|
+
], {
|
|
3429
|
+
env: this.hostEnvironment,
|
|
3430
|
+
timeoutMs: request.limits.maxProcessRuntimeMs,
|
|
3431
|
+
maxOutputBytes: request.limits.maxProcessOutputBytes,
|
|
3432
|
+
...request.abortSignal ? { abortSignal: request.abortSignal } : {},
|
|
3433
|
+
onStop: forceRemove
|
|
3434
|
+
});
|
|
3435
|
+
if (result.exitCode !== 0 || result.timedOut || result.cancelled || result.outputLimitExceeded) {
|
|
3000
3436
|
forceRemove();
|
|
3001
3437
|
await forcedRemoval;
|
|
3002
|
-
await this.
|
|
3438
|
+
await this.destroySession(session);
|
|
3439
|
+
return {
|
|
3440
|
+
...result,
|
|
3441
|
+
command: request.command,
|
|
3442
|
+
sessionReused,
|
|
3443
|
+
workspacePublished: false,
|
|
3444
|
+
workspaceExported: false
|
|
3445
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
try {
|
|
3448
|
+
if (!await this.onlyControllerProcessRemains(session)) {
|
|
3449
|
+
await this.destroySession(session);
|
|
3450
|
+
return {
|
|
3451
|
+
...result,
|
|
3452
|
+
command: request.command,
|
|
3453
|
+
exitCode: 126,
|
|
3454
|
+
stderr: [
|
|
3455
|
+
result.stderr,
|
|
3456
|
+
"OCI command left background processes running; its workspace changes were discarded."
|
|
3457
|
+
].filter(Boolean).join(`
|
|
3458
|
+
`),
|
|
3459
|
+
sessionReused,
|
|
3460
|
+
workspacePublished: false,
|
|
3461
|
+
workspaceExported: false
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
const workspaceSeal = await this.containerWorkspaceSeal(session, uid, gid, Boolean(request.dependencyRoot));
|
|
3465
|
+
if (workspaceSeal === session.snapshotSeal) {
|
|
3466
|
+
return {
|
|
3467
|
+
...result,
|
|
3468
|
+
command: request.command,
|
|
3469
|
+
sessionReused,
|
|
3470
|
+
workspacePublished: true,
|
|
3471
|
+
workspaceExported: false
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
const stagedRoot = path7.join(path7.dirname(request.snapshotRoot), `.workspace-sync-${randomUUID3()}`);
|
|
3475
|
+
try {
|
|
3476
|
+
await privateDirectory(stagedRoot);
|
|
3477
|
+
await this.cli(["cp", `${session.name}:/workspace/.`, stagedRoot], 60000);
|
|
3478
|
+
const validated = await validateSynchronizedSnapshot(stagedRoot, request.snapshotRoot, request.limits.maxWorkspaceBytes, request.limits.maxFileWriteBytes);
|
|
3479
|
+
await replaceSnapshot(request.snapshotRoot, stagedRoot);
|
|
3480
|
+
session.snapshotSeal = validated.seal;
|
|
3481
|
+
session.hostFingerprint = validated.metadataFingerprint;
|
|
3482
|
+
} finally {
|
|
3483
|
+
await rm(stagedRoot, { recursive: true, force: true });
|
|
3484
|
+
}
|
|
3485
|
+
return {
|
|
3486
|
+
...result,
|
|
3487
|
+
command: request.command,
|
|
3488
|
+
sessionReused,
|
|
3489
|
+
workspacePublished: true,
|
|
3490
|
+
workspaceExported: true
|
|
3491
|
+
};
|
|
3492
|
+
} catch (error) {
|
|
3493
|
+
await this.destroySession(session);
|
|
3494
|
+
throw error;
|
|
3003
3495
|
}
|
|
3004
3496
|
}
|
|
3497
|
+
async runLocked(request) {
|
|
3498
|
+
const key = runHash(request.runId);
|
|
3499
|
+
const requestFingerprint = this.requestFingerprint(request);
|
|
3500
|
+
let session = this.sessions.get(key);
|
|
3501
|
+
let sessionReused = Boolean(session);
|
|
3502
|
+
let validated;
|
|
3503
|
+
if (session) {
|
|
3504
|
+
const hostFingerprint = await snapshotMetadataFingerprint(request.snapshotRoot, request.limits.maxWorkspaceBytes);
|
|
3505
|
+
if (session.requestFingerprint !== requestFingerprint || session.hostFingerprint !== hostFingerprint) {
|
|
3506
|
+
await this.destroySession(session);
|
|
3507
|
+
session = undefined;
|
|
3508
|
+
sessionReused = false;
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
if (!session) {
|
|
3512
|
+
validated = await validateSynchronizedSnapshot(request.snapshotRoot, request.snapshotRoot, request.limits.maxWorkspaceBytes, request.limits.maxFileWriteBytes);
|
|
3513
|
+
session = await this.createSession(request, requestFingerprint, validated.seal, validated.metadataFingerprint);
|
|
3514
|
+
}
|
|
3515
|
+
try {
|
|
3516
|
+
return await this.runInSession(request, session, sessionReused);
|
|
3517
|
+
} catch (error) {
|
|
3518
|
+
if (this.sessions.get(key) === session)
|
|
3519
|
+
await this.destroySession(session).catch(() => {
|
|
3520
|
+
return;
|
|
3521
|
+
});
|
|
3522
|
+
throw error;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
async run(request) {
|
|
3526
|
+
return this.withRunLock(request.runId, () => this.runLocked(request));
|
|
3527
|
+
}
|
|
3005
3528
|
async removeByFilters(filters) {
|
|
3006
3529
|
const listed = await spawnBounded2([this.runtime, "ps", "-aq", ...filters.flatMap((filter) => ["--filter", filter])], {
|
|
3007
3530
|
env: this.hostEnvironment,
|
|
@@ -3047,15 +3570,23 @@ ${result.stdout}`)) {
|
|
|
3047
3570
|
return removed;
|
|
3048
3571
|
}
|
|
3049
3572
|
async removeRunContainers(runId) {
|
|
3573
|
+
const key = runHash(runId);
|
|
3574
|
+
const session = this.sessions.get(key);
|
|
3575
|
+
let cachedResources = 0;
|
|
3576
|
+
if (session) {
|
|
3577
|
+
await this.destroySession(session);
|
|
3578
|
+
cachedResources = 2;
|
|
3579
|
+
}
|
|
3050
3580
|
const filters = [
|
|
3051
3581
|
`label=${HARNESS_OCI_LABEL}=${HARNESS_OCI_LABEL_VALUE}`,
|
|
3052
|
-
`label=com.zhivex.harness.run=${
|
|
3582
|
+
`label=com.zhivex.harness.run=${key}`
|
|
3053
3583
|
];
|
|
3054
3584
|
const containers = await this.removeByFilters(filters);
|
|
3055
3585
|
const volumes = await this.removeVolumesByFilters(filters);
|
|
3056
|
-
return containers + volumes;
|
|
3586
|
+
return cachedResources + containers + volumes;
|
|
3057
3587
|
}
|
|
3058
3588
|
async cleanupOrphans() {
|
|
3589
|
+
this.sessions.clear();
|
|
3059
3590
|
let removed = 0;
|
|
3060
3591
|
for (const status of ["created", "exited", "dead", "paused"]) {
|
|
3061
3592
|
removed += await this.removeByFilters([
|
|
@@ -3069,34 +3600,81 @@ ${result.stdout}`)) {
|
|
|
3069
3600
|
return removed;
|
|
3070
3601
|
}
|
|
3071
3602
|
}
|
|
3072
|
-
var
|
|
3603
|
+
var SNAPSHOT_INVENTORY_PAGE_SIZE = 5000;
|
|
3604
|
+
var COPY_ON_WRITE_UNSUPPORTED_CODES = new Set(["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP"]);
|
|
3605
|
+
var collectSnapshotInventory = async (workspace, metrics) => {
|
|
3073
3606
|
const files = new Map;
|
|
3607
|
+
if (metrics)
|
|
3608
|
+
metrics.inventoryPasses += 1;
|
|
3074
3609
|
let cursor;
|
|
3075
3610
|
do {
|
|
3076
|
-
const page = await workspace.listFiles(".", {
|
|
3611
|
+
const page = await workspace.listFiles(".", {
|
|
3612
|
+
limit: SNAPSHOT_INVENTORY_PAGE_SIZE,
|
|
3613
|
+
...cursor ? { cursor } : {}
|
|
3614
|
+
});
|
|
3615
|
+
if (metrics)
|
|
3616
|
+
metrics.inventoryPages += 1;
|
|
3077
3617
|
for (const file of page.files) {
|
|
3078
3618
|
const absolute = path7.join(workspace.root, ...file.path.split("/"));
|
|
3079
3619
|
if (!isInside3(workspace.root, absolute))
|
|
3080
3620
|
throw new Error(`Snapshot file escaped its workspace: ${file.path}.`);
|
|
3081
|
-
const
|
|
3082
|
-
if (
|
|
3621
|
+
const entry = await lstat5(absolute);
|
|
3622
|
+
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
3083
3623
|
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
3624
|
}
|
|
3089
|
-
if (
|
|
3090
|
-
throw new Error(`Snapshot
|
|
3091
|
-
|
|
3625
|
+
if (entry.size !== file.size) {
|
|
3626
|
+
throw new Error(`Snapshot source changed while inventorying: ${file.path}.`);
|
|
3627
|
+
}
|
|
3628
|
+
files.set(file.path, {
|
|
3629
|
+
path: file.path,
|
|
3630
|
+
digest: file.digest,
|
|
3631
|
+
mode: entry.mode & 511,
|
|
3632
|
+
bytes: file.size
|
|
3633
|
+
});
|
|
3092
3634
|
}
|
|
3093
3635
|
cursor = page.nextCursor;
|
|
3094
3636
|
} while (cursor);
|
|
3095
3637
|
return files;
|
|
3096
3638
|
};
|
|
3097
|
-
var
|
|
3098
|
-
const
|
|
3099
|
-
|
|
3639
|
+
var readSnapshotFile = async (workspace, expected, metrics) => {
|
|
3640
|
+
const absolute = path7.join(workspace.root, ...expected.path.split("/"));
|
|
3641
|
+
if (!isInside3(workspace.root, absolute)) {
|
|
3642
|
+
throw new Error(`Snapshot file escaped its workspace: ${expected.path}.`);
|
|
3643
|
+
}
|
|
3644
|
+
const before = await lstat5(absolute);
|
|
3645
|
+
if (before.isSymbolicLink() || !before.isFile()) {
|
|
3646
|
+
throw new Error(`Snapshot source is not a regular file: ${expected.path}.`);
|
|
3647
|
+
}
|
|
3648
|
+
const contents = await readFile3(absolute);
|
|
3649
|
+
const after = await lstat5(absolute);
|
|
3650
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || (before.mode & 511) !== (after.mode & 511)) {
|
|
3651
|
+
throw new Error(`Snapshot source changed while reading: ${expected.path}.`);
|
|
3652
|
+
}
|
|
3653
|
+
const actualDigest = digest2(contents);
|
|
3654
|
+
if (actualDigest !== expected.digest || contents.byteLength !== expected.bytes || (before.mode & 511) !== expected.mode) {
|
|
3655
|
+
throw new Error(`Snapshot source changed after inventory: ${expected.path}.`);
|
|
3656
|
+
}
|
|
3657
|
+
if (metrics) {
|
|
3658
|
+
metrics.verifiedContentReads += 1;
|
|
3659
|
+
metrics.verifiedContentBytes += contents.byteLength;
|
|
3660
|
+
}
|
|
3661
|
+
return { ...expected, contents };
|
|
3662
|
+
};
|
|
3663
|
+
var copySnapshotFile = async (source, target, metrics) => {
|
|
3664
|
+
try {
|
|
3665
|
+
await copyFile(source, target, fsConstants.COPYFILE_FICLONE_FORCE);
|
|
3666
|
+
} catch (error) {
|
|
3667
|
+
const code = error.code;
|
|
3668
|
+
if (!code || !COPY_ON_WRITE_UNSUPPORTED_CODES.has(code))
|
|
3669
|
+
throw error;
|
|
3670
|
+
if (metrics)
|
|
3671
|
+
metrics.snapshotCloneFallbacks += 1;
|
|
3672
|
+
await copyFile(source, target);
|
|
3673
|
+
}
|
|
3674
|
+
};
|
|
3675
|
+
var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceBytes, metrics) => {
|
|
3676
|
+
const files = await collectSnapshotInventory(source, metrics);
|
|
3677
|
+
const totalBytes = [...files.values()].reduce((total, file) => total + file.bytes, 0);
|
|
3100
3678
|
if (totalBytes > maxWorkspaceBytes) {
|
|
3101
3679
|
throw new Error(`Workspace snapshot exceeds the ${maxWorkspaceBytes}-byte OCI limit.`);
|
|
3102
3680
|
}
|
|
@@ -3104,13 +3682,22 @@ var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceB
|
|
|
3104
3682
|
await rm(root, { recursive: true, force: true });
|
|
3105
3683
|
await privateDirectory(root);
|
|
3106
3684
|
}
|
|
3107
|
-
for (const
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3685
|
+
for (const expected of files.values()) {
|
|
3686
|
+
const file = await readSnapshotFile(source, expected, metrics);
|
|
3687
|
+
const baseTarget = path7.join(baseRoot, ...file.path.split("/"));
|
|
3688
|
+
const snapshotTarget = path7.join(snapshotRoot, ...file.path.split("/"));
|
|
3689
|
+
await Promise.all([
|
|
3690
|
+
mkdir3(path7.dirname(baseTarget), { recursive: true, mode: 448 }),
|
|
3691
|
+
mkdir3(path7.dirname(snapshotTarget), { recursive: true, mode: 448 })
|
|
3692
|
+
]);
|
|
3693
|
+
await writeFile(baseTarget, file.contents, { mode: file.mode });
|
|
3694
|
+
await chmod2(baseTarget, file.mode);
|
|
3695
|
+
await copySnapshotFile(baseTarget, snapshotTarget, metrics);
|
|
3696
|
+
await chmod2(snapshotTarget, file.mode);
|
|
3697
|
+
}
|
|
3698
|
+
if (metrics) {
|
|
3699
|
+
metrics.snapshotFiles += files.size;
|
|
3700
|
+
metrics.snapshotBytes += totalBytes;
|
|
3114
3701
|
}
|
|
3115
3702
|
return { files: files.size, bytes: totalBytes };
|
|
3116
3703
|
};
|
|
@@ -3129,9 +3716,11 @@ var textContent = (file, maxFileWriteBytes) => {
|
|
|
3129
3716
|
}
|
|
3130
3717
|
return file.contents.toString("utf8");
|
|
3131
3718
|
};
|
|
3132
|
-
var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes) => {
|
|
3133
|
-
const before = await
|
|
3134
|
-
|
|
3719
|
+
var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes, metrics) => {
|
|
3720
|
+
const [before, after] = await Promise.all([
|
|
3721
|
+
collectSnapshotInventory(base, metrics),
|
|
3722
|
+
collectSnapshotInventory(current, metrics)
|
|
3723
|
+
]);
|
|
3135
3724
|
const paths = [...new Set([...before.keys(), ...after.keys()])].sort();
|
|
3136
3725
|
const entries = [];
|
|
3137
3726
|
let totalBytes = 0;
|
|
@@ -3141,9 +3730,9 @@ var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes) =>
|
|
|
3141
3730
|
if (oldFile?.digest === newFile?.digest && oldFile?.mode === newFile?.mode)
|
|
3142
3731
|
continue;
|
|
3143
3732
|
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?.
|
|
3733
|
+
const beforeContent = oldFile ? textContent(await readSnapshotFile(base, oldFile, metrics), maxFileWriteBytes) : undefined;
|
|
3734
|
+
const afterContent = newFile ? textContent(await readSnapshotFile(current, newFile, metrics), maxFileWriteBytes) : undefined;
|
|
3735
|
+
const bytes = newFile?.bytes ?? 0;
|
|
3147
3736
|
totalBytes += bytes;
|
|
3148
3737
|
entries.push({
|
|
3149
3738
|
path: filePath,
|
|
@@ -3183,8 +3772,8 @@ var inspectHostPrecondition = async (workspace, filePath, expectedDigest, expect
|
|
|
3183
3772
|
throw error;
|
|
3184
3773
|
}
|
|
3185
3774
|
};
|
|
3186
|
-
var importPatch = async (runId, expectedPatchId, host, base, current, maxFileWriteBytes) => {
|
|
3187
|
-
const patch = await createEnvironmentPatch(runId, base, current, maxFileWriteBytes);
|
|
3775
|
+
var importPatch = async (runId, expectedPatchId, host, base, current, maxFileWriteBytes, metrics) => {
|
|
3776
|
+
const patch = await createEnvironmentPatch(runId, base, current, maxFileWriteBytes, metrics);
|
|
3188
3777
|
if (patch.patchId !== expectedPatchId) {
|
|
3189
3778
|
throw new Error("Environment patch changed after review; inspect it again before import.");
|
|
3190
3779
|
}
|
|
@@ -3278,7 +3867,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3278
3867
|
version: `${HARNESS_EXECUTION_POLICY_VERSION}:${image.imageDigest}`,
|
|
3279
3868
|
backend: "container",
|
|
3280
3869
|
assurance: "enforced",
|
|
3281
|
-
isolation: "per-
|
|
3870
|
+
isolation: "per-run",
|
|
3282
3871
|
workspace: {
|
|
3283
3872
|
id: workspaceIdentity,
|
|
3284
3873
|
root: "/workspace",
|
|
@@ -3315,6 +3904,8 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3315
3904
|
maxPids: options.config.maxPids,
|
|
3316
3905
|
maxCpus: options.config.maxCpus,
|
|
3317
3906
|
tmpfsMb: options.config.tmpfsMb,
|
|
3907
|
+
containerLifecycle: "warm-per-acquired-run",
|
|
3908
|
+
failedCommandRecovery: "discard-and-reseed",
|
|
3318
3909
|
patchImportApproval: true,
|
|
3319
3910
|
hostMutationMode: "reviewed-environment-patch"
|
|
3320
3911
|
}
|
|
@@ -3326,6 +3917,19 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3326
3917
|
image,
|
|
3327
3918
|
runtime,
|
|
3328
3919
|
async acquire(request) {
|
|
3920
|
+
const ioMetrics = {
|
|
3921
|
+
inventoryPasses: 0,
|
|
3922
|
+
inventoryPages: 0,
|
|
3923
|
+
verifiedContentReads: 0,
|
|
3924
|
+
verifiedContentBytes: 0,
|
|
3925
|
+
snapshotFiles: 0,
|
|
3926
|
+
snapshotBytes: 0,
|
|
3927
|
+
snapshotCloneFallbacks: 0,
|
|
3928
|
+
containerStarts: 0,
|
|
3929
|
+
containerReuses: 0,
|
|
3930
|
+
workspacePublishes: 0,
|
|
3931
|
+
workspaceExports: 0
|
|
3932
|
+
};
|
|
3329
3933
|
await privateDirectory(environmentRoot);
|
|
3330
3934
|
const directory = path7.join(environmentRoot, runHash(request.runId));
|
|
3331
3935
|
await privateDirectory(directory);
|
|
@@ -3344,7 +3948,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3344
3948
|
} catch (error) {
|
|
3345
3949
|
if (error.code !== "ENOENT")
|
|
3346
3950
|
throw error;
|
|
3347
|
-
const snapshot = await copyWorkspaceSnapshot(options.workspace, baseRoot, snapshotRoot, options.config.maxWorkspaceBytes);
|
|
3951
|
+
const snapshot = await copyWorkspaceSnapshot(options.workspace, baseRoot, snapshotRoot, options.config.maxWorkspaceBytes, ioMetrics);
|
|
3348
3952
|
const now = new Date().toISOString();
|
|
3349
3953
|
metadata = {
|
|
3350
3954
|
schemaVersion: HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION,
|
|
@@ -3401,6 +4005,14 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3401
4005
|
},
|
|
3402
4006
|
...context?.abortSignal ?? request.abortSignal ? { abortSignal: context?.abortSignal ?? request.abortSignal } : {}
|
|
3403
4007
|
});
|
|
4008
|
+
if (result.sessionReused === false)
|
|
4009
|
+
ioMetrics.containerStarts += 1;
|
|
4010
|
+
if (result.sessionReused === true)
|
|
4011
|
+
ioMetrics.containerReuses += 1;
|
|
4012
|
+
if (result.workspacePublished === true)
|
|
4013
|
+
ioMetrics.workspacePublishes += 1;
|
|
4014
|
+
if (result.workspaceExported === true)
|
|
4015
|
+
ioMetrics.workspaceExports += 1;
|
|
3404
4016
|
const forcedExitCode = result.cancelled ? 130 : result.timedOut ? 124 : result.outputLimitExceeded ? 125 : result.exitCode;
|
|
3405
4017
|
return {
|
|
3406
4018
|
command: result.command,
|
|
@@ -3447,7 +4059,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3447
4059
|
return await operation();
|
|
3448
4060
|
},
|
|
3449
4061
|
async status() {
|
|
3450
|
-
const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes);
|
|
4062
|
+
const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes, ioMetrics);
|
|
3451
4063
|
return {
|
|
3452
4064
|
schemaVersion: HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION,
|
|
3453
4065
|
kind: "environment-status",
|
|
@@ -3461,7 +4073,8 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3461
4073
|
rootFilesystem: "read-only",
|
|
3462
4074
|
workspace: "ephemeral-snapshot",
|
|
3463
4075
|
patchId: patch.patchId,
|
|
3464
|
-
changedFiles: patch.entries.length
|
|
4076
|
+
changedFiles: patch.entries.length,
|
|
4077
|
+
io: { ...ioMetrics }
|
|
3465
4078
|
};
|
|
3466
4079
|
},
|
|
3467
4080
|
runCommand,
|
|
@@ -3486,7 +4099,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3486
4099
|
return runCommand("bun", ["--no-env-file", "run", check], context);
|
|
3487
4100
|
},
|
|
3488
4101
|
async inspectPatch() {
|
|
3489
|
-
const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes);
|
|
4102
|
+
const patch = await createEnvironmentPatch(request.runId, base, workspace, options.config.maxFileWriteBytes, ioMetrics);
|
|
3490
4103
|
const payload = patchPayload(request.runId, patch.entries);
|
|
3491
4104
|
return {
|
|
3492
4105
|
...payload,
|
|
@@ -3495,7 +4108,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
3495
4108
|
};
|
|
3496
4109
|
},
|
|
3497
4110
|
importPatch(host, patchId) {
|
|
3498
|
-
return importPatch(request.runId, patchId, host, base, workspace, options.config.maxFileWriteBytes);
|
|
4111
|
+
return importPatch(request.runId, patchId, host, base, workspace, options.config.maxFileWriteBytes, ioMetrics);
|
|
3499
4112
|
},
|
|
3500
4113
|
async release(result) {
|
|
3501
4114
|
await runtime.removeRunContainers(request.runId);
|
|
@@ -3559,7 +4172,7 @@ var describeOciCommand = (command, args) => commandDisplay([command, ...args]);
|
|
|
3559
4172
|
|
|
3560
4173
|
// src/harness.ts
|
|
3561
4174
|
var APPROVAL_VERSION = "2026-08-17-v5";
|
|
3562
|
-
var TOOL_CONTRACT_VERSION = "
|
|
4175
|
+
var TOOL_CONTRACT_VERSION = "workspace-batch-v2";
|
|
3563
4176
|
var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, providerTransportFingerprint, executionEnvironment) => ({
|
|
3564
4177
|
schemaVersion: 1,
|
|
3565
4178
|
id: "zhivex-harness",
|
|
@@ -3589,7 +4202,7 @@ var HARNESS_INSTRUCTIONS = `You are Zhivex Harness, a provider-portable coding a
|
|
|
3589
4202
|
|
|
3590
4203
|
Rules:
|
|
3591
4204
|
- Match the user's language.
|
|
3592
|
-
- Inspect the repository before proposing changes. Prefer
|
|
4205
|
+
- Inspect the repository before proposing changes. Prefer search_many and read_files for independent lookups, then use search_files or read_file when pagination or one focused read is clearer.
|
|
3593
4206
|
- Use only workspace-relative paths. Never request or expose secrets.
|
|
3594
4207
|
- Make the smallest coherent change that fully addresses the task.
|
|
3595
4208
|
- For every file edit, first read its digest and call propose_edits. Apply exactly that reviewed proposal with apply_patch.
|
|
@@ -3661,6 +4274,23 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
3661
4274
|
metadata: readOnlyMetadata,
|
|
3662
4275
|
execute: async ({ path: path8, startLine, endLine }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).readFile(path8, startLine, endLine))
|
|
3663
4276
|
}),
|
|
4277
|
+
read_files: tool({
|
|
4278
|
+
name: "read_files",
|
|
4279
|
+
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.",
|
|
4280
|
+
schema: z3.object({
|
|
4281
|
+
files: z3.array(z3.object({
|
|
4282
|
+
path: z3.string().min(1),
|
|
4283
|
+
startLine: z3.number().int().min(1).default(1),
|
|
4284
|
+
endLine: z3.number().int().min(1).optional()
|
|
4285
|
+
})).min(1).max(20)
|
|
4286
|
+
}),
|
|
4287
|
+
metadata: readOnlyMetadata,
|
|
4288
|
+
execute: async ({ files }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).readFiles(files.map(({ path: path8, startLine, endLine }) => ({
|
|
4289
|
+
path: path8,
|
|
4290
|
+
startLine,
|
|
4291
|
+
...endLine !== undefined ? { endLine } : {}
|
|
4292
|
+
}))))
|
|
4293
|
+
}),
|
|
3664
4294
|
search_files: tool({
|
|
3665
4295
|
name: "search_files",
|
|
3666
4296
|
description: "Search for a literal string in text files using a stable cursor.",
|
|
@@ -3678,6 +4308,28 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
3678
4308
|
...cursor ? { cursor } : {}
|
|
3679
4309
|
}))
|
|
3680
4310
|
}),
|
|
4311
|
+
search_many: tool({
|
|
4312
|
+
name: "search_many",
|
|
4313
|
+
description: "Search up to 10 independent literal queries in one workspace pass, with at most 500 aggregate matches.",
|
|
4314
|
+
schema: z3.object({
|
|
4315
|
+
queries: z3.array(z3.object({
|
|
4316
|
+
query: z3.string().min(1).max(200),
|
|
4317
|
+
caseSensitive: z3.boolean().default(false)
|
|
4318
|
+
})).min(1).max(10),
|
|
4319
|
+
path: z3.string().min(1).default("."),
|
|
4320
|
+
limitPerQuery: z3.number().int().min(1).max(500).default(50)
|
|
4321
|
+
}).superRefine((input, context) => {
|
|
4322
|
+
if (input.queries.length * input.limitPerQuery > 500) {
|
|
4323
|
+
context.addIssue({
|
|
4324
|
+
code: "custom",
|
|
4325
|
+
path: ["limitPerQuery"],
|
|
4326
|
+
message: "search_many allows at most 500 aggregate matches."
|
|
4327
|
+
});
|
|
4328
|
+
}
|
|
4329
|
+
}),
|
|
4330
|
+
metadata: readOnlyMetadata,
|
|
4331
|
+
execute: async ({ queries, path: path8, limitPerQuery }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).searchMany(queries, path8, { limitPerQuery }))
|
|
4332
|
+
}),
|
|
3681
4333
|
propose_edits: tool({
|
|
3682
4334
|
name: "propose_edits",
|
|
3683
4335
|
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.",
|
|
@@ -4901,6 +5553,512 @@ var openCliSessionStore = async (options) => {
|
|
|
4901
5553
|
};
|
|
4902
5554
|
var openSessionStore = openCliSessionStore;
|
|
4903
5555
|
|
|
5556
|
+
// src/change-envelope.ts
|
|
5557
|
+
import { createHash as createHash9 } from "crypto";
|
|
5558
|
+
import { z as z4 } from "zod";
|
|
5559
|
+
var CHANGE_ENVELOPE_SCHEMA_VERSION = 1;
|
|
5560
|
+
var CHANGE_ENVELOPE_DIGEST_ALGORITHM = "sha256";
|
|
5561
|
+
var MAX_CHANGE_ENVELOPE_CHECKS = 100;
|
|
5562
|
+
var MAX_CHANGE_ENVELOPE_APPROVALS = 100;
|
|
5563
|
+
var MAX_CHANGE_ENVELOPE_APPROVAL_SCOPES = 32;
|
|
5564
|
+
var MAX_CHANGE_ENVELOPE_ATTESTATIONS = 32;
|
|
5565
|
+
var timestampSchema = z4.iso.datetime({ precision: 3 });
|
|
5566
|
+
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.");
|
|
5567
|
+
var approvalScopeSchema = z4.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._:/*-]*$/, "Approval scope must contain only bounded token characters.");
|
|
5568
|
+
var attestationFormatSchema = z4.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9.+_-]*$/);
|
|
5569
|
+
var mediaTypeSchema = z4.string().min(3).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/);
|
|
5570
|
+
var addDuplicateIssues = (values, key, context, path9) => {
|
|
5571
|
+
const seen = new Set;
|
|
5572
|
+
for (let index = 0;index < values.length; index += 1) {
|
|
5573
|
+
const value = values[index];
|
|
5574
|
+
if (value === undefined)
|
|
5575
|
+
continue;
|
|
5576
|
+
const identifier2 = key(value);
|
|
5577
|
+
if (seen.has(identifier2)) {
|
|
5578
|
+
context.addIssue({
|
|
5579
|
+
code: "custom",
|
|
5580
|
+
path: [index, path9],
|
|
5581
|
+
message: `Duplicate ${path9}: ${identifier2}.`
|
|
5582
|
+
});
|
|
5583
|
+
}
|
|
5584
|
+
seen.add(identifier2);
|
|
5585
|
+
}
|
|
5586
|
+
};
|
|
5587
|
+
var addCanonicalOrderIssue = (values, key, context, path9) => {
|
|
5588
|
+
for (let index = 1;index < values.length; index += 1) {
|
|
5589
|
+
const previous = values[index - 1];
|
|
5590
|
+
const current = values[index];
|
|
5591
|
+
if (previous !== undefined && current !== undefined && key(previous) >= key(current)) {
|
|
5592
|
+
context.addIssue({
|
|
5593
|
+
code: "custom",
|
|
5594
|
+
path: [index, path9],
|
|
5595
|
+
message: `${path9} entries must be unique and in canonical ascending order.`
|
|
5596
|
+
});
|
|
5597
|
+
}
|
|
5598
|
+
}
|
|
5599
|
+
};
|
|
5600
|
+
var approvalScopesInputSchema = z4.array(approvalScopeSchema).min(1).max(MAX_CHANGE_ENVELOPE_APPROVAL_SCOPES).superRefine((scopes, context) => addDuplicateIssues(scopes, (scope) => scope, context, "scope"));
|
|
5601
|
+
var canonicalApprovalScopesSchema = z4.array(approvalScopeSchema).min(1).max(MAX_CHANGE_ENVELOPE_APPROVAL_SCOPES).superRefine((scopes, context) => addCanonicalOrderIssue(scopes, (scope) => scope, context, "scope"));
|
|
5602
|
+
var changeEnvelopeCheckSchema = z4.strictObject({
|
|
5603
|
+
checkId: boundedIdentifierSchema,
|
|
5604
|
+
status: z4.enum(["passed", "failed", "skipped"]),
|
|
5605
|
+
redacted: z4.literal(true),
|
|
5606
|
+
startedAt: timestampSchema,
|
|
5607
|
+
completedAt: timestampSchema,
|
|
5608
|
+
durationMs: z4.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
5609
|
+
exitCode: z4.number().int().min(0).max(255).optional(),
|
|
5610
|
+
redactedOutputDigest: fileDigestSchema.optional(),
|
|
5611
|
+
runnerFingerprint: fileDigestSchema.optional()
|
|
5612
|
+
}).superRefine((check, context) => {
|
|
5613
|
+
const startedAt = Date.parse(check.startedAt);
|
|
5614
|
+
const completedAt = Date.parse(check.completedAt);
|
|
5615
|
+
if (completedAt < startedAt) {
|
|
5616
|
+
context.addIssue({
|
|
5617
|
+
code: "custom",
|
|
5618
|
+
path: ["completedAt"],
|
|
5619
|
+
message: "Check completion cannot precede its start."
|
|
5620
|
+
});
|
|
5621
|
+
}
|
|
5622
|
+
if (check.status === "passed" && check.exitCode !== undefined && check.exitCode !== 0) {
|
|
5623
|
+
context.addIssue({
|
|
5624
|
+
code: "custom",
|
|
5625
|
+
path: ["exitCode"],
|
|
5626
|
+
message: "A passed check cannot have a non-zero exit code."
|
|
5627
|
+
});
|
|
5628
|
+
}
|
|
5629
|
+
if (check.status === "failed" && check.exitCode === 0) {
|
|
5630
|
+
context.addIssue({
|
|
5631
|
+
code: "custom",
|
|
5632
|
+
path: ["exitCode"],
|
|
5633
|
+
message: "A failed check cannot have a zero exit code."
|
|
5634
|
+
});
|
|
5635
|
+
}
|
|
5636
|
+
if (check.status === "skipped" && check.exitCode !== undefined) {
|
|
5637
|
+
context.addIssue({
|
|
5638
|
+
code: "custom",
|
|
5639
|
+
path: ["exitCode"],
|
|
5640
|
+
message: "A skipped check cannot have an exit code."
|
|
5641
|
+
});
|
|
5642
|
+
}
|
|
5643
|
+
});
|
|
5644
|
+
var checkInputListSchema = z4.array(changeEnvelopeCheckSchema).min(1).max(MAX_CHANGE_ENVELOPE_CHECKS).superRefine((checks, context) => addDuplicateIssues(checks, (check) => check.checkId, context, "checkId"));
|
|
5645
|
+
var canonicalCheckListSchema = z4.array(changeEnvelopeCheckSchema).min(1).max(MAX_CHANGE_ENVELOPE_CHECKS).superRefine((checks, context) => addCanonicalOrderIssue(checks, (check) => check.checkId, context, "checkId"));
|
|
5646
|
+
var changeEnvelopeApprovalSchema = z4.strictObject({
|
|
5647
|
+
kind: z4.literal("approval-reference"),
|
|
5648
|
+
approvalId: boundedIdentifierSchema,
|
|
5649
|
+
approvalDigest: fileDigestSchema,
|
|
5650
|
+
approverDigest: fileDigestSchema,
|
|
5651
|
+
scopes: canonicalApprovalScopesSchema,
|
|
5652
|
+
approvedAt: timestampSchema,
|
|
5653
|
+
expiresAt: timestampSchema.optional()
|
|
5654
|
+
}).superRefine((approval, context) => {
|
|
5655
|
+
if (approval.expiresAt && Date.parse(approval.expiresAt) <= Date.parse(approval.approvedAt)) {
|
|
5656
|
+
context.addIssue({
|
|
5657
|
+
code: "custom",
|
|
5658
|
+
path: ["expiresAt"],
|
|
5659
|
+
message: "Approval expiry must be later than approval time."
|
|
5660
|
+
});
|
|
5661
|
+
}
|
|
5662
|
+
});
|
|
5663
|
+
var changeEnvelopeApprovalInputSchema = z4.strictObject({
|
|
5664
|
+
kind: z4.literal("approval-reference"),
|
|
5665
|
+
approvalId: boundedIdentifierSchema,
|
|
5666
|
+
approvalDigest: fileDigestSchema,
|
|
5667
|
+
approverDigest: fileDigestSchema,
|
|
5668
|
+
scopes: approvalScopesInputSchema,
|
|
5669
|
+
approvedAt: timestampSchema,
|
|
5670
|
+
expiresAt: timestampSchema.optional()
|
|
5671
|
+
}).superRefine((approval, context) => {
|
|
5672
|
+
if (approval.expiresAt && Date.parse(approval.expiresAt) <= Date.parse(approval.approvedAt)) {
|
|
5673
|
+
context.addIssue({
|
|
5674
|
+
code: "custom",
|
|
5675
|
+
path: ["expiresAt"],
|
|
5676
|
+
message: "Approval expiry must be later than approval time."
|
|
5677
|
+
});
|
|
5678
|
+
}
|
|
5679
|
+
});
|
|
5680
|
+
var approvalInputListSchema = z4.array(changeEnvelopeApprovalInputSchema).min(1).max(MAX_CHANGE_ENVELOPE_APPROVALS).superRefine((approvals, context) => addDuplicateIssues(approvals, (approval) => approval.approvalId, context, "approvalId"));
|
|
5681
|
+
var canonicalApprovalListSchema = z4.array(changeEnvelopeApprovalSchema).min(1).max(MAX_CHANGE_ENVELOPE_APPROVALS).superRefine((approvals, context) => addCanonicalOrderIssue(approvals, (approval) => approval.approvalId, context, "approvalId"));
|
|
5682
|
+
var externalAttestationReferenceSchema = z4.strictObject({
|
|
5683
|
+
kind: z4.literal("external-attestation-reference"),
|
|
5684
|
+
attestationId: boundedIdentifierSchema,
|
|
5685
|
+
format: attestationFormatSchema,
|
|
5686
|
+
mediaType: mediaTypeSchema,
|
|
5687
|
+
bundleDigest: fileDigestSchema
|
|
5688
|
+
});
|
|
5689
|
+
var externalAttestationInputListSchema = z4.array(externalAttestationReferenceSchema).min(1).max(MAX_CHANGE_ENVELOPE_ATTESTATIONS).superRefine((attestations, context) => addDuplicateIssues(attestations, (attestation) => attestation.attestationId, context, "attestationId"));
|
|
5690
|
+
var canonicalExternalAttestationListSchema = z4.array(externalAttestationReferenceSchema).min(1).max(MAX_CHANGE_ENVELOPE_ATTESTATIONS).superRefine((attestations, context) => addCanonicalOrderIssue(attestations, (attestation) => attestation.attestationId, context, "attestationId"));
|
|
5691
|
+
var changeEnvelopeBaseSchema = z4.strictObject({
|
|
5692
|
+
workspaceDigest: fileDigestSchema,
|
|
5693
|
+
treeDigest: fileDigestSchema
|
|
5694
|
+
});
|
|
5695
|
+
var changeEnvelopePatchSchema = z4.strictObject({
|
|
5696
|
+
patchId: boundedIdentifierSchema,
|
|
5697
|
+
patchDigest: fileDigestSchema
|
|
5698
|
+
});
|
|
5699
|
+
var changeEnvelopeFingerprintsSchema = z4.strictObject({
|
|
5700
|
+
harness: fileDigestSchema,
|
|
5701
|
+
policy: fileDigestSchema,
|
|
5702
|
+
environment: fileDigestSchema
|
|
5703
|
+
});
|
|
5704
|
+
var changeEnvelopeEvidenceSchema = z4.strictObject({
|
|
5705
|
+
schemaVersion: z4.literal(CHANGE_ENVELOPE_SCHEMA_VERSION),
|
|
5706
|
+
kind: z4.literal("check-evidence-summary"),
|
|
5707
|
+
digestAlgorithm: z4.literal(CHANGE_ENVELOPE_DIGEST_ALGORITHM),
|
|
5708
|
+
evidenceDigest: fileDigestSchema,
|
|
5709
|
+
checks: canonicalCheckListSchema
|
|
5710
|
+
});
|
|
5711
|
+
var changeEnvelopeInputSchema = z4.strictObject({
|
|
5712
|
+
createdAt: timestampSchema,
|
|
5713
|
+
expiresAt: timestampSchema.optional(),
|
|
5714
|
+
base: changeEnvelopeBaseSchema,
|
|
5715
|
+
patch: changeEnvelopePatchSchema,
|
|
5716
|
+
fingerprints: changeEnvelopeFingerprintsSchema,
|
|
5717
|
+
checks: checkInputListSchema,
|
|
5718
|
+
approvals: approvalInputListSchema.optional(),
|
|
5719
|
+
externalAttestations: externalAttestationInputListSchema.optional()
|
|
5720
|
+
});
|
|
5721
|
+
var changeEnvelopeSchema = z4.strictObject({
|
|
5722
|
+
schemaVersion: z4.literal(CHANGE_ENVELOPE_SCHEMA_VERSION),
|
|
5723
|
+
kind: z4.literal("change-envelope"),
|
|
5724
|
+
digestAlgorithm: z4.literal(CHANGE_ENVELOPE_DIGEST_ALGORITHM),
|
|
5725
|
+
envelopeId: fileDigestSchema,
|
|
5726
|
+
createdAt: timestampSchema,
|
|
5727
|
+
expiresAt: timestampSchema.optional(),
|
|
5728
|
+
base: changeEnvelopeBaseSchema,
|
|
5729
|
+
patch: changeEnvelopePatchSchema,
|
|
5730
|
+
fingerprints: changeEnvelopeFingerprintsSchema,
|
|
5731
|
+
evidence: changeEnvelopeEvidenceSchema,
|
|
5732
|
+
approvals: canonicalApprovalListSchema.optional(),
|
|
5733
|
+
externalAttestations: canonicalExternalAttestationListSchema.optional()
|
|
5734
|
+
}).superRefine((envelope, context) => {
|
|
5735
|
+
if (envelope.expiresAt && Date.parse(envelope.expiresAt) <= Date.parse(envelope.createdAt)) {
|
|
5736
|
+
context.addIssue({
|
|
5737
|
+
code: "custom",
|
|
5738
|
+
path: ["expiresAt"],
|
|
5739
|
+
message: "Envelope expiry must be later than creation time."
|
|
5740
|
+
});
|
|
5741
|
+
}
|
|
5742
|
+
for (let index = 0;index < envelope.evidence.checks.length; index += 1) {
|
|
5743
|
+
const check = envelope.evidence.checks[index];
|
|
5744
|
+
if (check && Date.parse(check.completedAt) > Date.parse(envelope.createdAt)) {
|
|
5745
|
+
context.addIssue({
|
|
5746
|
+
code: "custom",
|
|
5747
|
+
path: ["evidence", "checks", index, "completedAt"],
|
|
5748
|
+
message: "Check completion cannot be later than envelope creation time."
|
|
5749
|
+
});
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
for (let index = 0;index < (envelope.approvals?.length ?? 0); index += 1) {
|
|
5753
|
+
const approval = envelope.approvals?.[index];
|
|
5754
|
+
if (approval && Date.parse(approval.approvedAt) > Date.parse(envelope.createdAt)) {
|
|
5755
|
+
context.addIssue({
|
|
5756
|
+
code: "custom",
|
|
5757
|
+
path: ["approvals", index, "approvedAt"],
|
|
5758
|
+
message: "Approval time cannot be later than envelope creation time."
|
|
5759
|
+
});
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
});
|
|
5763
|
+
var canonicalJson = (value) => {
|
|
5764
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
5765
|
+
return JSON.stringify(value);
|
|
5766
|
+
}
|
|
5767
|
+
if (typeof value === "number") {
|
|
5768
|
+
if (!Number.isFinite(value))
|
|
5769
|
+
throw new TypeError("Canonical JSON cannot encode a non-finite number.");
|
|
5770
|
+
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
5771
|
+
}
|
|
5772
|
+
if (Array.isArray(value)) {
|
|
5773
|
+
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
5774
|
+
}
|
|
5775
|
+
if (typeof value === "object") {
|
|
5776
|
+
const record = value;
|
|
5777
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
|
|
5778
|
+
}
|
|
5779
|
+
throw new TypeError(`Canonical JSON cannot encode ${typeof value}.`);
|
|
5780
|
+
};
|
|
5781
|
+
var digestCanonical = (value) => `sha256:${createHash9("sha256").update(canonicalJson(value)).digest("hex")}`;
|
|
5782
|
+
var digestChangeEnvelopeArtifact = (value) => `sha256:${createHash9("sha256").update(value).digest("hex")}`;
|
|
5783
|
+
var evidencePayload = (checks) => ({
|
|
5784
|
+
schemaVersion: CHANGE_ENVELOPE_SCHEMA_VERSION,
|
|
5785
|
+
kind: "check-evidence-summary",
|
|
5786
|
+
digestAlgorithm: CHANGE_ENVELOPE_DIGEST_ALGORITHM,
|
|
5787
|
+
checks: [...checks]
|
|
5788
|
+
});
|
|
5789
|
+
var envelopePayload = (envelope) => {
|
|
5790
|
+
const { envelopeId: _envelopeId, ...payload } = envelope;
|
|
5791
|
+
return payload;
|
|
5792
|
+
};
|
|
5793
|
+
var sortBy = (values, key) => [...values].sort((left, right) => {
|
|
5794
|
+
const leftKey = key(left);
|
|
5795
|
+
const rightKey = key(right);
|
|
5796
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
5797
|
+
});
|
|
5798
|
+
var createChangeEnvelope = (input) => {
|
|
5799
|
+
const parsed = changeEnvelopeInputSchema.parse(input);
|
|
5800
|
+
const checks = sortBy(parsed.checks, (check) => check.checkId);
|
|
5801
|
+
const evidenceWithoutDigest = evidencePayload(checks);
|
|
5802
|
+
const evidence = {
|
|
5803
|
+
...evidenceWithoutDigest,
|
|
5804
|
+
evidenceDigest: digestCanonical(evidenceWithoutDigest)
|
|
5805
|
+
};
|
|
5806
|
+
const approvals = parsed.approvals ? sortBy(parsed.approvals, (approval) => approval.approvalId).map((approval) => ({
|
|
5807
|
+
...approval,
|
|
5808
|
+
scopes: [...approval.scopes].sort()
|
|
5809
|
+
})) : undefined;
|
|
5810
|
+
const externalAttestations = parsed.externalAttestations ? sortBy(parsed.externalAttestations, (attestation) => attestation.attestationId) : undefined;
|
|
5811
|
+
const payload = {
|
|
5812
|
+
schemaVersion: CHANGE_ENVELOPE_SCHEMA_VERSION,
|
|
5813
|
+
kind: "change-envelope",
|
|
5814
|
+
digestAlgorithm: CHANGE_ENVELOPE_DIGEST_ALGORITHM,
|
|
5815
|
+
createdAt: parsed.createdAt,
|
|
5816
|
+
...parsed.expiresAt ? { expiresAt: parsed.expiresAt } : {},
|
|
5817
|
+
base: parsed.base,
|
|
5818
|
+
patch: parsed.patch,
|
|
5819
|
+
fingerprints: parsed.fingerprints,
|
|
5820
|
+
evidence,
|
|
5821
|
+
...approvals ? { approvals } : {},
|
|
5822
|
+
...externalAttestations ? { externalAttestations } : {}
|
|
5823
|
+
};
|
|
5824
|
+
return changeEnvelopeSchema.parse({
|
|
5825
|
+
...payload,
|
|
5826
|
+
envelopeId: digestCanonical(payload)
|
|
5827
|
+
});
|
|
5828
|
+
};
|
|
5829
|
+
var canonicalizeChangeEnvelope = (input) => canonicalJson(changeEnvelopeSchema.parse(input));
|
|
5830
|
+
var computeChangeEnvelopeDigest = (input) => {
|
|
5831
|
+
const parsed = changeEnvelopeSchema.parse(input);
|
|
5832
|
+
return digestCanonical(envelopePayload(parsed));
|
|
5833
|
+
};
|
|
5834
|
+
var computeChangeEnvelopeEvidenceDigest = (input) => {
|
|
5835
|
+
const parsed = changeEnvelopeEvidenceSchema.parse(input);
|
|
5836
|
+
return digestCanonical(evidencePayload(parsed.checks));
|
|
5837
|
+
};
|
|
5838
|
+
var changeEnvelopePreconditionsSchema = z4.strictObject({
|
|
5839
|
+
envelopeId: fileDigestSchema.optional(),
|
|
5840
|
+
baseWorkspaceDigest: fileDigestSchema.optional(),
|
|
5841
|
+
baseTreeDigest: fileDigestSchema.optional(),
|
|
5842
|
+
patchId: boundedIdentifierSchema.optional(),
|
|
5843
|
+
patchDigest: fileDigestSchema.optional(),
|
|
5844
|
+
harnessFingerprint: fileDigestSchema.optional(),
|
|
5845
|
+
policyFingerprint: fileDigestSchema.optional(),
|
|
5846
|
+
environmentFingerprint: fileDigestSchema.optional(),
|
|
5847
|
+
evidenceDigest: fileDigestSchema.optional(),
|
|
5848
|
+
requiredPassedChecks: z4.array(boundedIdentifierSchema).max(MAX_CHANGE_ENVELOPE_CHECKS).optional(),
|
|
5849
|
+
requiredApprovalDigests: z4.array(fileDigestSchema).max(MAX_CHANGE_ENVELOPE_APPROVALS).optional(),
|
|
5850
|
+
requiredApprovalScopes: z4.array(approvalScopeSchema).max(MAX_CHANGE_ENVELOPE_APPROVAL_SCOPES).optional(),
|
|
5851
|
+
requiredAttestationDigests: z4.array(fileDigestSchema).max(MAX_CHANGE_ENVELOPE_ATTESTATIONS).optional()
|
|
5852
|
+
}).superRefine((preconditions, context) => {
|
|
5853
|
+
if (preconditions.requiredPassedChecks) {
|
|
5854
|
+
addDuplicateIssues(preconditions.requiredPassedChecks, (checkId) => checkId, context, "requiredPassedCheck");
|
|
5855
|
+
}
|
|
5856
|
+
if (preconditions.requiredApprovalDigests) {
|
|
5857
|
+
addDuplicateIssues(preconditions.requiredApprovalDigests, (digest3) => digest3, context, "requiredApprovalDigest");
|
|
5858
|
+
}
|
|
5859
|
+
if (preconditions.requiredApprovalScopes) {
|
|
5860
|
+
addDuplicateIssues(preconditions.requiredApprovalScopes, (scope) => scope, context, "requiredApprovalScope");
|
|
5861
|
+
}
|
|
5862
|
+
if (preconditions.requiredAttestationDigests) {
|
|
5863
|
+
addDuplicateIssues(preconditions.requiredAttestationDigests, (digest3) => digest3, context, "requiredAttestationDigest");
|
|
5864
|
+
}
|
|
5865
|
+
});
|
|
5866
|
+
var verificationOptionsSchema = z4.strictObject({
|
|
5867
|
+
now: z4.union([z4.date(), z4.number().int(), timestampSchema]).optional(),
|
|
5868
|
+
preconditions: changeEnvelopePreconditionsSchema.optional()
|
|
5869
|
+
});
|
|
5870
|
+
var verificationTime = (value) => {
|
|
5871
|
+
const date = value === undefined ? new Date : value instanceof Date ? value : new Date(value);
|
|
5872
|
+
if (!Number.isFinite(date.getTime()))
|
|
5873
|
+
throw new TypeError("Verification time must be a valid date.");
|
|
5874
|
+
return date;
|
|
5875
|
+
};
|
|
5876
|
+
var verifyChangeEnvelope = (input, options = {}) => {
|
|
5877
|
+
const parsedOptions = verificationOptionsSchema.parse(options);
|
|
5878
|
+
const now = verificationTime(parsedOptions.now);
|
|
5879
|
+
const checkedAt = now.toISOString();
|
|
5880
|
+
const parsedEnvelope = changeEnvelopeSchema.safeParse(input);
|
|
5881
|
+
if (!parsedEnvelope.success) {
|
|
5882
|
+
return {
|
|
5883
|
+
valid: false,
|
|
5884
|
+
verificationScope: "integrity-expiration-and-preconditions-only",
|
|
5885
|
+
issues: parsedEnvelope.error.issues.map((issue) => ({
|
|
5886
|
+
code: "invalid-schema",
|
|
5887
|
+
path: issue.path.map((segment) => typeof segment === "symbol" ? segment.description ?? "symbol" : segment),
|
|
5888
|
+
message: issue.message
|
|
5889
|
+
})),
|
|
5890
|
+
integrity: { envelopeDigestValid: false, evidenceDigestValid: false },
|
|
5891
|
+
expiration: { checkedAt, envelopeNotYetValid: false, envelopeExpired: false, expiredApprovalIds: [] },
|
|
5892
|
+
checks: {
|
|
5893
|
+
recorded: 0,
|
|
5894
|
+
passed: [],
|
|
5895
|
+
failed: [],
|
|
5896
|
+
skipped: [],
|
|
5897
|
+
requiredPassed: [...parsedOptions.preconditions?.requiredPassedChecks ?? []].sort(),
|
|
5898
|
+
missingPassed: [...parsedOptions.preconditions?.requiredPassedChecks ?? []].sort()
|
|
5899
|
+
},
|
|
5900
|
+
approvals: {
|
|
5901
|
+
recorded: 0,
|
|
5902
|
+
requiredScopes: [...parsedOptions.preconditions?.requiredApprovalScopes ?? []].sort(),
|
|
5903
|
+
missingScopes: [...parsedOptions.preconditions?.requiredApprovalScopes ?? []].sort(),
|
|
5904
|
+
authenticity: "not-verified"
|
|
5905
|
+
},
|
|
5906
|
+
externalAttestations: {
|
|
5907
|
+
referenced: 0,
|
|
5908
|
+
requiredDigests: [...parsedOptions.preconditions?.requiredAttestationDigests ?? []].sort(),
|
|
5909
|
+
missingDigests: [...parsedOptions.preconditions?.requiredAttestationDigests ?? []].sort(),
|
|
5910
|
+
authenticity: "not-verified"
|
|
5911
|
+
}
|
|
5912
|
+
};
|
|
5913
|
+
}
|
|
5914
|
+
const envelope = parsedEnvelope.data;
|
|
5915
|
+
const issues = [];
|
|
5916
|
+
const computedEvidenceDigest = digestCanonical(evidencePayload(envelope.evidence.checks));
|
|
5917
|
+
const evidenceDigestValid = computedEvidenceDigest === envelope.evidence.evidenceDigest;
|
|
5918
|
+
if (!evidenceDigestValid) {
|
|
5919
|
+
issues.push({
|
|
5920
|
+
code: "evidence-digest-mismatch",
|
|
5921
|
+
path: ["evidence", "evidenceDigest"],
|
|
5922
|
+
message: "Evidence digest does not match the canonical redacted check summary.",
|
|
5923
|
+
expected: computedEvidenceDigest,
|
|
5924
|
+
actual: envelope.evidence.evidenceDigest
|
|
5925
|
+
});
|
|
5926
|
+
}
|
|
5927
|
+
const computedEnvelopeDigest = digestCanonical(envelopePayload(envelope));
|
|
5928
|
+
const envelopeDigestValid = computedEnvelopeDigest === envelope.envelopeId;
|
|
5929
|
+
if (!envelopeDigestValid) {
|
|
5930
|
+
issues.push({
|
|
5931
|
+
code: "envelope-digest-mismatch",
|
|
5932
|
+
path: ["envelopeId"],
|
|
5933
|
+
message: "Envelope digest does not match its canonical payload.",
|
|
5934
|
+
expected: computedEnvelopeDigest,
|
|
5935
|
+
actual: envelope.envelopeId
|
|
5936
|
+
});
|
|
5937
|
+
}
|
|
5938
|
+
const envelopeNotYetValid = now.getTime() < Date.parse(envelope.createdAt);
|
|
5939
|
+
if (envelopeNotYetValid) {
|
|
5940
|
+
issues.push({
|
|
5941
|
+
code: "envelope-not-yet-valid",
|
|
5942
|
+
path: ["createdAt"],
|
|
5943
|
+
message: "Change envelope creation time is in the future."
|
|
5944
|
+
});
|
|
5945
|
+
}
|
|
5946
|
+
const envelopeExpired = envelope.expiresAt !== undefined && now.getTime() >= Date.parse(envelope.expiresAt);
|
|
5947
|
+
if (envelopeExpired) {
|
|
5948
|
+
issues.push({
|
|
5949
|
+
code: "envelope-expired",
|
|
5950
|
+
path: ["expiresAt"],
|
|
5951
|
+
message: "Change envelope has expired."
|
|
5952
|
+
});
|
|
5953
|
+
}
|
|
5954
|
+
const expiredApprovalIds = (envelope.approvals ?? []).filter((approval) => approval.expiresAt !== undefined && now.getTime() >= Date.parse(approval.expiresAt)).map((approval) => approval.approvalId);
|
|
5955
|
+
for (const approvalId of expiredApprovalIds) {
|
|
5956
|
+
issues.push({
|
|
5957
|
+
code: "approval-expired",
|
|
5958
|
+
path: ["approvals", envelope.approvals?.findIndex((approval) => approval.approvalId === approvalId) ?? 0, "expiresAt"],
|
|
5959
|
+
message: `Recorded approval has expired: ${approvalId}.`
|
|
5960
|
+
});
|
|
5961
|
+
}
|
|
5962
|
+
const preconditions = parsedOptions.preconditions;
|
|
5963
|
+
const comparePrecondition = (field, expected, actual, path9) => {
|
|
5964
|
+
if (expected !== undefined && expected !== actual) {
|
|
5965
|
+
issues.push({
|
|
5966
|
+
code: "precondition-mismatch",
|
|
5967
|
+
path: path9,
|
|
5968
|
+
message: `Change envelope precondition does not match: ${field}.`,
|
|
5969
|
+
expected,
|
|
5970
|
+
actual
|
|
5971
|
+
});
|
|
5972
|
+
}
|
|
5973
|
+
};
|
|
5974
|
+
comparePrecondition("envelopeId", preconditions?.envelopeId, envelope.envelopeId, ["envelopeId"]);
|
|
5975
|
+
comparePrecondition("baseWorkspaceDigest", preconditions?.baseWorkspaceDigest, envelope.base.workspaceDigest, ["base", "workspaceDigest"]);
|
|
5976
|
+
comparePrecondition("baseTreeDigest", preconditions?.baseTreeDigest, envelope.base.treeDigest, ["base", "treeDigest"]);
|
|
5977
|
+
comparePrecondition("patchId", preconditions?.patchId, envelope.patch.patchId, ["patch", "patchId"]);
|
|
5978
|
+
comparePrecondition("patchDigest", preconditions?.patchDigest, envelope.patch.patchDigest, ["patch", "patchDigest"]);
|
|
5979
|
+
comparePrecondition("harnessFingerprint", preconditions?.harnessFingerprint, envelope.fingerprints.harness, ["fingerprints", "harness"]);
|
|
5980
|
+
comparePrecondition("policyFingerprint", preconditions?.policyFingerprint, envelope.fingerprints.policy, ["fingerprints", "policy"]);
|
|
5981
|
+
comparePrecondition("environmentFingerprint", preconditions?.environmentFingerprint, envelope.fingerprints.environment, ["fingerprints", "environment"]);
|
|
5982
|
+
comparePrecondition("evidenceDigest", preconditions?.evidenceDigest, envelope.evidence.evidenceDigest, ["evidence", "evidenceDigest"]);
|
|
5983
|
+
const passedChecks = envelope.evidence.checks.filter((check) => check.status === "passed").map((check) => check.checkId);
|
|
5984
|
+
const failedChecks = envelope.evidence.checks.filter((check) => check.status === "failed").map((check) => check.checkId);
|
|
5985
|
+
const skippedChecks = envelope.evidence.checks.filter((check) => check.status === "skipped").map((check) => check.checkId);
|
|
5986
|
+
const passedCheckSet = new Set(passedChecks);
|
|
5987
|
+
const requiredPassed = [...preconditions?.requiredPassedChecks ?? []].sort();
|
|
5988
|
+
const missingPassed = requiredPassed.filter((checkId) => !passedCheckSet.has(checkId));
|
|
5989
|
+
for (const checkId of missingPassed) {
|
|
5990
|
+
issues.push({
|
|
5991
|
+
code: "missing-passed-check",
|
|
5992
|
+
path: ["evidence", "checks"],
|
|
5993
|
+
message: `Required passed check is not recorded: ${checkId}.`,
|
|
5994
|
+
expected: checkId
|
|
5995
|
+
});
|
|
5996
|
+
}
|
|
5997
|
+
const expiredApprovals = new Set(expiredApprovalIds);
|
|
5998
|
+
const activeApprovals = (envelope.approvals ?? []).filter((approval) => !expiredApprovals.has(approval.approvalId));
|
|
5999
|
+
const approvalDigests = new Set(activeApprovals.map((approval) => approval.approvalDigest));
|
|
6000
|
+
for (const requiredDigest of preconditions?.requiredApprovalDigests ?? []) {
|
|
6001
|
+
if (!approvalDigests.has(requiredDigest)) {
|
|
6002
|
+
issues.push({
|
|
6003
|
+
code: "missing-approval-digest",
|
|
6004
|
+
path: ["approvals"],
|
|
6005
|
+
message: `Required active approval digest is not recorded: ${requiredDigest}.`,
|
|
6006
|
+
expected: requiredDigest
|
|
6007
|
+
});
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
const approvedScopes = new Set(activeApprovals.flatMap((approval) => approval.scopes));
|
|
6011
|
+
const requiredScopes = [...preconditions?.requiredApprovalScopes ?? []].sort();
|
|
6012
|
+
const missingScopes = requiredScopes.filter((scope) => !approvedScopes.has(scope));
|
|
6013
|
+
for (const scope of missingScopes) {
|
|
6014
|
+
issues.push({
|
|
6015
|
+
code: "missing-approval-scope",
|
|
6016
|
+
path: ["approvals"],
|
|
6017
|
+
message: `Required active approval scope is not recorded: ${scope}.`,
|
|
6018
|
+
expected: scope
|
|
6019
|
+
});
|
|
6020
|
+
}
|
|
6021
|
+
const attestationDigests = new Set((envelope.externalAttestations ?? []).map((attestation) => attestation.bundleDigest));
|
|
6022
|
+
const requiredAttestationDigests = [...preconditions?.requiredAttestationDigests ?? []].sort();
|
|
6023
|
+
const missingAttestationDigests = requiredAttestationDigests.filter((digest3) => !attestationDigests.has(digest3));
|
|
6024
|
+
for (const requiredDigest of missingAttestationDigests) {
|
|
6025
|
+
issues.push({
|
|
6026
|
+
code: "missing-attestation-digest",
|
|
6027
|
+
path: ["externalAttestations"],
|
|
6028
|
+
message: `Required external attestation digest is not referenced: ${requiredDigest}.`,
|
|
6029
|
+
expected: requiredDigest
|
|
6030
|
+
});
|
|
6031
|
+
}
|
|
6032
|
+
return {
|
|
6033
|
+
valid: issues.length === 0,
|
|
6034
|
+
verificationScope: "integrity-expiration-and-preconditions-only",
|
|
6035
|
+
envelope,
|
|
6036
|
+
issues,
|
|
6037
|
+
integrity: { envelopeDigestValid, evidenceDigestValid },
|
|
6038
|
+
expiration: { checkedAt, envelopeNotYetValid, envelopeExpired, expiredApprovalIds },
|
|
6039
|
+
checks: {
|
|
6040
|
+
recorded: envelope.evidence.checks.length,
|
|
6041
|
+
passed: passedChecks,
|
|
6042
|
+
failed: failedChecks,
|
|
6043
|
+
skipped: skippedChecks,
|
|
6044
|
+
requiredPassed,
|
|
6045
|
+
missingPassed
|
|
6046
|
+
},
|
|
6047
|
+
approvals: {
|
|
6048
|
+
recorded: envelope.approvals?.length ?? 0,
|
|
6049
|
+
requiredScopes,
|
|
6050
|
+
missingScopes,
|
|
6051
|
+
authenticity: "not-verified"
|
|
6052
|
+
},
|
|
6053
|
+
externalAttestations: {
|
|
6054
|
+
referenced: envelope.externalAttestations?.length ?? 0,
|
|
6055
|
+
requiredDigests: requiredAttestationDigests,
|
|
6056
|
+
missingDigests: missingAttestationDigests,
|
|
6057
|
+
authenticity: "not-verified"
|
|
6058
|
+
}
|
|
6059
|
+
};
|
|
6060
|
+
};
|
|
6061
|
+
|
|
4904
6062
|
// src/cli.ts
|
|
4905
6063
|
var HARNESS_RESUME_METADATA_KEY = "zhivexHarnessResume";
|
|
4906
6064
|
var HARNESS_RESUME_METADATA_SCHEMA_VERSION = 1;
|
|
@@ -4910,9 +6068,11 @@ var CLI_EXIT_CODES = {
|
|
|
4910
6068
|
usageError: 2,
|
|
4911
6069
|
doctorFailed: 3
|
|
4912
6070
|
};
|
|
4913
|
-
var COMMANDS = new Set(["run", "review", "chat", "providers", "doctor", "resume", "runs", "sessions", "help", "version"]);
|
|
6071
|
+
var COMMANDS = new Set(["run", "review", "chat", "providers", "doctor", "resume", "runs", "sessions", "changes", "help", "version"]);
|
|
4914
6072
|
var RUNS_COMMANDS = new Set(["list", "inspect", "cancel", "cleanup", "export"]);
|
|
4915
6073
|
var SESSIONS_COMMANDS = new Set(["list", "inspect", "rename", "fork", "archive"]);
|
|
6074
|
+
var CHANGES_COMMANDS = new Set(["create", "verify"]);
|
|
6075
|
+
var CLI_TIMESTAMP_SCHEMA = z5.iso.datetime({ precision: 3 });
|
|
4916
6076
|
var RUN_STATUSES = new Set([
|
|
4917
6077
|
"queued",
|
|
4918
6078
|
"running",
|
|
@@ -5132,6 +6292,23 @@ var parseCliArgs = (argv) => {
|
|
|
5132
6292
|
options.mcpConfigPath = optionValue(argv, index, argument);
|
|
5133
6293
|
index += 1;
|
|
5134
6294
|
break;
|
|
6295
|
+
case "--patch":
|
|
6296
|
+
options.patchPath = optionValue(argv, index, argument);
|
|
6297
|
+
index += 1;
|
|
6298
|
+
break;
|
|
6299
|
+
case "--preconditions":
|
|
6300
|
+
options.preconditionsPath = optionValue(argv, index, argument);
|
|
6301
|
+
index += 1;
|
|
6302
|
+
break;
|
|
6303
|
+
case "--now": {
|
|
6304
|
+
const value = optionValue(argv, index, argument);
|
|
6305
|
+
if (!CLI_TIMESTAMP_SCHEMA.safeParse(value).success) {
|
|
6306
|
+
throw new CliUsageError("--now must be a millisecond-precision ISO-8601 UTC timestamp.");
|
|
6307
|
+
}
|
|
6308
|
+
options.verificationTime = value;
|
|
6309
|
+
index += 1;
|
|
6310
|
+
break;
|
|
6311
|
+
}
|
|
5135
6312
|
case "--execution":
|
|
5136
6313
|
options.executionBackend = optionValue(argv, index, argument);
|
|
5137
6314
|
index += 1;
|
|
@@ -5424,6 +6601,9 @@ var parseCliArgs = (argv) => {
|
|
|
5424
6601
|
if (options.jsonl && options.command !== "run" && options.command !== "resume") {
|
|
5425
6602
|
throw new CliUsageError("--jsonl is supported by run and resume.");
|
|
5426
6603
|
}
|
|
6604
|
+
if (options.command !== "changes" && (options.patchPath || options.preconditionsPath || options.verificationTime)) {
|
|
6605
|
+
throw new CliUsageError("--patch, --preconditions, and --now are supported only by changes.");
|
|
6606
|
+
}
|
|
5427
6607
|
if (options.command === "resume") {
|
|
5428
6608
|
const runId = positional.shift();
|
|
5429
6609
|
if (runId) {
|
|
@@ -5473,6 +6653,26 @@ var parseCliArgs = (argv) => {
|
|
|
5473
6653
|
if (positional.length > 0) {
|
|
5474
6654
|
throw new CliUsageError(`sessions ${sessionsCommand} received unexpected positional arguments.`);
|
|
5475
6655
|
}
|
|
6656
|
+
} else if (options.command === "changes") {
|
|
6657
|
+
const changesCommand = positional.shift();
|
|
6658
|
+
if (!changesCommand || !CHANGES_COMMANDS.has(changesCommand)) {
|
|
6659
|
+
throw new CliUsageError("changes requires one of: create, verify.");
|
|
6660
|
+
}
|
|
6661
|
+
options.changesCommand = changesCommand;
|
|
6662
|
+
const artifactPath = positional.shift();
|
|
6663
|
+
if (!artifactPath) {
|
|
6664
|
+
throw new CliUsageError(`changes ${changesCommand} requires an input JSON file.`);
|
|
6665
|
+
}
|
|
6666
|
+
options.artifactPath = artifactPath;
|
|
6667
|
+
if (!options.patchPath) {
|
|
6668
|
+
throw new CliUsageError(`changes ${changesCommand} requires --patch <file>.`);
|
|
6669
|
+
}
|
|
6670
|
+
if (changesCommand === "create" && (options.preconditionsPath || options.verificationTime)) {
|
|
6671
|
+
throw new CliUsageError("--preconditions and --now are only supported by changes verify.");
|
|
6672
|
+
}
|
|
6673
|
+
if (positional.length > 0) {
|
|
6674
|
+
throw new CliUsageError(`changes ${changesCommand} received unexpected positional arguments.`);
|
|
6675
|
+
}
|
|
5476
6676
|
} else if (options.command === "run" || options.command === "review") {
|
|
5477
6677
|
const prompt = positional.join(" ").trim();
|
|
5478
6678
|
if (prompt) {
|
|
@@ -5495,6 +6695,8 @@ Usage:
|
|
|
5495
6695
|
zhx resume [options] <runId> --approve|--deny
|
|
5496
6696
|
zhx runs list [--status <status>] [--limit <n>] [--json]
|
|
5497
6697
|
zhx sessions list|inspect|rename|fork|archive
|
|
6698
|
+
zhx changes create <input.json> --patch <artifact>
|
|
6699
|
+
zhx changes verify <envelope.json> --patch <artifact> [--preconditions <file>]
|
|
5498
6700
|
|
|
5499
6701
|
The long command zhivex-harness remains supported for compatibility.
|
|
5500
6702
|
|
|
@@ -5507,6 +6709,9 @@ Options:
|
|
|
5507
6709
|
--workspace <path> Target workspace (default: cwd)
|
|
5508
6710
|
--state-dir <path> Durable run-state directory
|
|
5509
6711
|
--mcp-config <path> Declarative governed MCP JSON configuration
|
|
6712
|
+
--patch <path> Exact patch/artifact bytes bound to a change envelope
|
|
6713
|
+
--preconditions <path> Verification preconditions JSON for changes verify
|
|
6714
|
+
--now <ISO-8601 UTC> Explicit millisecond verification time (default: current time)
|
|
5510
6715
|
--execution <none|oci> Enforced execution backend (default: none)
|
|
5511
6716
|
--oci-runtime <docker|podman> Local OCI runtime (default: docker)
|
|
5512
6717
|
--oci-image <reference> Preloaded immutable-capable OCI image
|
|
@@ -6305,7 +7510,7 @@ var inspectWorkspace = async (workspace) => {
|
|
|
6305
7510
|
if (!workspaceStat.isDirectory()) {
|
|
6306
7511
|
return diagnostic("workspace", "fail", "Workspace is not a directory.", { path: workspace });
|
|
6307
7512
|
}
|
|
6308
|
-
await access(workspace,
|
|
7513
|
+
await access(workspace, fsConstants2.R_OK);
|
|
6309
7514
|
return diagnostic("workspace", "pass", "Workspace exists and is readable.", { path: workspace });
|
|
6310
7515
|
} catch (error) {
|
|
6311
7516
|
const code = error.code ?? "UNKNOWN";
|
|
@@ -6439,7 +7644,7 @@ var inspectStateDirectory = async (workspace, stateDirectory) => {
|
|
|
6439
7644
|
writable: false
|
|
6440
7645
|
});
|
|
6441
7646
|
}
|
|
6442
|
-
await access(existing.path,
|
|
7647
|
+
await access(existing.path, fsConstants2.R_OK | fsConstants2.W_OK);
|
|
6443
7648
|
return diagnostic("state-directory", "pass", "State directory is safe and writable.", {
|
|
6444
7649
|
path: stateDirectory,
|
|
6445
7650
|
exists: existing.path === stateDirectory,
|
|
@@ -6841,6 +8046,90 @@ var updateIndexedSessionRun = async (config, runId, status) => {
|
|
|
6841
8046
|
}
|
|
6842
8047
|
};
|
|
6843
8048
|
var cliExitCodeForError = (error) => error instanceof CliUsageError ? CLI_EXIT_CODES.usageError : CLI_EXIT_CODES.runtimeError;
|
|
8049
|
+
var MAX_CHANGE_ENVELOPE_JSON_BYTES = 4 * 1024 * 1024;
|
|
8050
|
+
var MAX_CHANGE_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
8051
|
+
var readStableCliArtifact = async (filePath, maxBytes) => {
|
|
8052
|
+
const absolute = path9.resolve(process.cwd(), filePath);
|
|
8053
|
+
try {
|
|
8054
|
+
const before = await lstat7(absolute);
|
|
8055
|
+
if (before.isSymbolicLink() || !before.isFile()) {
|
|
8056
|
+
throw new CliUsageError(`Change artifact must be a regular non-symlink file: ${filePath}`);
|
|
8057
|
+
}
|
|
8058
|
+
if (before.size > maxBytes) {
|
|
8059
|
+
throw new CliUsageError(`Change artifact exceeds the ${maxBytes}-byte limit: ${filePath}`);
|
|
8060
|
+
}
|
|
8061
|
+
const contents = await readFile4(absolute);
|
|
8062
|
+
const after = await lstat7(absolute);
|
|
8063
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
|
|
8064
|
+
throw new CliUsageError(`Change artifact changed while it was being read: ${filePath}`);
|
|
8065
|
+
}
|
|
8066
|
+
return contents;
|
|
8067
|
+
} catch (error) {
|
|
8068
|
+
if (error instanceof CliUsageError)
|
|
8069
|
+
throw error;
|
|
8070
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
8071
|
+
if (typeof code === "string") {
|
|
8072
|
+
throw new CliUsageError(`Change artifact cannot be read (${code}): ${filePath}`);
|
|
8073
|
+
}
|
|
8074
|
+
throw error;
|
|
8075
|
+
}
|
|
8076
|
+
};
|
|
8077
|
+
var readCliJsonArtifact = async (filePath) => {
|
|
8078
|
+
const contents = await readStableCliArtifact(filePath, MAX_CHANGE_ENVELOPE_JSON_BYTES);
|
|
8079
|
+
try {
|
|
8080
|
+
return JSON.parse(contents.toString("utf8"));
|
|
8081
|
+
} catch {
|
|
8082
|
+
throw new CliUsageError(`Change artifact is not valid JSON: ${filePath}`);
|
|
8083
|
+
}
|
|
8084
|
+
};
|
|
8085
|
+
var objectValue = (value, label) => {
|
|
8086
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
8087
|
+
throw new CliUsageError(`${label} must be a JSON object.`);
|
|
8088
|
+
}
|
|
8089
|
+
return value;
|
|
8090
|
+
};
|
|
8091
|
+
var manageChanges = async (options) => {
|
|
8092
|
+
if (!options.changesCommand || !options.artifactPath || !options.patchPath) {
|
|
8093
|
+
throw new CliUsageError("Incomplete changes command.");
|
|
8094
|
+
}
|
|
8095
|
+
const patchBytes = await readStableCliArtifact(options.patchPath, MAX_CHANGE_ARTIFACT_BYTES);
|
|
8096
|
+
const patchDigest = digestChangeEnvelopeArtifact(patchBytes);
|
|
8097
|
+
if (options.changesCommand === "create") {
|
|
8098
|
+
const input = objectValue(await readCliJsonArtifact(options.artifactPath), "Change envelope input");
|
|
8099
|
+
const patchInput = objectValue(input.patch, "Change envelope patch");
|
|
8100
|
+
if (patchInput.patchDigest !== undefined && patchInput.patchDigest !== patchDigest) {
|
|
8101
|
+
throw new CliUsageError("The declared patch.patchDigest does not match the exact --patch bytes.");
|
|
8102
|
+
}
|
|
8103
|
+
const envelope2 = createChangeEnvelope({
|
|
8104
|
+
...input,
|
|
8105
|
+
patch: { ...patchInput, patchDigest }
|
|
8106
|
+
});
|
|
8107
|
+
process.stdout.write(`${canonicalizeChangeEnvelope(envelope2)}
|
|
8108
|
+
`);
|
|
8109
|
+
return;
|
|
8110
|
+
}
|
|
8111
|
+
const envelope = await readCliJsonArtifact(options.artifactPath);
|
|
8112
|
+
const suppliedPreconditions = options.preconditionsPath ? objectValue(await readCliJsonArtifact(options.preconditionsPath), "Change envelope preconditions") : {};
|
|
8113
|
+
if (suppliedPreconditions.patchDigest !== undefined && suppliedPreconditions.patchDigest !== patchDigest) {
|
|
8114
|
+
throw new CliUsageError("The precondition patchDigest does not match the exact --patch bytes.");
|
|
8115
|
+
}
|
|
8116
|
+
const preconditions = changeEnvelopePreconditionsSchema.parse({
|
|
8117
|
+
...suppliedPreconditions,
|
|
8118
|
+
patchDigest
|
|
8119
|
+
});
|
|
8120
|
+
const result = verifyChangeEnvelope(envelope, {
|
|
8121
|
+
...options.verificationTime ? { now: options.verificationTime } : {},
|
|
8122
|
+
preconditions
|
|
8123
|
+
});
|
|
8124
|
+
process.stdout.write(`${JSON.stringify({
|
|
8125
|
+
schemaVersion: 1,
|
|
8126
|
+
kind: "change-envelope-verification",
|
|
8127
|
+
...result
|
|
8128
|
+
})}
|
|
8129
|
+
`);
|
|
8130
|
+
if (!result.valid)
|
|
8131
|
+
process.exitCode = CLI_EXIT_CODES.runtimeError;
|
|
8132
|
+
};
|
|
6844
8133
|
var main = async (argv = process.argv.slice(2)) => {
|
|
6845
8134
|
const options = parseCliArgs(argv);
|
|
6846
8135
|
switch (options.command) {
|
|
@@ -6870,6 +8159,9 @@ var main = async (argv = process.argv.slice(2)) => {
|
|
|
6870
8159
|
case "sessions":
|
|
6871
8160
|
await manageSessions(options);
|
|
6872
8161
|
return;
|
|
8162
|
+
case "changes":
|
|
8163
|
+
await manageChanges(options);
|
|
8164
|
+
return;
|
|
6873
8165
|
case "run":
|
|
6874
8166
|
if (options.implicitCommand && !options.prompt && process.stdin.isTTY && process.stdout.isTTY) {
|
|
6875
8167
|
await chat(options);
|