@zhivex-ai/harness 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -10
- package/README.md +7 -5
- package/SUPPORT.md +2 -2
- package/contracts/client-protocol-v1.schema.json +593 -0
- package/contracts/engine-api.json +156 -0
- package/contracts/public-api.json +12 -0
- package/contracts/stable-api-signatures.json +33 -33
- package/dist/acp-cli.js +54 -20
- package/dist/audit/index.d.ts +3 -0
- package/dist/audit/index.js +482 -0
- package/dist/audit/pack.d.ts +72 -0
- package/dist/audit-pack-e4vaq7np.js +532 -0
- package/dist/cli/arguments.d.ts +3 -1
- package/dist/cli/audit-pack.d.ts +24 -0
- package/dist/cli/cli-credentials.d.ts +6 -0
- package/dist/cli/cli-profiles.d.ts +4 -2
- package/dist/cli/node-engine.d.ts +2 -0
- package/dist/{cli-2h37qyvg.js → cli-1cf4dcnj.js} +245 -642
- package/dist/{cli-entry-w0hterjs.js → cli-entry-3s63zn4w.js} +40 -13
- package/dist/cli-entry-7f0cen32.js +9990 -0
- package/dist/cli-entry-a890x3kq.js +10348 -0
- package/dist/{cli-entry-20w6nh0k.js → cli-entry-hdtfef27.js} +439 -19
- package/dist/cli-entry-rgs6bxmy.js +484 -0
- package/dist/cli.js +27 -2
- package/dist/client/json-contracts.d.ts +4 -0
- package/dist/client/protocol.d.ts +133 -0
- package/dist/client/task-control-host.d.ts +400 -0
- package/dist/client/task-projection-host.d.ts +12 -0
- package/dist/client/task-projection.d.ts +168 -0
- package/dist/compat/code-support.js +28 -22
- package/dist/{config-qt69hkqj.js → config-r9fdhce6.js} +3 -3
- package/dist/{config-j9624t6t.js → config-vyajmksy.js} +1 -1
- package/dist/engine/acp.js +15 -11
- package/dist/engine/client.js +14 -10
- package/dist/engine/desktop-providers-v1.d.ts +2 -0
- package/dist/engine/desktop-providers-v1.js +23 -3
- package/dist/engine/desktop-state-v1.js +11 -8
- package/dist/engine/index.d.ts +3 -0
- package/dist/engine/index.js +101 -554
- package/dist/engine/mcp-stdio-v1.js +8 -6
- package/dist/engine/models.js +76 -4
- package/dist/engine/protocol.d.ts +2 -0
- package/dist/engine/protocol.js +110 -0
- package/dist/engine/service.js +15 -11
- package/dist/hostile-repository-demo.js +1095 -505
- package/dist/{index-937cb7y8.js → index-02pqtbbg.js} +559 -33
- package/dist/{index-pp474137.js → index-0zs2dz26.js} +766 -931
- package/dist/{index-8zjmnyw5.js → index-1265j7t2.js} +73 -6
- package/dist/index-4fx5f3k5.js +269 -0
- package/dist/{index-nmjdwd4p.js → index-4gt045j0.js} +109 -1
- package/dist/{index-6dft2vev.js → index-6599mpmy.js} +5 -3
- package/dist/{index-tfzta544.js → index-74kkpyqk.js} +9 -7
- package/dist/{index-61gthdz6.js → index-8sm2a1wa.js} +1 -1
- package/dist/index-c39197wq.js +552 -0
- package/dist/{index-5jhgatga.js → index-crta5wzv.js} +7 -6
- package/dist/{index-m34q25jj.js → index-d9zkmhz3.js} +143 -824
- package/dist/{index-pdmevfxv.js → index-dtb2c6ds.js} +5 -259
- package/dist/index-f6mqa5gk.js +1535 -0
- package/dist/index-j6zxnc8m.js +490 -0
- package/dist/{index-rkbf7dqg.js → index-mv8sb1ns.js} +5 -3
- package/dist/{index-xqxeypt5.js → index-mzydbzp8.js} +76 -4
- package/dist/{index-wg1sd3at.js → index-qfsrrnz6.js} +1 -1
- package/dist/{index-m82cfspw.js → index-sk3cap35.js} +877 -963
- package/dist/index-sstgcsr0.js +511 -0
- package/dist/index-wm9xeq1n.js +329 -0
- package/dist/index-xdbkcccf.js +719 -0
- package/dist/{index-s2k2edts.js → index-zxg4ve4j.js} +41 -1548
- package/dist/index.d.ts +3 -0
- package/dist/index.js +93 -77
- package/dist/internal/desktop/providers.d.ts +2 -0
- package/dist/models/catalog.d.ts +2 -0
- package/dist/persistence/sqlite-database.d.ts +1 -0
- package/dist/providers/azure-openai.d.ts +14 -0
- package/dist/providers/bedrock.d.ts +23 -0
- package/dist/providers/providers.d.ts +1 -1
- package/dist/runtime/durable-review-group.d.ts +33 -0
- package/dist/runtime/harness.d.ts +11 -0
- package/dist/runtime/run-interruption.d.ts +7 -0
- package/dist/runtime/shared-budget.d.ts +30 -0
- package/dist/runtime/task-continuity.d.ts +300 -77
- package/dist/service-cli.js +15 -11
- package/dist/{service-client-cli-fn54ev6w.js → service-client-cli-eqrdc9zq.js} +400 -305
- package/dist/verify-historical-migrations.js +1095 -505
- package/dist/zhx.js +27 -2
- package/docs/AUDIT_PACK.md +99 -0
- package/docs/AZURE_OPENAI.md +53 -0
- package/docs/BEDROCK.md +56 -0
- package/docs/CLI.md +15 -0
- package/docs/CLIENT_PROTOCOL.md +178 -0
- package/docs/CODE_RELEASE.md +31 -1
- package/docs/CODE_WEB_TASKS.md +116 -0
- package/docs/CONTEXT_ENGINEERING.md +59 -0
- package/docs/CREDENTIALS.md +21 -0
- package/docs/DURABLE_OPERATIONS.md +93 -1
- package/docs/ENGINE_API.md +5 -2
- package/docs/EXTENSIBILITY.md +2 -2
- package/docs/FIRST_USE.md +6 -6
- package/docs/MINIMUM_TASK_AUTHORITY.md +3 -3
- package/docs/PROJECT_MEMORY.md +1 -1
- package/docs/README.md +6 -1
- package/docs/STABILITY.md +3 -1
- package/docs/SUPPORT_MATRIX.md +1 -1
- package/docs/SUPPORT_MATRIX_1_1.md +1 -1
- package/docs/USAGE.md +3 -1
- package/docs/VERTEX.md +2 -2
- package/docs/WEB_LIMITS.md +4 -3
- package/docs/adr/0002-isolated-mcp-stdio.md +6 -3
- package/docs/support-matrix-1.1.json +3 -1
- package/docs/support-matrix.json +6 -4
- package/evaluations/code-task-ui.json +20 -0
- package/evaluations/representative-assembly-matrix.json +10 -1
- package/evaluations/task-projection.json +10 -0
- package/evaluations/task-recovery-lab.json +60 -0
- package/examples/README.md +3 -3
- package/package.json +23 -13
- package/dist/cli-entry-4ngptr62.js +0 -19900
- package/dist/index-m49jx72n.js +0 -220
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HARNESS_EXECUTION_POLICY_VERSION
|
|
3
|
-
} from "./index-
|
|
3
|
+
} from "./index-1265j7t2.js";
|
|
4
|
+
import {
|
|
5
|
+
FileSizeLimitError,
|
|
6
|
+
MAX_EDIT_CHANGES,
|
|
7
|
+
MAX_EDIT_PROPOSAL_BYTES,
|
|
8
|
+
SqliteDatabase,
|
|
9
|
+
UnsafeFileTypeError,
|
|
10
|
+
assertTaskAcceptanceImport,
|
|
11
|
+
confirmTaskAcceptanceImport,
|
|
12
|
+
createEditProposal,
|
|
13
|
+
fileDigestSchema,
|
|
14
|
+
moveFileInputSchema,
|
|
15
|
+
mutationAuditEntrySchema,
|
|
16
|
+
quarantineFileInputSchema,
|
|
17
|
+
readRegularFileNoFollow,
|
|
18
|
+
restoreFileInputSchema,
|
|
19
|
+
statRegularFileNoFollow,
|
|
20
|
+
taskAcceptanceChecks,
|
|
21
|
+
validateEditProposal,
|
|
22
|
+
workspaceFilePathSchema
|
|
23
|
+
} from "./index-xdbkcccf.js";
|
|
4
24
|
import {
|
|
5
25
|
HarnessError,
|
|
6
26
|
HarnessExecutionError,
|
|
@@ -8,517 +28,9 @@ import {
|
|
|
8
28
|
HarnessWorkspaceError
|
|
9
29
|
} from "./index-bpc596v3.js";
|
|
10
30
|
|
|
11
|
-
// src/workspace/edit-contracts.ts
|
|
12
|
-
import { createHash } from "node:crypto";
|
|
13
|
-
import path from "node:path";
|
|
14
|
-
import { z } from "zod";
|
|
15
|
-
var EDIT_CONTRACT_SCHEMA_VERSION = 1;
|
|
16
|
-
var EDIT_DIGEST_ALGORITHM = "sha256";
|
|
17
|
-
var MAX_EDIT_CHANGES = 50;
|
|
18
|
-
var MAX_EDIT_FILE_BYTES = 1024 * 1024;
|
|
19
|
-
var MAX_EDIT_PROPOSAL_BYTES = 4 * 1024 * 1024;
|
|
20
|
-
var SHA256_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
21
|
-
var fileDigestSchema = z.string().regex(SHA256_DIGEST_PATTERN, "Digest must use the format sha256:<64 lowercase hexadecimal characters>.");
|
|
22
|
-
var expectedEditDigestSchema = fileDigestSchema.nullable().describe("Required. Use the inspected sha256 digest for an existing file, or explicit JSON null for a create-only target. Never omit this field.");
|
|
23
|
-
var workspaceFilePathSchema = z.string().min(1).max(1024).refine((value) => !value.includes("\x00"), "Path cannot contain a NUL byte.").refine((value) => !path.isAbsolute(value) && !/^[A-Za-z]:[\\/]/.test(value), "Path must be relative.").refine((value) => {
|
|
24
|
-
const segments = value.split(/[\\/]+/);
|
|
25
|
-
return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
|
|
26
|
-
}, "Path must be normalized and cannot contain empty, dot, or parent segments.");
|
|
27
|
-
var editChangeSchema = z.strictObject({
|
|
28
|
-
path: workspaceFilePathSchema,
|
|
29
|
-
expectedDigest: expectedEditDigestSchema,
|
|
30
|
-
content: z.string()
|
|
31
|
-
}).superRefine((change, context) => {
|
|
32
|
-
if (Buffer.byteLength(change.content, "utf8") > MAX_EDIT_FILE_BYTES) {
|
|
33
|
-
context.addIssue({
|
|
34
|
-
code: "custom",
|
|
35
|
-
path: ["content"],
|
|
36
|
-
message: `Content exceeds the ${MAX_EDIT_FILE_BYTES}-byte per-file limit.`
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
var editChangesSchema = z.array(editChangeSchema).min(1).max(MAX_EDIT_CHANGES).superRefine((changes, context) => {
|
|
41
|
-
const paths = new Set;
|
|
42
|
-
let totalBytes = 0;
|
|
43
|
-
for (let index = 0;index < changes.length; index += 1) {
|
|
44
|
-
const change = changes[index];
|
|
45
|
-
if (!change) {
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (paths.has(change.path)) {
|
|
49
|
-
context.addIssue({
|
|
50
|
-
code: "custom",
|
|
51
|
-
path: [index, "path"],
|
|
52
|
-
message: `Duplicate target path: ${change.path}.`
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
paths.add(change.path);
|
|
56
|
-
totalBytes += Buffer.byteLength(change.content, "utf8");
|
|
57
|
-
}
|
|
58
|
-
if (totalBytes > MAX_EDIT_PROPOSAL_BYTES) {
|
|
59
|
-
context.addIssue({
|
|
60
|
-
code: "custom",
|
|
61
|
-
message: `Proposal exceeds the ${MAX_EDIT_PROPOSAL_BYTES}-byte aggregate limit.`
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
var editProposalInputSchema = z.strictObject({
|
|
66
|
-
changes: editChangesSchema
|
|
67
|
-
});
|
|
68
|
-
var editProposalChangeSchema = z.strictObject({
|
|
69
|
-
path: workspaceFilePathSchema,
|
|
70
|
-
expectedDigest: expectedEditDigestSchema,
|
|
71
|
-
contentDigest: fileDigestSchema,
|
|
72
|
-
bytes: z.number().int().min(0).max(MAX_EDIT_FILE_BYTES)
|
|
73
|
-
});
|
|
74
|
-
var editProposalSchema = z.strictObject({
|
|
75
|
-
schemaVersion: z.literal(EDIT_CONTRACT_SCHEMA_VERSION),
|
|
76
|
-
kind: z.literal("edit-proposal"),
|
|
77
|
-
proposalId: fileDigestSchema,
|
|
78
|
-
digestAlgorithm: z.literal(EDIT_DIGEST_ALGORITHM),
|
|
79
|
-
changes: z.array(editProposalChangeSchema).min(1).max(MAX_EDIT_CHANGES)
|
|
80
|
-
});
|
|
81
|
-
var applyEditProposalInputSchema = z.strictObject({
|
|
82
|
-
proposalId: fileDigestSchema,
|
|
83
|
-
changes: editChangesSchema
|
|
84
|
-
}).superRefine((input, context) => {
|
|
85
|
-
if (!editProposalInputSchema.safeParse({ changes: input.changes }).success)
|
|
86
|
-
return;
|
|
87
|
-
if (createEditProposal({ changes: input.changes }).proposalId !== input.proposalId) {
|
|
88
|
-
context.addIssue({
|
|
89
|
-
code: "custom",
|
|
90
|
-
path: ["proposalId"],
|
|
91
|
-
message: "proposalId does not match the supplied paths, preconditions, and content. Read proposalId from the matching propose_edits result; do not invent it."
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
var moveFileInputSchema = z.strictObject({
|
|
96
|
-
source: workspaceFilePathSchema,
|
|
97
|
-
destination: workspaceFilePathSchema,
|
|
98
|
-
expectedDigest: fileDigestSchema
|
|
99
|
-
}).refine((input) => input.source !== input.destination, {
|
|
100
|
-
path: ["destination"],
|
|
101
|
-
message: "Move destination must differ from source."
|
|
102
|
-
});
|
|
103
|
-
var quarantineFileInputSchema = z.strictObject({
|
|
104
|
-
path: workspaceFilePathSchema,
|
|
105
|
-
expectedDigest: fileDigestSchema
|
|
106
|
-
});
|
|
107
|
-
var restoreFileInputSchema = z.strictObject({
|
|
108
|
-
quarantineId: z.string().min(1).max(200).regex(/^[A-Za-z0-9._:-]+$/),
|
|
109
|
-
destination: workspaceFilePathSchema.optional(),
|
|
110
|
-
expectedDigest: fileDigestSchema.optional()
|
|
111
|
-
});
|
|
112
|
-
var mutationOperationSchema = z.enum(["create", "update", "move", "quarantine", "restore"]);
|
|
113
|
-
var mutationAuditEntrySchema = z.strictObject({
|
|
114
|
-
id: z.string().min(1).max(200),
|
|
115
|
-
operation: mutationOperationSchema,
|
|
116
|
-
path: workspaceFilePathSchema,
|
|
117
|
-
destination: workspaceFilePathSchema.optional(),
|
|
118
|
-
beforeDigest: fileDigestSchema.optional(),
|
|
119
|
-
afterDigest: fileDigestSchema.optional(),
|
|
120
|
-
timestamp: z.iso.datetime(),
|
|
121
|
-
quarantineId: z.string().min(1).max(200).optional()
|
|
122
|
-
});
|
|
123
|
-
var digest = (value) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
124
|
-
var canonicalProposalPayload = (changes) => ({
|
|
125
|
-
schemaVersion: EDIT_CONTRACT_SCHEMA_VERSION,
|
|
126
|
-
kind: "edit-proposal",
|
|
127
|
-
digestAlgorithm: EDIT_DIGEST_ALGORITHM,
|
|
128
|
-
changes: [...changes].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0).map((change) => ({
|
|
129
|
-
path: change.path,
|
|
130
|
-
expectedDigest: change.expectedDigest,
|
|
131
|
-
contentDigest: digest(change.content),
|
|
132
|
-
bytes: Buffer.byteLength(change.content, "utf8")
|
|
133
|
-
}))
|
|
134
|
-
});
|
|
135
|
-
var createEditProposal = (input) => {
|
|
136
|
-
const parsed = editProposalInputSchema.parse(input);
|
|
137
|
-
const payload = canonicalProposalPayload(parsed.changes);
|
|
138
|
-
return editProposalSchema.parse({
|
|
139
|
-
...payload,
|
|
140
|
-
proposalId: digest(JSON.stringify(payload))
|
|
141
|
-
});
|
|
142
|
-
};
|
|
143
|
-
var validateEditProposal = (input) => {
|
|
144
|
-
const parsed = applyEditProposalInputSchema.parse(input);
|
|
145
|
-
const proposal = createEditProposal({ changes: parsed.changes });
|
|
146
|
-
if (proposal.proposalId !== parsed.proposalId) {
|
|
147
|
-
throw new Error("proposalId does not match the supplied paths, preconditions, and content.");
|
|
148
|
-
}
|
|
149
|
-
return parsed;
|
|
150
|
-
};
|
|
151
|
-
var editContractDocument = (kind, result) => ({
|
|
152
|
-
schemaVersion: EDIT_CONTRACT_SCHEMA_VERSION,
|
|
153
|
-
kind,
|
|
154
|
-
result
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// src/persistence/persistence-secrets.ts
|
|
158
|
-
var protectedValues = new Set;
|
|
159
|
-
function protectPersistenceSecret(secret) {
|
|
160
|
-
if (!secret)
|
|
161
|
-
return;
|
|
162
|
-
protectedValues.add(secret);
|
|
163
|
-
protectedValues.add(JSON.stringify(secret).slice(1, -1));
|
|
164
|
-
}
|
|
165
|
-
function assertNoPersistenceSecret(value) {
|
|
166
|
-
if (!protectedValues.size)
|
|
167
|
-
return;
|
|
168
|
-
const seen = new WeakSet;
|
|
169
|
-
const visit = (item) => {
|
|
170
|
-
if (typeof item === "string") {
|
|
171
|
-
for (const secret of protectedValues)
|
|
172
|
-
if (item.includes(secret))
|
|
173
|
-
throw new Error("PERSISTENCE_SECRET_REJECTED");
|
|
174
|
-
} else if (item instanceof Uint8Array) {
|
|
175
|
-
const bytes = Buffer.from(item.buffer, item.byteOffset, item.byteLength);
|
|
176
|
-
for (const secret of protectedValues)
|
|
177
|
-
if (bytes.includes(Buffer.from(secret)))
|
|
178
|
-
throw new Error("PERSISTENCE_SECRET_REJECTED");
|
|
179
|
-
} else if (item && typeof item === "object" && !seen.has(item)) {
|
|
180
|
-
seen.add(item);
|
|
181
|
-
for (const [key, entry] of Object.entries(item)) {
|
|
182
|
-
visit(key);
|
|
183
|
-
visit(entry);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
visit(value);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// src/persistence/sqlite-access.ts
|
|
191
|
-
import { closeSync, constants, fchmodSync, fstatSync, lstatSync, openSync, realpathSync } from "node:fs";
|
|
192
|
-
import path2 from "node:path";
|
|
193
|
-
var O_SHLOCK = 16;
|
|
194
|
-
var O_EXLOCK = 32;
|
|
195
|
-
var held = new WeakMap;
|
|
196
|
-
var activeDescriptors = new Set;
|
|
197
|
-
function filename(databasePath) {
|
|
198
|
-
const directory = realpathSync(path2.dirname(path2.resolve(databasePath)));
|
|
199
|
-
return path2.join(directory, `.${path2.basename(databasePath)}.access-lock`);
|
|
200
|
-
}
|
|
201
|
-
function releaseDescriptor(state) {
|
|
202
|
-
closeSync(state.fd);
|
|
203
|
-
activeDescriptors.delete(state.fd);
|
|
204
|
-
}
|
|
205
|
-
function makeLease(lock, fd, exclusive) {
|
|
206
|
-
const state = { filename: lock, fd, closed: false, borrowers: 0, exclusive };
|
|
207
|
-
const lease = Object.freeze({ fd, close() {
|
|
208
|
-
if (!state.closed) {
|
|
209
|
-
state.closed = true;
|
|
210
|
-
if (state.borrowers === 0)
|
|
211
|
-
releaseDescriptor(state);
|
|
212
|
-
}
|
|
213
|
-
} });
|
|
214
|
-
held.set(lease, state);
|
|
215
|
-
activeDescriptors.add(fd);
|
|
216
|
-
return lease;
|
|
217
|
-
}
|
|
218
|
-
function acquireSqliteAccess(databasePath, exclusive = false) {
|
|
219
|
-
if (databasePath === ":memory:" || process.platform !== "darwin")
|
|
220
|
-
return;
|
|
221
|
-
let fd;
|
|
222
|
-
try {
|
|
223
|
-
const lock = filename(databasePath);
|
|
224
|
-
fd = openSync(lock, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW | constants.O_NONBLOCK | (exclusive ? O_EXLOCK : O_SHLOCK), 384);
|
|
225
|
-
const info = fstatSync(fd);
|
|
226
|
-
const current = lstatSync(lock);
|
|
227
|
-
if (!info.isFile() || info.nlink !== 1 || info.uid !== process.getuid?.() || info.size !== 0 || current.isSymbolicLink() || current.dev !== info.dev || current.ino !== info.ino)
|
|
228
|
-
throw new Error;
|
|
229
|
-
if (info.mode & 63)
|
|
230
|
-
fchmodSync(fd, 384);
|
|
231
|
-
if (fstatSync(fd).mode & 63)
|
|
232
|
-
throw new Error;
|
|
233
|
-
return makeLease(lock, fd, exclusive);
|
|
234
|
-
} catch {
|
|
235
|
-
if (fd !== undefined)
|
|
236
|
-
closeSync(fd);
|
|
237
|
-
throw new Error("SQLITE_ACCESS_UNAVAILABLE");
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
function retainSqliteAccess(lease, databasePath) {
|
|
241
|
-
validateSqliteAccess(lease, databasePath);
|
|
242
|
-
const state = held.get(lease);
|
|
243
|
-
state.borrowers++;
|
|
244
|
-
let released = false;
|
|
245
|
-
return () => {
|
|
246
|
-
if (!released) {
|
|
247
|
-
released = true;
|
|
248
|
-
state.borrowers--;
|
|
249
|
-
if (state.closed && state.borrowers === 0)
|
|
250
|
-
releaseDescriptor(state);
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
function exclusiveSqliteAccessDescriptor(lease, databasePath) {
|
|
255
|
-
validateSqliteAccess(lease, databasePath);
|
|
256
|
-
const state = held.get(lease);
|
|
257
|
-
if (!state.exclusive || state.borrowers !== 0)
|
|
258
|
-
throw new Error("SQLITE_ACCESS_UNAVAILABLE");
|
|
259
|
-
return state.fd;
|
|
260
|
-
}
|
|
261
|
-
function adoptExclusiveSqliteAccess(databasePath, fd) {
|
|
262
|
-
try {
|
|
263
|
-
if (process.platform !== "darwin" || !Number.isInteger(fd) || fd < 4 || fd > 603 || activeDescriptors.has(fd))
|
|
264
|
-
throw new Error;
|
|
265
|
-
const lock = filename(databasePath), current = lstatSync(lock), owned = fstatSync(fd);
|
|
266
|
-
if (!owned.isFile() || owned.nlink !== 1 || owned.uid !== process.getuid?.() || owned.mode & 63 || owned.size !== 0 || current.isSymbolicLink() || current.dev !== owned.dev || current.ino !== owned.ino)
|
|
267
|
-
throw new Error;
|
|
268
|
-
return makeLease(lock, fd, true);
|
|
269
|
-
} catch {
|
|
270
|
-
throw new Error("SQLITE_ACCESS_UNAVAILABLE");
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
function validateSqliteAccess(lease, databasePath) {
|
|
274
|
-
try {
|
|
275
|
-
const state = held.get(lease);
|
|
276
|
-
if (!state || state.closed || state.filename !== filename(databasePath))
|
|
277
|
-
throw new Error;
|
|
278
|
-
const current = lstatSync(state.filename), owned = fstatSync(state.fd);
|
|
279
|
-
if (current.isSymbolicLink() || !current.isFile() || current.nlink !== 1 || current.dev !== owned.dev || current.ino !== owned.ino)
|
|
280
|
-
throw new Error;
|
|
281
|
-
} catch {
|
|
282
|
-
throw new Error("SQLITE_ACCESS_UNAVAILABLE");
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// src/persistence/sqlite-database.ts
|
|
287
|
-
import { createRequire } from "node:module";
|
|
288
|
-
var require2 = createRequire(process.execPath);
|
|
289
|
-
var DatabaseSyncConstructor;
|
|
290
|
-
var loadDatabaseSync = () => {
|
|
291
|
-
DatabaseSyncConstructor ??= require2("node:sqlite").DatabaseSync;
|
|
292
|
-
return DatabaseSyncConstructor;
|
|
293
|
-
};
|
|
294
|
-
var normalizeNumberedParameters = (sql) => {
|
|
295
|
-
const indexes = new Set;
|
|
296
|
-
const normalizedSql = sql.replace(/\?([1-9]\d*)/g, (_match, rawIndex) => {
|
|
297
|
-
const index = Number(rawIndex);
|
|
298
|
-
indexes.add(index);
|
|
299
|
-
return `$__zhivex_${index}`;
|
|
300
|
-
});
|
|
301
|
-
return {
|
|
302
|
-
sql: normalizedSql,
|
|
303
|
-
indexes: [...indexes].sort((left, right) => left - right)
|
|
304
|
-
};
|
|
305
|
-
};
|
|
306
|
-
var numberedBindings = (indexes, parameters) => {
|
|
307
|
-
if (indexes.length === 0)
|
|
308
|
-
return;
|
|
309
|
-
const maximumIndex = indexes.at(-1);
|
|
310
|
-
if (parameters.length !== maximumIndex) {
|
|
311
|
-
throw new Error(`SQLite numbered parameters require exactly ${maximumIndex} binding${maximumIndex === 1 ? "" : "s"}; received ${parameters.length}.`);
|
|
312
|
-
}
|
|
313
|
-
return Object.fromEntries(indexes.map((index) => [`$__zhivex_${index}`, parameters[index - 1]]));
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
class SqliteDatabase {
|
|
317
|
-
#database;
|
|
318
|
-
#access;
|
|
319
|
-
#ownedAccess;
|
|
320
|
-
#releaseBorrowed;
|
|
321
|
-
#databasePath;
|
|
322
|
-
constructor(databasePath, options = {}) {
|
|
323
|
-
options.create;
|
|
324
|
-
options.strict;
|
|
325
|
-
if (options.accessLease)
|
|
326
|
-
validateSqliteAccess(options.accessLease, databasePath);
|
|
327
|
-
this.#access = options.accessLease ?? acquireSqliteAccess(databasePath);
|
|
328
|
-
this.#ownedAccess = options.accessLease === undefined;
|
|
329
|
-
this.#databasePath = databasePath;
|
|
330
|
-
this.#releaseBorrowed = options.accessLease ? retainSqliteAccess(options.accessLease, databasePath) : undefined;
|
|
331
|
-
try {
|
|
332
|
-
const DatabaseSync = loadDatabaseSync();
|
|
333
|
-
this.#database = new DatabaseSync(databasePath, {
|
|
334
|
-
...options.readonly === undefined ? {} : { readOnly: options.readonly }
|
|
335
|
-
});
|
|
336
|
-
} catch (error) {
|
|
337
|
-
if (this.#ownedAccess)
|
|
338
|
-
this.#access?.close();
|
|
339
|
-
this.#releaseBorrowed?.();
|
|
340
|
-
throw error;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
exec(sql) {
|
|
344
|
-
if (this.#access)
|
|
345
|
-
validateSqliteAccess(this.#access, this.#databasePath);
|
|
346
|
-
assertNoPersistenceSecret(sql);
|
|
347
|
-
this.#database.exec(sql);
|
|
348
|
-
}
|
|
349
|
-
query(sql) {
|
|
350
|
-
assertNoPersistenceSecret(sql);
|
|
351
|
-
if (this.#access)
|
|
352
|
-
validateSqliteAccess(this.#access, this.#databasePath);
|
|
353
|
-
const normalized = normalizeNumberedParameters(sql);
|
|
354
|
-
const statement = this.#database.prepare(normalized.sql);
|
|
355
|
-
const invoke = (method, parameters) => {
|
|
356
|
-
if (this.#access)
|
|
357
|
-
validateSqliteAccess(this.#access, this.#databasePath);
|
|
358
|
-
assertNoPersistenceSecret(sql);
|
|
359
|
-
assertNoPersistenceSecret(parameters);
|
|
360
|
-
const bindings = numberedBindings(normalized.indexes, parameters);
|
|
361
|
-
return bindings ? method(bindings) : method(...parameters);
|
|
362
|
-
};
|
|
363
|
-
return {
|
|
364
|
-
run: (...parameters) => invoke(statement.run.bind(statement), parameters),
|
|
365
|
-
get: (...parameters) => invoke(statement.get.bind(statement), parameters),
|
|
366
|
-
all: (...parameters) => invoke(statement.all.bind(statement), parameters)
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
close(throwOnError = true) {
|
|
370
|
-
try {
|
|
371
|
-
this.#database.close();
|
|
372
|
-
if (this.#ownedAccess)
|
|
373
|
-
this.#access?.close();
|
|
374
|
-
this.#releaseBorrowed?.();
|
|
375
|
-
} catch (error) {
|
|
376
|
-
if (throwOnError) {
|
|
377
|
-
throw error;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// src/workspace/file-security.ts
|
|
384
|
-
import { constants as fsConstants } from "node:fs";
|
|
385
|
-
import { open } from "node:fs/promises";
|
|
386
|
-
import path3 from "node:path";
|
|
387
|
-
var sameOpenFile = (before, after) => before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs && before.mode === after.mode && before.nlink === after.nlink;
|
|
388
|
-
|
|
389
|
-
class UnsafeFileTypeError extends Error {
|
|
390
|
-
constructor(label) {
|
|
391
|
-
super(`${label} must be a regular file and must not be a symbolic link.`);
|
|
392
|
-
this.name = "UnsafeFileTypeError";
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
class FileSizeLimitError extends Error {
|
|
397
|
-
label;
|
|
398
|
-
maxBytes;
|
|
399
|
-
constructor(label, maxBytes) {
|
|
400
|
-
super(`${label} exceeds the ${maxBytes}-byte limit.`);
|
|
401
|
-
this.label = label;
|
|
402
|
-
this.maxBytes = maxBytes;
|
|
403
|
-
this.name = "FileSizeLimitError";
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
class FileChangedWhileReadingError extends Error {
|
|
408
|
-
constructor(label) {
|
|
409
|
-
super(`${label} changed while it was being read.`);
|
|
410
|
-
this.name = "FileChangedWhileReadingError";
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
var invalidFileError = (label) => new UnsafeFileTypeError(label);
|
|
414
|
-
var openRegularFileNoFollow = async (target, label) => {
|
|
415
|
-
try {
|
|
416
|
-
const flags = fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK;
|
|
417
|
-
let absolute = path3.resolve(target);
|
|
418
|
-
if (process.platform === "darwin") {
|
|
419
|
-
absolute = absolute.replace(/^\/(tmp|var|etc)(\/|$)/, "/private/$1$2");
|
|
420
|
-
return await open(absolute, fsConstants.O_RDONLY | fsConstants.O_NONBLOCK | 536870912, 384);
|
|
421
|
-
}
|
|
422
|
-
if (process.platform !== "linux")
|
|
423
|
-
throw new Error("Safe descriptor-relative reads require Linux or macOS.");
|
|
424
|
-
const segments = absolute.split("/").filter(Boolean);
|
|
425
|
-
const leaf = segments.pop();
|
|
426
|
-
if (!leaf)
|
|
427
|
-
throw invalidFileError(label);
|
|
428
|
-
let directory = await open("/", fsConstants.O_RDONLY | fsConstants.O_DIRECTORY);
|
|
429
|
-
try {
|
|
430
|
-
for (const segment of segments) {
|
|
431
|
-
const next = await open(`/proc/self/fd/${directory.fd}/${segment}`, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
|
|
432
|
-
await directory.close();
|
|
433
|
-
directory = next;
|
|
434
|
-
}
|
|
435
|
-
return await open(`/proc/self/fd/${directory.fd}/${leaf}`, flags);
|
|
436
|
-
} finally {
|
|
437
|
-
await directory.close();
|
|
438
|
-
}
|
|
439
|
-
} catch (error) {
|
|
440
|
-
const code = error.code;
|
|
441
|
-
if (code === "ELOOP" || code === "EMLINK" || code === "ENOTDIR")
|
|
442
|
-
throw invalidFileError(label);
|
|
443
|
-
throw error;
|
|
444
|
-
}
|
|
445
|
-
};
|
|
446
|
-
var statRegularFileNoFollow = async (target, options) => {
|
|
447
|
-
const handle = await openRegularFileNoFollow(target, options.label);
|
|
448
|
-
try {
|
|
449
|
-
const stat = await handle.stat();
|
|
450
|
-
if (!stat.isFile() || options.requireSingleLink && stat.nlink !== 1) {
|
|
451
|
-
throw invalidFileError(options.label);
|
|
452
|
-
}
|
|
453
|
-
return stat;
|
|
454
|
-
} finally {
|
|
455
|
-
await handle.close();
|
|
456
|
-
}
|
|
457
|
-
};
|
|
458
|
-
var readRegularFileNoFollow = async (target, options) => {
|
|
459
|
-
const handle = await openRegularFileNoFollow(target, options.label);
|
|
460
|
-
try {
|
|
461
|
-
const before = await handle.stat();
|
|
462
|
-
if (!before.isFile() || options.requireSingleLink && before.nlink !== 1) {
|
|
463
|
-
throw invalidFileError(options.label);
|
|
464
|
-
}
|
|
465
|
-
if (!Number.isSafeInteger(before.size) || before.size < 0) {
|
|
466
|
-
throw new Error(`${options.label} has an unsupported size.`);
|
|
467
|
-
}
|
|
468
|
-
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
|
|
469
|
-
throw new Error(`${options.label} has an invalid byte limit.`);
|
|
470
|
-
}
|
|
471
|
-
if (before.size > options.maxBytes) {
|
|
472
|
-
throw new FileSizeLimitError(options.label, options.maxBytes);
|
|
473
|
-
}
|
|
474
|
-
const contents = Buffer.alloc(before.size);
|
|
475
|
-
let offset = 0;
|
|
476
|
-
while (offset < contents.byteLength) {
|
|
477
|
-
const { bytesRead } = await handle.read(contents, offset, contents.byteLength - offset, offset);
|
|
478
|
-
if (bytesRead === 0)
|
|
479
|
-
break;
|
|
480
|
-
offset += bytesRead;
|
|
481
|
-
}
|
|
482
|
-
const probe = Buffer.allocUnsafe(1);
|
|
483
|
-
const { bytesRead: extraBytes } = await handle.read(probe, 0, 1, before.size);
|
|
484
|
-
const after = await handle.stat();
|
|
485
|
-
if (offset !== before.size || extraBytes !== 0 || !sameOpenFile(before, after) || options.requireSingleLink && after.nlink !== 1) {
|
|
486
|
-
throw new FileChangedWhileReadingError(options.label);
|
|
487
|
-
}
|
|
488
|
-
return { contents, stat: after };
|
|
489
|
-
} finally {
|
|
490
|
-
await handle.close();
|
|
491
|
-
}
|
|
492
|
-
};
|
|
493
|
-
async function* readRegularFileChunksNoFollow(target, options) {
|
|
494
|
-
const handle = await openRegularFileNoFollow(target, options.label);
|
|
495
|
-
try {
|
|
496
|
-
const before = await handle.stat();
|
|
497
|
-
if (!before.isFile() || options.requireSingleLink && before.nlink !== 1)
|
|
498
|
-
throw invalidFileError(options.label);
|
|
499
|
-
if (options.requirePrivate && (before.mode & 63) !== 0)
|
|
500
|
-
throw new Error(`${options.label} permissions must not grant group or other access.`);
|
|
501
|
-
let offset = 0;
|
|
502
|
-
while (offset < before.size) {
|
|
503
|
-
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, before.size - offset));
|
|
504
|
-
const { bytesRead: bytesRead2 } = await handle.read(buffer, 0, buffer.length, offset);
|
|
505
|
-
if (!bytesRead2)
|
|
506
|
-
throw new FileChangedWhileReadingError(options.label);
|
|
507
|
-
offset += bytesRead2;
|
|
508
|
-
yield buffer.subarray(0, bytesRead2);
|
|
509
|
-
}
|
|
510
|
-
const probe = Buffer.allocUnsafe(1);
|
|
511
|
-
const { bytesRead } = await handle.read(probe, 0, 1, offset);
|
|
512
|
-
if (bytesRead || !sameOpenFile(before, await handle.stat()))
|
|
513
|
-
throw new FileChangedWhileReadingError(options.label);
|
|
514
|
-
} finally {
|
|
515
|
-
await handle.close();
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
31
|
// src/execution/package-manager.ts
|
|
520
32
|
import { lstat } from "node:fs/promises";
|
|
521
|
-
import
|
|
33
|
+
import path from "node:path";
|
|
522
34
|
var HARNESS_PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
523
35
|
var LOCKFILES = {
|
|
524
36
|
npm: ["package-lock.json", "npm-shrinkwrap.json"],
|
|
@@ -547,7 +59,7 @@ var existingLockfiles = async (root) => {
|
|
|
547
59
|
for (const manager of HARNESS_PACKAGE_MANAGERS) {
|
|
548
60
|
for (const file of LOCKFILES[manager]) {
|
|
549
61
|
try {
|
|
550
|
-
const entry = await lstat(
|
|
62
|
+
const entry = await lstat(path.join(root, file));
|
|
551
63
|
if (entry.isSymbolicLink()) {
|
|
552
64
|
throw new Error(`Package-manager lockfile must not be a symbolic link: ${file}.`);
|
|
553
65
|
}
|
|
@@ -793,10 +305,10 @@ var runPortableProcess = async (command, options = {}) => {
|
|
|
793
305
|
};
|
|
794
306
|
|
|
795
307
|
// src/workspace/workspace-mutation-lock.ts
|
|
796
|
-
import { createHash
|
|
308
|
+
import { createHash } from "node:crypto";
|
|
797
309
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
798
|
-
import { lstat as lstat2, mkdir, open
|
|
799
|
-
import
|
|
310
|
+
import { lstat as lstat2, mkdir, open, realpath } from "node:fs/promises";
|
|
311
|
+
import path2 from "node:path";
|
|
800
312
|
import { setTimeout as delay } from "node:timers/promises";
|
|
801
313
|
var ownership = new AsyncLocalStorage;
|
|
802
314
|
var withWorkspaceMutation = async (root, operation) => {
|
|
@@ -808,7 +320,7 @@ var withWorkspaceMutation = async (root, operation) => {
|
|
|
808
320
|
if (uid === undefined || !["darwin", "linux"].includes(process.platform)) {
|
|
809
321
|
throw new HarnessWorkspaceError("Workspace mutation locks require Linux or macOS user identity.");
|
|
810
322
|
}
|
|
811
|
-
const directory =
|
|
323
|
+
const directory = path2.join(process.platform === "darwin" ? "/private/tmp" : "/tmp", `zhivex-harness-mutation-locks-${uid}`);
|
|
812
324
|
await mkdir(directory, { mode: 448 }).catch((error) => {
|
|
813
325
|
if (error.code !== "EEXIST")
|
|
814
326
|
throw error;
|
|
@@ -817,22 +329,22 @@ var withWorkspaceMutation = async (root, operation) => {
|
|
|
817
329
|
if (!entry.isDirectory() || entry.isSymbolicLink() || entry.uid !== uid || (entry.mode & 63) !== 0 || await realpath(directory) !== directory) {
|
|
818
330
|
throw new HarnessWorkspaceError("Workspace mutation lock requires a real state directory without symbolic links.");
|
|
819
331
|
}
|
|
820
|
-
const
|
|
332
|
+
const filename = path2.join(directory, `${createHash("sha256").update(root).digest("hex")}.sqlite`);
|
|
821
333
|
try {
|
|
822
|
-
const created = await
|
|
334
|
+
const created = await open(filename, "wx", 384);
|
|
823
335
|
await created.close();
|
|
824
336
|
} catch (error) {
|
|
825
337
|
if (error.code !== "EEXIST")
|
|
826
338
|
throw error;
|
|
827
339
|
}
|
|
828
|
-
const before = await statRegularFileNoFollow(
|
|
340
|
+
const before = await statRegularFileNoFollow(filename, { label: "Workspace mutation lock", requireSingleLink: true });
|
|
829
341
|
if (before.uid !== uid || (before.mode & 63) !== 0)
|
|
830
342
|
throw new HarnessWorkspaceError("Workspace mutation lock must be owner-only.");
|
|
831
|
-
const database = new SqliteDatabase(
|
|
343
|
+
const database = new SqliteDatabase(filename, { create: false });
|
|
832
344
|
let acquired = false;
|
|
833
345
|
const token = { active: true };
|
|
834
346
|
try {
|
|
835
|
-
const after = await statRegularFileNoFollow(
|
|
347
|
+
const after = await statRegularFileNoFollow(filename, { label: "Workspace mutation lock", requireSingleLink: true });
|
|
836
348
|
if (before.dev !== after.dev || before.ino !== after.ino) {
|
|
837
349
|
throw new HarnessWorkspaceError("Workspace mutation lock changed while opening.");
|
|
838
350
|
}
|
|
@@ -865,12 +377,12 @@ var withWorkspaceMutation = async (root, operation) => {
|
|
|
865
377
|
};
|
|
866
378
|
|
|
867
379
|
// src/workspace/replacement-edits.ts
|
|
868
|
-
import { z
|
|
869
|
-
var replacementEditSchema =
|
|
380
|
+
import { z } from "zod";
|
|
381
|
+
var replacementEditSchema = z.strictObject({
|
|
870
382
|
path: workspaceFilePathSchema,
|
|
871
383
|
expectedDigest: fileDigestSchema,
|
|
872
|
-
oldText:
|
|
873
|
-
newText:
|
|
384
|
+
oldText: z.string().min(1).max(64000),
|
|
385
|
+
newText: z.string().max(64000)
|
|
874
386
|
});
|
|
875
387
|
|
|
876
388
|
// src/workspace/bounded-reads.ts
|
|
@@ -887,9 +399,9 @@ async function* boundedBatches(items, operation, concurrency = 8) {
|
|
|
887
399
|
}
|
|
888
400
|
|
|
889
401
|
// src/workspace/workspace.ts
|
|
890
|
-
import { createHash as
|
|
891
|
-
import { link, lstat as lstat3, mkdir as mkdir2, open as
|
|
892
|
-
import
|
|
402
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
403
|
+
import { link, lstat as lstat3, mkdir as mkdir2, open as open2, readdir, realpath as realpath2, rename, stat, unlink } from "node:fs/promises";
|
|
404
|
+
import path3 from "node:path";
|
|
893
405
|
|
|
894
406
|
// src/workspace/path-policy.ts
|
|
895
407
|
var HARD_IGNORES = new Set([".git", ".next", ".turbo", ".zhivex-harness", "coverage", "dist", "node_modules"]);
|
|
@@ -925,11 +437,11 @@ var MAX_GIT_DIFF_PATHS = 2048;
|
|
|
925
437
|
var MAX_GIT_DIFF_ARGV_CHARACTERS = 128000;
|
|
926
438
|
var GIT_SAFE_PREFIX = ["git", "-c", "core.fsmonitor=false"];
|
|
927
439
|
var isInside = (root, candidate) => {
|
|
928
|
-
const relative =
|
|
929
|
-
return relative === "" || !relative.startsWith(`..${
|
|
440
|
+
const relative = path3.relative(root, candidate);
|
|
441
|
+
return relative === "" || !relative.startsWith(`..${path3.sep}`) && relative !== ".." && !path3.isAbsolute(relative);
|
|
930
442
|
};
|
|
931
|
-
var wirePath = (value) => value.split(
|
|
932
|
-
var digestBytes = (value) => `sha256:${
|
|
443
|
+
var wirePath = (value) => value.split(path3.sep).join("/");
|
|
444
|
+
var digestBytes = (value) => `sha256:${createHash2("sha256").update(value).digest("hex")}`;
|
|
933
445
|
var truncate = (value, maxLength = MAX_TOOL_OUTPUT) => value.length <= maxLength ? value : `${value.slice(0, maxLength)}
|
|
934
446
|
… output truncated (${value.length - maxLength} characters omitted)`;
|
|
935
447
|
var cursorEncode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
@@ -1058,7 +570,7 @@ var nulFields = (result, label) => {
|
|
|
1058
570
|
};
|
|
1059
571
|
var safeGitPath = (candidate) => {
|
|
1060
572
|
const normalized = wirePath(candidate);
|
|
1061
|
-
return normalized !== "" && normalized !== "." && !
|
|
573
|
+
return normalized !== "" && normalized !== "." && !path3.isAbsolute(candidate) && !normalized.startsWith("../") && !isHardIgnored(normalized);
|
|
1062
574
|
};
|
|
1063
575
|
var renderGitPath = (candidate) => /[\0\r\n\t]/.test(candidate) ? JSON.stringify(candidate) : candidate;
|
|
1064
576
|
var filteredGitStatus = (result) => {
|
|
@@ -1145,7 +657,7 @@ class Workspace {
|
|
|
1145
657
|
}
|
|
1146
658
|
static async open(root, options = {}) {
|
|
1147
659
|
try {
|
|
1148
|
-
const resolved = await realpath2(
|
|
660
|
+
const resolved = await realpath2(path3.resolve(root));
|
|
1149
661
|
if (!(await stat(resolved)).isDirectory()) {
|
|
1150
662
|
throw new HarnessWorkspaceError(`The workspace is not a directory: ${root}`);
|
|
1151
663
|
}
|
|
@@ -1172,10 +684,10 @@ class Workspace {
|
|
|
1172
684
|
lexicalPath(relativePath) {
|
|
1173
685
|
if (!relativePath || relativePath.includes("\x00"))
|
|
1174
686
|
throw new HarnessWorkspaceError("The path must be a valid relative path.");
|
|
1175
|
-
const candidate =
|
|
687
|
+
const candidate = path3.resolve(this.root, relativePath);
|
|
1176
688
|
if (!isInside(this.root, candidate))
|
|
1177
689
|
throw new HarnessWorkspaceError(`The path escapes the workspace: ${relativePath}`);
|
|
1178
|
-
const relative = wirePath(
|
|
690
|
+
const relative = wirePath(path3.relative(this.root, candidate));
|
|
1179
691
|
if (relative && isHardIgnored(relative)) {
|
|
1180
692
|
const blocked = relative.split("/").find((segment) => HARD_IGNORES.has(segment.toLocaleLowerCase()) || isSensitiveName(segment));
|
|
1181
693
|
throw new HarnessWorkspaceError(`The path is protected by the harness policy: ${blocked}`);
|
|
@@ -1186,10 +698,10 @@ class Workspace {
|
|
|
1186
698
|
const candidate = this.lexicalPath(relativePath);
|
|
1187
699
|
if (candidate === this.root && options.requireFile)
|
|
1188
700
|
throw new HarnessWorkspaceError("The workspace root is not a regular file.");
|
|
1189
|
-
const segments =
|
|
701
|
+
const segments = path3.relative(this.root, candidate).split(path3.sep).filter(Boolean);
|
|
1190
702
|
let current = this.root;
|
|
1191
703
|
for (let index = 0;index < segments.length; index += 1) {
|
|
1192
|
-
current =
|
|
704
|
+
current = path3.join(current, segments[index]);
|
|
1193
705
|
const leaf = index === segments.length - 1;
|
|
1194
706
|
if (leaf && options.skipLeafValidation)
|
|
1195
707
|
continue;
|
|
@@ -1223,14 +735,14 @@ class Workspace {
|
|
|
1223
735
|
if (!allowBinary && contents.includes(0))
|
|
1224
736
|
throw new HarnessWorkspaceError("The file appears to be binary and cannot be read as text.");
|
|
1225
737
|
return {
|
|
1226
|
-
path: wirePath(
|
|
738
|
+
path: wirePath(path3.relative(this.root, safe.path)),
|
|
1227
739
|
absolutePath: safe.path,
|
|
1228
740
|
contents,
|
|
1229
741
|
digest: digestBytes(contents),
|
|
1230
742
|
mode: file.stat.mode & 511
|
|
1231
743
|
};
|
|
1232
744
|
}
|
|
1233
|
-
fingerprint(relativePath, kind, entry,
|
|
745
|
+
fingerprint(relativePath, kind, entry, digest) {
|
|
1234
746
|
return {
|
|
1235
747
|
path: relativePath || ".",
|
|
1236
748
|
kind,
|
|
@@ -1239,7 +751,7 @@ class Workspace {
|
|
|
1239
751
|
size: entry.size,
|
|
1240
752
|
mtimeMs: entry.mtimeMs,
|
|
1241
753
|
ctimeMs: entry.ctimeMs,
|
|
1242
|
-
...
|
|
754
|
+
...digest ? { digest } : {}
|
|
1243
755
|
};
|
|
1244
756
|
}
|
|
1245
757
|
sameFingerprint(entry, fingerprint) {
|
|
@@ -1247,20 +759,20 @@ class Workspace {
|
|
|
1247
759
|
}
|
|
1248
760
|
async rulesForDirectory(directory, base, inherited, ignoreFiles) {
|
|
1249
761
|
const rules = [...inherited];
|
|
1250
|
-
for (const
|
|
762
|
+
for (const filename of IGNORE_FILES) {
|
|
1251
763
|
try {
|
|
1252
|
-
const ignorePath =
|
|
764
|
+
const ignorePath = path3.join(directory, filename);
|
|
1253
765
|
const inspected = await statRegularFileNoFollow(ignorePath, { label: "The ignore file" });
|
|
1254
766
|
if (inspected.size > MAX_FILE_BYTES) {
|
|
1255
|
-
ignoreFiles.push(this.fingerprint(wirePath(
|
|
767
|
+
ignoreFiles.push(this.fingerprint(wirePath(path3.relative(this.root, ignorePath)), "ignore-file", inspected));
|
|
1256
768
|
continue;
|
|
1257
769
|
}
|
|
1258
770
|
const file = await readRegularFileNoFollow(ignorePath, {
|
|
1259
771
|
label: "The ignore file",
|
|
1260
772
|
maxBytes: MAX_FILE_BYTES
|
|
1261
773
|
});
|
|
1262
|
-
const
|
|
1263
|
-
ignoreFiles.push(this.fingerprint(wirePath(
|
|
774
|
+
const digest = digestBytes(file.contents);
|
|
775
|
+
ignoreFiles.push(this.fingerprint(wirePath(path3.relative(this.root, ignorePath)), "ignore-file", file.stat, digest));
|
|
1264
776
|
rules.push(...parseIgnoreRules(file.contents.toString("utf8"), base));
|
|
1265
777
|
} catch (error) {
|
|
1266
778
|
if (error.code !== "ENOENT" && !(error instanceof UnsafeFileTypeError))
|
|
@@ -1291,7 +803,7 @@ class Workspace {
|
|
|
1291
803
|
if (isHardIgnored(relative) || entry.isSymbolicLink() || isIgnoredByRules(relative, entry.isDirectory(), rules))
|
|
1292
804
|
continue;
|
|
1293
805
|
if (entry.isDirectory())
|
|
1294
|
-
children.push({ directory:
|
|
806
|
+
children.push({ directory: path3.join(directory, entry.name), base: relative, inherited: rules });
|
|
1295
807
|
else if (entry.isFile())
|
|
1296
808
|
files.push({ path: relative });
|
|
1297
809
|
}
|
|
@@ -1309,14 +821,14 @@ class Workspace {
|
|
|
1309
821
|
async isWorkspaceIndexFresh(index) {
|
|
1310
822
|
try {
|
|
1311
823
|
for await (const batch of boundedBatches(index.directories, async (fingerprint) => {
|
|
1312
|
-
const absolute = fingerprint.path === "." ? this.root :
|
|
824
|
+
const absolute = fingerprint.path === "." ? this.root : path3.join(this.root, fingerprint.path);
|
|
1313
825
|
const entry = await lstat3(absolute);
|
|
1314
826
|
return !entry.isSymbolicLink() && entry.isDirectory() && this.sameFingerprint(entry, fingerprint);
|
|
1315
827
|
}))
|
|
1316
828
|
if (batch.some((fresh) => !fresh))
|
|
1317
829
|
return false;
|
|
1318
830
|
for await (const batch of boundedBatches(index.ignoreFiles, async (fingerprint) => {
|
|
1319
|
-
const absolute =
|
|
831
|
+
const absolute = path3.join(this.root, fingerprint.path);
|
|
1320
832
|
if (fingerprint.digest) {
|
|
1321
833
|
const file = await readRegularFileNoFollow(absolute, {
|
|
1322
834
|
label: "The ignore file",
|
|
@@ -1412,7 +924,7 @@ class Workspace {
|
|
|
1412
924
|
const start = await this.safePath(relativePath);
|
|
1413
925
|
if (!(await lstat3(start.path)).isDirectory())
|
|
1414
926
|
throw new HarnessWorkspaceError("list_files requires a directory.");
|
|
1415
|
-
const requestPath = wirePath(
|
|
927
|
+
const requestPath = wirePath(path3.relative(this.root, start.path)) || ".";
|
|
1416
928
|
const input = typeof options === "number" ? { limit: options } : options;
|
|
1417
929
|
const limit = input.limit ?? 200;
|
|
1418
930
|
const includeDigests = input.includeDigests ?? true;
|
|
@@ -1517,7 +1029,7 @@ class Workspace {
|
|
|
1517
1029
|
}
|
|
1518
1030
|
const ordered = requests.map((request) => ({
|
|
1519
1031
|
...request,
|
|
1520
|
-
path: wirePath(
|
|
1032
|
+
path: wirePath(path3.relative(this.root, this.lexicalPath(request.path)))
|
|
1521
1033
|
})).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));
|
|
1522
1034
|
for (const request of ordered) {
|
|
1523
1035
|
if (!Number.isSafeInteger(request.startLine ?? 1) || (request.startLine ?? 1) < 1 || request.endLine !== undefined && (!Number.isSafeInteger(request.endLine) || request.endLine < (request.startLine ?? 1))) {
|
|
@@ -1554,7 +1066,7 @@ class Workspace {
|
|
|
1554
1066
|
throw new HarnessWorkspaceError("The search query must be between 1 and 200 characters.");
|
|
1555
1067
|
const start = await this.safePath(relativePath);
|
|
1556
1068
|
const isDirectory = (await lstat3(start.path)).isDirectory();
|
|
1557
|
-
const requestPath = wirePath(
|
|
1069
|
+
const requestPath = wirePath(path3.relative(this.root, start.path)) || ".";
|
|
1558
1070
|
const limit = options.limit ?? options.maxMatches ?? 100;
|
|
1559
1071
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)
|
|
1560
1072
|
throw new HarnessWorkspaceError("limit must be between 1 and 500.");
|
|
@@ -1679,7 +1191,7 @@ class Workspace {
|
|
|
1679
1191
|
});
|
|
1680
1192
|
const start = await this.safePath(relativePath);
|
|
1681
1193
|
const isDirectory = (await lstat3(start.path)).isDirectory();
|
|
1682
|
-
const requestPath = wirePath(
|
|
1194
|
+
const requestPath = wirePath(path3.relative(this.root, start.path)) || ".";
|
|
1683
1195
|
const prefix = requestPath === "." ? "" : `${requestPath}/`;
|
|
1684
1196
|
const index = isDirectory ? await this.getWorkspaceIndex() : undefined;
|
|
1685
1197
|
const candidateIndex = requestPath === "." ? 0 : firstPathAtLeast(index?.files ?? [], prefix);
|
|
@@ -1773,7 +1285,7 @@ class Workspace {
|
|
|
1773
1285
|
this.auditEntries.push(record);
|
|
1774
1286
|
if (this.auditPath) {
|
|
1775
1287
|
const temporary = `${this.auditPath}.${randomUUID()}.tmp`;
|
|
1776
|
-
const handle = await
|
|
1288
|
+
const handle = await open2(temporary, "wx", 384);
|
|
1777
1289
|
try {
|
|
1778
1290
|
await handle.writeFile(JSON.stringify(this.auditEntries));
|
|
1779
1291
|
await handle.sync();
|
|
@@ -1792,14 +1304,14 @@ class Workspace {
|
|
|
1792
1304
|
return this.auditEntries.map((entry) => ({ ...entry }));
|
|
1793
1305
|
}
|
|
1794
1306
|
async secureWorkspaceDirectory(directory, mode = 493) {
|
|
1795
|
-
const resolvedInput =
|
|
1307
|
+
const resolvedInput = path3.resolve(directory);
|
|
1796
1308
|
if (!isInside(this.root, resolvedInput)) {
|
|
1797
1309
|
throw new HarnessWorkspaceError("A mutation directory escapes the workspace.");
|
|
1798
1310
|
}
|
|
1799
|
-
const segments =
|
|
1311
|
+
const segments = path3.relative(this.root, resolvedInput).split(path3.sep).filter(Boolean);
|
|
1800
1312
|
let current = this.root;
|
|
1801
1313
|
for (const segment of segments) {
|
|
1802
|
-
current =
|
|
1314
|
+
current = path3.join(current, segment);
|
|
1803
1315
|
try {
|
|
1804
1316
|
const entry = await lstat3(current);
|
|
1805
1317
|
if (entry.isSymbolicLink())
|
|
@@ -1827,9 +1339,9 @@ class Workspace {
|
|
|
1827
1339
|
return canonical;
|
|
1828
1340
|
}
|
|
1829
1341
|
async stageFile(target, contents, mode) {
|
|
1830
|
-
const directory = await this.secureWorkspaceDirectory(
|
|
1831
|
-
const temporary =
|
|
1832
|
-
const handle = await
|
|
1342
|
+
const directory = await this.secureWorkspaceDirectory(path3.dirname(target));
|
|
1343
|
+
const temporary = path3.join(directory, `.${path3.basename(target)}.zhivex-${randomUUID()}.tmp`);
|
|
1344
|
+
const handle = await open2(temporary, "wx", mode);
|
|
1833
1345
|
try {
|
|
1834
1346
|
await handle.writeFile(contents);
|
|
1835
1347
|
await handle.chmod(mode);
|
|
@@ -2052,7 +1564,7 @@ class Workspace {
|
|
|
2052
1564
|
const destination = await this.safePath(parsed.destination, { allowMissing: true });
|
|
2053
1565
|
if (destination.exists)
|
|
2054
1566
|
throw new HarnessWorkspaceError(`Move destination already exists: ${parsed.destination}.`);
|
|
2055
|
-
await this.secureWorkspaceDirectory(
|
|
1567
|
+
await this.secureWorkspaceDirectory(path3.dirname(destination.path));
|
|
2056
1568
|
const destinationRecheck = await this.safePath(parsed.destination, { allowMissing: true });
|
|
2057
1569
|
if (destinationRecheck.exists)
|
|
2058
1570
|
throw new HarnessWorkspaceError(`Move destination already exists: ${parsed.destination}.`);
|
|
@@ -2080,7 +1592,7 @@ class Workspace {
|
|
|
2080
1592
|
const segments = [".zhivex-harness", "quarantine"];
|
|
2081
1593
|
let current = this.root;
|
|
2082
1594
|
for (const segment of segments) {
|
|
2083
|
-
current =
|
|
1595
|
+
current = path3.join(current, segment);
|
|
2084
1596
|
try {
|
|
2085
1597
|
const entry = await lstat3(current);
|
|
2086
1598
|
if (entry.isSymbolicLink()) {
|
|
@@ -2111,7 +1623,7 @@ class Workspace {
|
|
|
2111
1623
|
}
|
|
2112
1624
|
async writeManifest(manifest) {
|
|
2113
1625
|
const directory = await this.secureQuarantineDirectory(true);
|
|
2114
|
-
const target =
|
|
1626
|
+
const target = path3.join(directory, `${manifest.quarantineId}.json`);
|
|
2115
1627
|
const temporary = await this.stageFile(target, `${JSON.stringify(manifest, null, 2)}
|
|
2116
1628
|
`, 384);
|
|
2117
1629
|
await rename(temporary, target);
|
|
@@ -2130,7 +1642,7 @@ class Workspace {
|
|
|
2130
1642
|
throw new HarnessWorkspaceError(`Stale quarantine rejected for ${parsed.path}.`);
|
|
2131
1643
|
const quarantineId = `${Date.now()}-${randomUUID()}`;
|
|
2132
1644
|
const directory = await this.secureQuarantineDirectory(true);
|
|
2133
|
-
const dataPath =
|
|
1645
|
+
const dataPath = path3.join(directory, `${quarantineId}.data`);
|
|
2134
1646
|
if ((await this.readStableFile(parsed.path)).digest !== parsed.expectedDigest)
|
|
2135
1647
|
throw new HarnessWorkspaceError(`Stale quarantine rejected for ${parsed.path}.`);
|
|
2136
1648
|
await link(source.absolutePath, dataPath);
|
|
@@ -2153,7 +1665,7 @@ class Workspace {
|
|
|
2153
1665
|
await unlink(source.absolutePath);
|
|
2154
1666
|
} catch (error) {
|
|
2155
1667
|
await unlink(dataPath).catch(() => {});
|
|
2156
|
-
await unlink(
|
|
1668
|
+
await unlink(path3.join(directory, `${quarantineId}.json`)).catch(() => {});
|
|
2157
1669
|
throw error;
|
|
2158
1670
|
}
|
|
2159
1671
|
this.invalidateWorkspaceIndex();
|
|
@@ -2169,7 +1681,7 @@ class Workspace {
|
|
|
2169
1681
|
const directory = await this.secureQuarantineDirectory(false);
|
|
2170
1682
|
let manifest;
|
|
2171
1683
|
try {
|
|
2172
|
-
const manifestFile = await this.readInternalRegularFile(
|
|
1684
|
+
const manifestFile = await this.readInternalRegularFile(path3.join(directory, `${parsed.quarantineId}.json`), "Quarantine manifest", 64 * 1024);
|
|
2173
1685
|
manifest = JSON.parse(manifestFile.contents.toString("utf8"));
|
|
2174
1686
|
} catch {
|
|
2175
1687
|
throw new HarnessWorkspaceError(`Quarantine entry was not found: ${parsed.quarantineId}.`);
|
|
@@ -2177,17 +1689,17 @@ class Workspace {
|
|
|
2177
1689
|
if (manifest.schemaVersion !== 1 || manifest.quarantineId !== parsed.quarantineId || manifest.status !== "quarantined") {
|
|
2178
1690
|
throw new HarnessWorkspaceError(`Quarantine entry is not restorable: ${parsed.quarantineId}.`);
|
|
2179
1691
|
}
|
|
2180
|
-
const dataPath =
|
|
1692
|
+
const dataPath = path3.join(directory, `${parsed.quarantineId}.data`);
|
|
2181
1693
|
const dataFile = await this.readInternalRegularFile(dataPath, "Quarantine payload", MAX_FILE_BYTES);
|
|
2182
|
-
const
|
|
2183
|
-
if (
|
|
1694
|
+
const digest = digestBytes(dataFile.contents);
|
|
1695
|
+
if (digest !== manifest.digest || parsed.expectedDigest && parsed.expectedDigest !== digest) {
|
|
2184
1696
|
throw new HarnessWorkspaceError(`Quarantine digest mismatch: ${parsed.quarantineId}.`);
|
|
2185
1697
|
}
|
|
2186
1698
|
const destinationPath = parsed.destination ?? manifest.originalPath;
|
|
2187
1699
|
const destination = await this.safePath(destinationPath, { allowMissing: true });
|
|
2188
1700
|
if (destination.exists)
|
|
2189
1701
|
throw new HarnessWorkspaceError(`Restore destination already exists: ${destinationPath}.`);
|
|
2190
|
-
await this.secureWorkspaceDirectory(
|
|
1702
|
+
await this.secureWorkspaceDirectory(path3.dirname(destination.path));
|
|
2191
1703
|
const destinationRecheck = await this.safePath(destinationPath, { allowMissing: true });
|
|
2192
1704
|
if (destinationRecheck.exists)
|
|
2193
1705
|
throw new HarnessWorkspaceError(`Restore destination already exists: ${destinationPath}.`);
|
|
@@ -2197,7 +1709,7 @@ class Workspace {
|
|
|
2197
1709
|
}
|
|
2198
1710
|
await link(dataPath, destination.path);
|
|
2199
1711
|
const linkedDestination = await this.readInternalRegularFile(destination.path, "Restore destination", MAX_FILE_BYTES);
|
|
2200
|
-
if (linkedDestination.stat.dev !== dataFile.stat.dev || linkedDestination.stat.ino !== dataFile.stat.ino || digestBytes(linkedDestination.contents) !==
|
|
1712
|
+
if (linkedDestination.stat.dev !== dataFile.stat.dev || linkedDestination.stat.ino !== dataFile.stat.ino || digestBytes(linkedDestination.contents) !== digest) {
|
|
2201
1713
|
await unlink(destination.path).catch(() => {});
|
|
2202
1714
|
throw new HarnessWorkspaceError("Restore destination did not match the quarantined payload.");
|
|
2203
1715
|
}
|
|
@@ -2230,8 +1742,8 @@ class Workspace {
|
|
|
2230
1742
|
throw rollbackErrors.length ? new AggregateError([error, ...rollbackErrors], "Restore failed and rollback was incomplete.") : error;
|
|
2231
1743
|
}
|
|
2232
1744
|
this.invalidateWorkspaceIndex();
|
|
2233
|
-
const audit = await this.audit({ operation: "restore", path: destinationPath, afterDigest:
|
|
2234
|
-
return { quarantineId: parsed.quarantineId, path: destinationPath, digest
|
|
1745
|
+
const audit = await this.audit({ operation: "restore", path: destinationPath, afterDigest: digest, quarantineId: parsed.quarantineId });
|
|
1746
|
+
return { quarantineId: parsed.quarantineId, path: destinationPath, digest, audit };
|
|
2235
1747
|
}
|
|
2236
1748
|
async runCheck(check, expectedScript, allowedChecks = DEFAULT_CHECKS) {
|
|
2237
1749
|
let manifest;
|
|
@@ -2273,214 +1785,21 @@ class EnvironmentPatchDriftError extends Error {
|
|
|
2273
1785
|
}
|
|
2274
1786
|
}
|
|
2275
1787
|
|
|
2276
|
-
// src/runtime/task-acceptance-delivery.ts
|
|
2277
|
-
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
2278
|
-
import { z as z4 } from "zod";
|
|
2279
|
-
|
|
2280
|
-
// src/runtime/task-acceptance-checks.ts
|
|
2281
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
2282
|
-
import { z as z3 } from "zod";
|
|
2283
|
-
var sha = z3.string().regex(/^sha256:[a-f0-9]{64}$/);
|
|
2284
|
-
var taskCheckReceiptSchema = z3.strictObject({
|
|
2285
|
-
schemaVersion: z3.literal(1),
|
|
2286
|
-
checkId: z3.string().max(128),
|
|
2287
|
-
contractDigest: sha,
|
|
2288
|
-
contractRevision: z3.number().int().positive(),
|
|
2289
|
-
runId: z3.string().max(512),
|
|
2290
|
-
executionIdentity: z3.string().max(512),
|
|
2291
|
-
patchId: sha,
|
|
2292
|
-
snapshotDigest: sha,
|
|
2293
|
-
argvDigest: sha,
|
|
2294
|
-
exitCode: z3.number().int(),
|
|
2295
|
-
timedOut: z3.boolean(),
|
|
2296
|
-
unchanged: z3.boolean(),
|
|
2297
|
-
source: z3.strictObject({ id: z3.string().min(1).max(512), name: z3.string().min(1).max(128), startedAt: z3.number().finite().nonnegative().optional() }).optional()
|
|
2298
|
-
});
|
|
2299
|
-
var argvDigest = (command, args) => "sha256:" + createHash4("sha256").update(JSON.stringify([command, ...args])).digest("hex");
|
|
2300
|
-
function createTaskAcceptanceChecks(ledger, previous = [], journal = []) {
|
|
2301
|
-
const current = ledger.revisions.at(-1);
|
|
2302
|
-
const receipts = new Map;
|
|
2303
|
-
const parsed = z3.array(taskCheckReceiptSchema).max(32).safeParse(previous);
|
|
2304
|
-
if (parsed.success)
|
|
2305
|
-
for (const receipt of parsed.data) {
|
|
2306
|
-
const check = current.contract.requiredChecks.find((c) => c.id === receipt.checkId);
|
|
2307
|
-
if (check && receipt.contractDigest === current.digest && receipt.contractRevision === current.revision && receipt.argvDigest === argvDigest(check.command, check.args) && reconciledReceipt(receipt, journal))
|
|
2308
|
-
receipts.set(check.id, receipt);
|
|
2309
|
-
}
|
|
2310
|
-
return {
|
|
2311
|
-
matching(command, args) {
|
|
2312
|
-
return current.contract.requiredChecks.filter((check) => argvDigest(check.command, check.args) === argvDigest(command, args));
|
|
2313
|
-
},
|
|
2314
|
-
begin(checkIds) {
|
|
2315
|
-
for (const id of checkIds)
|
|
2316
|
-
receipts.delete(id);
|
|
2317
|
-
},
|
|
2318
|
-
record(checkId, before, after, result, source) {
|
|
2319
|
-
const check = current.contract.requiredChecks.find((c) => c.id === checkId);
|
|
2320
|
-
if (!check)
|
|
2321
|
-
throw new Error("TASK_ACCEPTANCE_UNKNOWN_CHECK");
|
|
2322
|
-
const receipt = taskCheckReceiptSchema.parse({
|
|
2323
|
-
schemaVersion: 1,
|
|
2324
|
-
checkId,
|
|
2325
|
-
contractDigest: current.digest,
|
|
2326
|
-
contractRevision: current.revision,
|
|
2327
|
-
...before,
|
|
2328
|
-
...source ? { source: { id: source.id, name: source.name, ...source.startedAt !== undefined ? { startedAt: source.startedAt } : {} } } : {},
|
|
2329
|
-
argvDigest: argvDigest(check.command, check.args),
|
|
2330
|
-
exitCode: result.exitCode,
|
|
2331
|
-
timedOut: result.timedOut,
|
|
2332
|
-
unchanged: before.runId === after.runId && before.executionIdentity === after.executionIdentity && before.patchId === after.patchId && before.snapshotDigest === after.snapshotDigest
|
|
2333
|
-
});
|
|
2334
|
-
receipts.set(checkId, receipt);
|
|
2335
|
-
return structuredClone(receipt);
|
|
2336
|
-
},
|
|
2337
|
-
missing(binding) {
|
|
2338
|
-
return current.contract.requiredChecks.filter((check) => {
|
|
2339
|
-
const receipt = receipts.get(check.id);
|
|
2340
|
-
return !receipt || receipt.exitCode !== 0 || receipt.timedOut || !receipt.unchanged || receipt.runId !== binding.runId || receipt.executionIdentity !== binding.executionIdentity || receipt.patchId !== binding.patchId || receipt.snapshotDigest !== binding.snapshotDigest;
|
|
2341
|
-
}).map((check) => check.id);
|
|
2342
|
-
},
|
|
2343
|
-
snapshot() {
|
|
2344
|
-
return [...receipts.values()].map((receipt) => structuredClone(receipt));
|
|
2345
|
-
}
|
|
2346
|
-
};
|
|
2347
|
-
}
|
|
2348
|
-
var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
2349
|
-
var commandDigest = (value) => {
|
|
2350
|
-
const input = object(value);
|
|
2351
|
-
return typeof input?.command === "string" && Array.isArray(input.args) && input.args.every((arg) => typeof arg === "string") ? argvDigest(input.command, input.args) : undefined;
|
|
2352
|
-
};
|
|
2353
|
-
function reconciledReceipt(receipt, journal) {
|
|
2354
|
-
if (!receipt.source)
|
|
2355
|
-
return false;
|
|
2356
|
-
const matches = journal.filter((row2) => row2.runId === receipt.runId && row2.providerToolCallId === receipt.source.id && row2.toolName === receipt.source.name);
|
|
2357
|
-
if (matches.length !== 1)
|
|
2358
|
-
return false;
|
|
2359
|
-
const row = matches[0];
|
|
2360
|
-
const startedAt = receipt.source.startedAt ?? row.startedAt;
|
|
2361
|
-
if (row.status !== "completed" || !Number.isFinite(startedAt) || !Number.isFinite(row.completedAt) || row.completedAt < startedAt)
|
|
2362
|
-
return false;
|
|
2363
|
-
if (!["run_environment_command", "run_check", "verify_and_apply_environment_patch", "verify_and_apply_reviewed_edits"].includes(row.toolName))
|
|
2364
|
-
return false;
|
|
2365
|
-
const output = object(row.output), result = object(output?.verification) ?? output;
|
|
2366
|
-
if (!result || result.exitCode !== receipt.exitCode || result.timedOut !== receipt.timedOut || !Array.isArray(result.command) || !result.command.every((arg) => typeof arg === "string") || typeof result.command[0] !== "string" || argvDigest(result.command[0], result.command.slice(1)) !== receipt.argvDigest)
|
|
2367
|
-
return false;
|
|
2368
|
-
if (row.toolName !== "run_check" && commandDigest(row.input) !== receipt.argvDigest)
|
|
2369
|
-
return false;
|
|
2370
|
-
for (const attempt of journal) {
|
|
2371
|
-
if (attempt === row || attempt.runId !== receipt.runId)
|
|
2372
|
-
continue;
|
|
2373
|
-
if (attempt.status === "pending" || attempt.status === "running")
|
|
2374
|
-
return false;
|
|
2375
|
-
const input = object(attempt.input);
|
|
2376
|
-
const sameCheck = commandDigest(input) === receipt.argvDigest || attempt.toolName === "run_check" && row.toolName === "run_check" && input?.check === object(row.input)?.check || attempt.toolName === "run_environment_batch" && Array.isArray(input?.commands) && input.commands.some((command) => commandDigest(command) === receipt.argvDigest);
|
|
2377
|
-
if (sameCheck && (!Number.isFinite(attempt.completedAt) || attempt.completedAt >= startedAt))
|
|
2378
|
-
return false;
|
|
2379
|
-
}
|
|
2380
|
-
return true;
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
// src/runtime/task-acceptance-delivery.ts
|
|
2384
|
-
var scopes = new AsyncLocalStorage2;
|
|
2385
|
-
function withTaskAcceptanceDelivery(workspace, ledger, work, previousEvidence, journal = []) {
|
|
2386
|
-
const latest = ledger.revisions.at(-1);
|
|
2387
|
-
const previous = z4.object({ contractDigest: z4.literal(latest.digest), contractRevision: z4.literal(latest.revision), diagnostic: z4.strictObject({
|
|
2388
|
-
code: z4.literal("TASK_ACCEPTANCE_SCOPE_VIOLATION"),
|
|
2389
|
-
runId: z4.string().max(512),
|
|
2390
|
-
patchId: z4.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
2391
|
-
paths: z4.array(z4.string().max(1024)).max(256)
|
|
2392
|
-
}) }).safeParse(previousEvidence);
|
|
2393
|
-
const previousDelivery = z4.object({ contractDigest: z4.literal(latest.digest), contractRevision: z4.literal(latest.revision), delivery: z4.strictObject({
|
|
2394
|
-
runId: z4.string().min(1).max(512),
|
|
2395
|
-
executionIdentity: z4.string().min(1).max(512),
|
|
2396
|
-
patchId: z4.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
2397
|
-
snapshotDigest: z4.string().regex(/^sha256:[a-f0-9]{64}$/)
|
|
2398
|
-
}) }).safeParse(previousEvidence);
|
|
2399
|
-
return scopes.run({
|
|
2400
|
-
...previousDelivery.success ? { previousDelivery: previousDelivery.data.delivery } : {},
|
|
2401
|
-
workspace,
|
|
2402
|
-
revision: latest.revision,
|
|
2403
|
-
digest: latest.digest,
|
|
2404
|
-
checks: createTaskAcceptanceChecks(ledger, previousEvidence && typeof previousEvidence === "object" && "checks" in previousEvidence ? previousEvidence.checks : [], journal),
|
|
2405
|
-
allowed: new Set(latest.contract.allowedWritePaths),
|
|
2406
|
-
protected: new Set(latest.contract.protectedFiles.map((file) => file.toLowerCase())),
|
|
2407
|
-
...previous.success ? { violation: previous.data.diagnostic } : {}
|
|
2408
|
-
}, work);
|
|
2409
|
-
}
|
|
2410
|
-
function assertTaskAcceptanceImport(input) {
|
|
2411
|
-
const scope = scopes.getStore();
|
|
2412
|
-
if (!scope)
|
|
2413
|
-
return;
|
|
2414
|
-
const blocked = input.entries.filter((entry) => {
|
|
2415
|
-
const candidate = entry.path.toLowerCase();
|
|
2416
|
-
return input.workspace !== scope.workspace || !scope.allowed.has(entry.path) || [...scope.protected].some((file) => candidate === file || candidate.startsWith(file + "/") || file.startsWith(candidate + "/"));
|
|
2417
|
-
});
|
|
2418
|
-
if (blocked.length) {
|
|
2419
|
-
scope.violation = { code: "TASK_ACCEPTANCE_SCOPE_VIOLATION", runId: input.runId, patchId: input.patchId, paths: blocked.map((entry) => entry.path).slice(0, 256) };
|
|
2420
|
-
throw new Error("TASK_ACCEPTANCE_SCOPE_VIOLATION: the OCI patch touches files outside the task contract; no host import was performed.");
|
|
2421
|
-
}
|
|
2422
|
-
if (!input.executionIdentity || !input.snapshotDigest || scope.checks.missing({ runId: input.runId, patchId: input.patchId, executionIdentity: input.executionIdentity, snapshotDigest: input.snapshotDigest }).length) {
|
|
2423
|
-
throw new Error("TASK_ACCEPTANCE_CHECKS_MISSING: every required check must succeed on the exact final snapshot and patch before import.");
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
var taskAcceptanceChecks = () => scopes.getStore()?.checks;
|
|
2427
|
-
function taskAcceptanceDeliveryDiagnostic(digest2, revision) {
|
|
2428
|
-
const scope = scopes.getStore();
|
|
2429
|
-
return scope?.digest === digest2 && scope.revision === revision && scope.violation ? structuredClone(scope.violation) : undefined;
|
|
2430
|
-
}
|
|
2431
|
-
function confirmTaskAcceptanceImport(binding, validate) {
|
|
2432
|
-
const scope = scopes.getStore();
|
|
2433
|
-
if (scope)
|
|
2434
|
-
scope.imported = { binding: structuredClone(binding), validate };
|
|
2435
|
-
}
|
|
2436
|
-
async function evaluateTaskAcceptanceOutcome(state, ledger) {
|
|
2437
|
-
const current = ledger.revisions.at(-1), scope = scopes.getStore();
|
|
2438
|
-
const terminal = ["completed", "failed", "cancelled", "timed_out"].includes(state.status);
|
|
2439
|
-
if (!scope || scope.digest !== current.digest || scope.revision !== current.revision) {
|
|
2440
|
-
return { status: terminal ? "incomplete" : "pending", reason: "TASK_ACCEPTANCE_NO_DELIVERY_EVIDENCE" };
|
|
2441
|
-
}
|
|
2442
|
-
if (scope.violation)
|
|
2443
|
-
return { status: "failed", reason: scope.violation.code };
|
|
2444
|
-
if (state.status !== "completed")
|
|
2445
|
-
return { status: state.status === "failed" ? "failed" : terminal ? "incomplete" : "pending", reason: "TASK_ACCEPTANCE_RUN_NOT_COMPLETED" };
|
|
2446
|
-
if (state.finishReason !== "stop")
|
|
2447
|
-
return { status: "incomplete", reason: "TASK_ACCEPTANCE_RUN_NOT_FINISHED" };
|
|
2448
|
-
const imported = scope.imported;
|
|
2449
|
-
if (!imported || imported.binding.runId !== state.runId)
|
|
2450
|
-
return { status: "incomplete", reason: "TASK_ACCEPTANCE_IMPORT_UNCONFIRMED" };
|
|
2451
|
-
const delivery = { ...imported.binding };
|
|
2452
|
-
if (scope.checks.missing(delivery).length)
|
|
2453
|
-
return { status: "incomplete", reason: "TASK_ACCEPTANCE_CHECKS_MISSING", delivery };
|
|
2454
|
-
try {
|
|
2455
|
-
if (!await imported.validate())
|
|
2456
|
-
return { status: "incomplete", reason: "TASK_ACCEPTANCE_DELIVERY_DRIFT", delivery };
|
|
2457
|
-
} catch {
|
|
2458
|
-
return { status: "incomplete", reason: "TASK_ACCEPTANCE_DELIVERY_INSPECTION_FAILED", delivery };
|
|
2459
|
-
}
|
|
2460
|
-
return { status: current.contract.humanReview.length ? "pending_review" : "verified", reason: current.contract.humanReview.length ? "TASK_ACCEPTANCE_HUMAN_REVIEW_REQUIRED" : "TASK_ACCEPTANCE_VERIFIED", delivery };
|
|
2461
|
-
}
|
|
2462
|
-
async function taskAcceptanceOutcome(state, ledger) {
|
|
2463
|
-
const outcome = await evaluateTaskAcceptanceOutcome(state, ledger);
|
|
2464
|
-
const scope = scopes.getStore(), latest = ledger.revisions.at(-1);
|
|
2465
|
-
const delivery = scope?.digest === latest.digest && scope.revision === latest.revision ? scope.imported?.binding ?? scope.previousDelivery : undefined;
|
|
2466
|
-
return { ...outcome, ...delivery && delivery.runId === state.runId ? { delivery: structuredClone(delivery) } : {} };
|
|
2467
|
-
}
|
|
2468
|
-
|
|
2469
1788
|
// src/execution/acceptance-snapshot.ts
|
|
2470
|
-
import { createHash as
|
|
1789
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2471
1790
|
import { lstat as lstat4, readdir as readdir2 } from "node:fs/promises";
|
|
2472
|
-
import
|
|
1791
|
+
import path4 from "node:path";
|
|
2473
1792
|
async function acceptanceSnapshotDigest(root, maxBytes) {
|
|
2474
1793
|
const rows = [];
|
|
2475
1794
|
let bytes = 0;
|
|
2476
1795
|
async function visit(relative) {
|
|
2477
|
-
const names = await readdir2(
|
|
1796
|
+
const names = await readdir2(path4.join(root, relative));
|
|
2478
1797
|
names.sort();
|
|
2479
1798
|
for (const name of names) {
|
|
2480
1799
|
const item = relative ? relative + "/" + name : name;
|
|
2481
1800
|
if (rows.length >= 20000)
|
|
2482
1801
|
throw new Error("TASK_ACCEPTANCE_SNAPSHOT_CAPACITY");
|
|
2483
|
-
const absolute =
|
|
1802
|
+
const absolute = path4.join(root, item), entry = await lstat4(absolute);
|
|
2484
1803
|
if (entry.isSymbolicLink())
|
|
2485
1804
|
throw new Error("TASK_ACCEPTANCE_SNAPSHOT_UNSAFE");
|
|
2486
1805
|
if (entry.isDirectory()) {
|
|
@@ -2489,13 +1808,13 @@ async function acceptanceSnapshotDigest(root, maxBytes) {
|
|
|
2489
1808
|
} else if (entry.isFile() && entry.nlink === 1) {
|
|
2490
1809
|
const file = await readRegularFileNoFollow(absolute, { label: "Acceptance snapshot file", maxBytes: maxBytes - bytes, requireSingleLink: true });
|
|
2491
1810
|
bytes += file.contents.byteLength;
|
|
2492
|
-
rows.push([item, "file", file.stat.mode & 511,
|
|
1811
|
+
rows.push([item, "file", file.stat.mode & 511, createHash3("sha256").update(file.contents).digest("hex")]);
|
|
2493
1812
|
} else
|
|
2494
1813
|
throw new Error("TASK_ACCEPTANCE_SNAPSHOT_UNSAFE");
|
|
2495
1814
|
}
|
|
2496
1815
|
}
|
|
2497
1816
|
await visit("");
|
|
2498
|
-
return "sha256:" +
|
|
1817
|
+
return "sha256:" + createHash3("sha256").update(JSON.stringify(rows)).digest("hex");
|
|
2499
1818
|
}
|
|
2500
1819
|
|
|
2501
1820
|
// src/execution/oci-observability.ts
|
|
@@ -2519,9 +1838,9 @@ async function observeOciPhase(operation, action) {
|
|
|
2519
1838
|
}
|
|
2520
1839
|
|
|
2521
1840
|
// src/execution/execution-environment.ts
|
|
2522
|
-
import { createHash as
|
|
1841
|
+
import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
|
|
2523
1842
|
import { spawn as spawn2 } from "node:child_process";
|
|
2524
|
-
import { constants as
|
|
1843
|
+
import { constants as fsConstants } from "node:fs";
|
|
2525
1844
|
import {
|
|
2526
1845
|
chmod,
|
|
2527
1846
|
copyFile,
|
|
@@ -2533,7 +1852,7 @@ import {
|
|
|
2533
1852
|
rm,
|
|
2534
1853
|
writeFile
|
|
2535
1854
|
} from "node:fs/promises";
|
|
2536
|
-
import
|
|
1855
|
+
import path5 from "node:path";
|
|
2537
1856
|
import { createAgentExecutionEnvironmentBinding } from "@zhivex-ai/agents/beta";
|
|
2538
1857
|
var HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION = 1;
|
|
2539
1858
|
var HARNESS_OCI_LABEL = "com.zhivex.harness.execution";
|
|
@@ -2577,7 +1896,7 @@ var BUILT_IN_TOOL_NAMES = new Set([
|
|
|
2577
1896
|
"verify_and_apply_reviewed_edits",
|
|
2578
1897
|
"environment_status"
|
|
2579
1898
|
]);
|
|
2580
|
-
var
|
|
1899
|
+
var digest = (value) => `sha256:${createHash4("sha256").update(value).digest("hex")}`;
|
|
2581
1900
|
var executionScopeKey = (scope) => {
|
|
2582
1901
|
if (scope) {
|
|
2583
1902
|
for (const value of [scope.tenantId, scope.userId, scope.namespace]) {
|
|
@@ -2590,11 +1909,11 @@ var executionScopeKey = (scope) => {
|
|
|
2590
1909
|
}
|
|
2591
1910
|
return JSON.stringify([scope?.tenantId ?? null, scope?.userId ?? null, scope?.namespace ?? null]);
|
|
2592
1911
|
};
|
|
2593
|
-
var executionIdentity = (workspace, stateDirectory, runId, scopeKey) =>
|
|
2594
|
-
var runHash = (runId) =>
|
|
1912
|
+
var executionIdentity = (workspace, stateDirectory, runId, scopeKey) => digest(JSON.stringify(["scoped-oci-v2", workspace, stateDirectory, scopeKey, runId]));
|
|
1913
|
+
var runHash = (runId) => createHash4("sha256").update(runId).digest("hex").slice(0, 24);
|
|
2595
1914
|
var isInside2 = (root, candidate) => {
|
|
2596
|
-
const relative =
|
|
2597
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
1915
|
+
const relative = path5.relative(root, candidate);
|
|
1916
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path5.sep}`) && !path5.isAbsolute(relative);
|
|
2598
1917
|
};
|
|
2599
1918
|
var privateDirectory = async (directory) => {
|
|
2600
1919
|
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
@@ -2733,14 +2052,14 @@ var updateSnapshotMetadata = (target, kind, relative, entry) => {
|
|
|
2733
2052
|
var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspaceBytes, maxFileWriteBytes) => {
|
|
2734
2053
|
let entries = 0;
|
|
2735
2054
|
let totalBytes = 0;
|
|
2736
|
-
const seal =
|
|
2737
|
-
const metadata =
|
|
2055
|
+
const seal = createHash4("sha256");
|
|
2056
|
+
const metadata = createHash4("sha256");
|
|
2738
2057
|
const visit = async (directory, relativeDirectory = "") => {
|
|
2739
2058
|
const children = (await readdir3(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name));
|
|
2740
2059
|
for (const child of children) {
|
|
2741
2060
|
const name = child.name;
|
|
2742
2061
|
const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
|
2743
|
-
const absolute =
|
|
2062
|
+
const absolute = path5.join(directory, name);
|
|
2744
2063
|
if (!isInside2(stagedRoot, absolute) || relative.length > 1024) {
|
|
2745
2064
|
throw new Error(`OCI workspace export contains an invalid path: ${relative}.`);
|
|
2746
2065
|
}
|
|
@@ -2787,18 +2106,18 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
2787
2106
|
if (totalBytes > maxWorkspaceBytes) {
|
|
2788
2107
|
throw new Error(`OCI workspace export exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
|
|
2789
2108
|
}
|
|
2790
|
-
const contentDigest =
|
|
2109
|
+
const contentDigest = digest(contents);
|
|
2791
2110
|
updateSnapshotMetadata(metadata, "file", relative, entry);
|
|
2792
2111
|
seal.update(`file\x00${relative}\x00${entry.mode & 511}\x00${entry.size}\x00${contentDigest}\x00`);
|
|
2793
2112
|
if (entry.size > maxFileWriteBytes) {
|
|
2794
|
-
const previous =
|
|
2113
|
+
const previous = path5.join(previousRoot, ...relative.split("/"));
|
|
2795
2114
|
let unchanged = false;
|
|
2796
2115
|
try {
|
|
2797
2116
|
const previousFile = await readRegularFileNoFollow(previous, {
|
|
2798
2117
|
label: `Previous OCI workspace file ${relative}`,
|
|
2799
2118
|
maxBytes: entry.size
|
|
2800
2119
|
});
|
|
2801
|
-
unchanged = previousFile.stat.size === entry.size &&
|
|
2120
|
+
unchanged = previousFile.stat.size === entry.size && digest(previousFile.contents) === contentDigest;
|
|
2802
2121
|
} catch (error) {
|
|
2803
2122
|
if (error.code !== "ENOENT")
|
|
2804
2123
|
throw error;
|
|
@@ -2820,13 +2139,13 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
2820
2139
|
var snapshotMetadataFingerprint = async (root, maxWorkspaceBytes) => {
|
|
2821
2140
|
let entries = 0;
|
|
2822
2141
|
let totalBytes = 0;
|
|
2823
|
-
const metadata =
|
|
2142
|
+
const metadata = createHash4("sha256");
|
|
2824
2143
|
const visit = async (directory, relativeDirectory = "") => {
|
|
2825
2144
|
for (const name of (await readdir3(directory)).sort()) {
|
|
2826
2145
|
const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
|
2827
2146
|
if (relative === "node_modules")
|
|
2828
2147
|
continue;
|
|
2829
|
-
const absolute =
|
|
2148
|
+
const absolute = path5.join(directory, name);
|
|
2830
2149
|
if (!isInside2(root, absolute) || relative.length > 1024) {
|
|
2831
2150
|
throw new Error(`OCI workspace snapshot contains an invalid path: ${relative}.`);
|
|
2832
2151
|
}
|
|
@@ -2986,7 +2305,7 @@ ${result.stdout}`)) {
|
|
|
2986
2305
|
}
|
|
2987
2306
|
}
|
|
2988
2307
|
requestFingerprint(request) {
|
|
2989
|
-
return
|
|
2308
|
+
return digest(JSON.stringify({
|
|
2990
2309
|
runId: request.runId,
|
|
2991
2310
|
snapshotRoot: request.snapshotRoot,
|
|
2992
2311
|
dependencyRoot: request.dependencyRoot ?? null,
|
|
@@ -3105,7 +2424,7 @@ ${result.stdout}`)) {
|
|
|
3105
2424
|
await this.cli([
|
|
3106
2425
|
"cp",
|
|
3107
2426
|
"-a",
|
|
3108
|
-
`${request.snapshotRoot}${
|
|
2427
|
+
`${request.snapshotRoot}${path5.sep}.`,
|
|
3109
2428
|
`${name}:/workspace`
|
|
3110
2429
|
], 60000);
|
|
3111
2430
|
if (request.dependencyRoot) {
|
|
@@ -3237,7 +2556,7 @@ ${result.stdout}`)) {
|
|
|
3237
2556
|
};
|
|
3238
2557
|
}
|
|
3239
2558
|
const workspaceExportStartedAt = process.hrtime.bigint();
|
|
3240
|
-
const stagedRoot =
|
|
2559
|
+
const stagedRoot = path5.join(path5.dirname(request.snapshotRoot), `.workspace-sync-${randomUUID2()}`);
|
|
3241
2560
|
try {
|
|
3242
2561
|
await privateDirectory(stagedRoot);
|
|
3243
2562
|
await observeOciPhase("oci_export", () => this.cli(["cp", `${session.name}:/workspace/.`, stagedRoot], 60000));
|
|
@@ -3451,7 +2770,7 @@ var collectSnapshotInventory = async (workspace, maxWorkspaceBytes, metrics, max
|
|
|
3451
2770
|
if (metrics)
|
|
3452
2771
|
metrics.inventoryPages += 1;
|
|
3453
2772
|
for await (const candidates of boundedBatches(page.files, async (file) => {
|
|
3454
|
-
const absolute =
|
|
2773
|
+
const absolute = path5.join(workspace.root, ...file.path.split("/"));
|
|
3455
2774
|
if (!isInside2(workspace.root, absolute))
|
|
3456
2775
|
throw new Error(`Snapshot file escaped its workspace: ${file.path}.`);
|
|
3457
2776
|
if (await realpath3(absolute) !== absolute)
|
|
@@ -3472,7 +2791,7 @@ var collectSnapshotInventory = async (workspace, maxWorkspaceBytes, metrics, max
|
|
|
3472
2791
|
if (stable.stat.dev !== entry.dev || stable.stat.ino !== entry.ino || stable.stat.size !== entry.size || stable.stat.mtimeMs !== entry.mtimeMs || stable.stat.ctimeMs !== entry.ctimeMs) {
|
|
3473
2792
|
throw new Error(`Snapshot inventory file changed while being read: ${file.path}.`);
|
|
3474
2793
|
}
|
|
3475
|
-
return { path: file.path, digest:
|
|
2794
|
+
return { path: file.path, digest: digest(stable.contents), mode: stable.stat.mode & 511, bytes: stable.contents.byteLength };
|
|
3476
2795
|
}, 4))
|
|
3477
2796
|
for (const file of batch) {
|
|
3478
2797
|
if (files.size >= maxEntries)
|
|
@@ -3485,7 +2804,7 @@ var collectSnapshotInventory = async (workspace, maxWorkspaceBytes, metrics, max
|
|
|
3485
2804
|
return files;
|
|
3486
2805
|
};
|
|
3487
2806
|
var readSnapshotFile = async (workspace, expected, metrics, requireSingleLink = false) => {
|
|
3488
|
-
const absolute =
|
|
2807
|
+
const absolute = path5.join(workspace.root, ...expected.path.split("/"));
|
|
3489
2808
|
if (!isInside2(workspace.root, absolute)) {
|
|
3490
2809
|
throw new Error(`Snapshot file escaped its workspace: ${expected.path}.`);
|
|
3491
2810
|
}
|
|
@@ -3498,7 +2817,7 @@ var readSnapshotFile = async (workspace, expected, metrics, requireSingleLink =
|
|
|
3498
2817
|
});
|
|
3499
2818
|
const { contents } = file;
|
|
3500
2819
|
const entry = file.stat;
|
|
3501
|
-
const actualDigest =
|
|
2820
|
+
const actualDigest = digest(contents);
|
|
3502
2821
|
if (actualDigest !== expected.digest || contents.byteLength !== expected.bytes || (entry.mode & 511) !== expected.mode) {
|
|
3503
2822
|
throw new Error(`Snapshot source changed after inventory: ${expected.path}.`);
|
|
3504
2823
|
}
|
|
@@ -3510,7 +2829,7 @@ var readSnapshotFile = async (workspace, expected, metrics, requireSingleLink =
|
|
|
3510
2829
|
};
|
|
3511
2830
|
var copySnapshotFile = async (source, target, metrics) => {
|
|
3512
2831
|
try {
|
|
3513
|
-
await copyFile(source, target,
|
|
2832
|
+
await copyFile(source, target, fsConstants.COPYFILE_FICLONE_FORCE);
|
|
3514
2833
|
} catch (error) {
|
|
3515
2834
|
const code = error.code;
|
|
3516
2835
|
if (!code || !COPY_ON_WRITE_UNSUPPORTED_CODES.has(code))
|
|
@@ -3532,11 +2851,11 @@ var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceB
|
|
|
3532
2851
|
}
|
|
3533
2852
|
for await (const _batch of boundedBatches([...files.values()], async (expected) => {
|
|
3534
2853
|
const file = await readSnapshotFile(source, expected, metrics);
|
|
3535
|
-
const baseTarget =
|
|
3536
|
-
const snapshotTarget =
|
|
2854
|
+
const baseTarget = path5.join(baseRoot, ...file.path.split("/"));
|
|
2855
|
+
const snapshotTarget = path5.join(snapshotRoot, ...file.path.split("/"));
|
|
3537
2856
|
await Promise.all([
|
|
3538
|
-
mkdir3(
|
|
3539
|
-
mkdir3(
|
|
2857
|
+
mkdir3(path5.dirname(baseTarget), { recursive: true, mode: 448 }),
|
|
2858
|
+
mkdir3(path5.dirname(snapshotTarget), { recursive: true, mode: 448 })
|
|
3540
2859
|
]);
|
|
3541
2860
|
await writeFile(baseTarget, file.contents, { mode: file.mode });
|
|
3542
2861
|
await chmod(baseTarget, file.mode);
|
|
@@ -3552,7 +2871,7 @@ var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceB
|
|
|
3552
2871
|
var deliveryPaths = (value) => {
|
|
3553
2872
|
if (value === undefined)
|
|
3554
2873
|
return [];
|
|
3555
|
-
if (!Array.isArray(value) || value.length > MAX_SYNC_ENTRIES || value.some((item) => typeof item !== "string" || !item || item.length > 8192 || item.includes("\\") || item.includes("\x00") ||
|
|
2874
|
+
if (!Array.isArray(value) || value.length > MAX_SYNC_ENTRIES || value.some((item) => typeof item !== "string" || !item || item.length > 8192 || item.includes("\\") || item.includes("\x00") || path5.posix.isAbsolute(item) || item.split("/").some((segment) => !segment || segment === "." || segment === "..")) || Buffer.byteLength(JSON.stringify(value)) > 512 * 1024) {
|
|
3556
2875
|
throw new Error("Invalid or oversized OCI delivery path history.");
|
|
3557
2876
|
}
|
|
3558
2877
|
return [...new Set(value)];
|
|
@@ -3613,7 +2932,7 @@ var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes, max
|
|
|
3613
2932
|
throw new Error(`Environment patch exceeds the ${MAX_EDIT_PROPOSAL_BYTES}-byte import limit.`);
|
|
3614
2933
|
}
|
|
3615
2934
|
const payload = patchPayload(runId, entries);
|
|
3616
|
-
return { entries, totalBytes, patchId:
|
|
2935
|
+
return { entries, totalBytes, patchId: digest(JSON.stringify({ ...payload, executionIdentity: identity ?? null })) };
|
|
3617
2936
|
};
|
|
3618
2937
|
var inspectHostPrecondition = async (workspace, filePath, expectedDigest, expectedMode) => {
|
|
3619
2938
|
try {
|
|
@@ -3745,7 +3064,7 @@ var harnessExecutionSession = (context) => {
|
|
|
3745
3064
|
var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
3746
3065
|
const runtime = options.runtime ?? new CliOciRuntimeAdapter(options.config.runtime);
|
|
3747
3066
|
const image = await runtime.inspectImage(options.config.image);
|
|
3748
|
-
const workspaceIdentity =
|
|
3067
|
+
const workspaceIdentity = digest(options.workspace.root);
|
|
3749
3068
|
const manifest = {
|
|
3750
3069
|
schemaVersion: 1,
|
|
3751
3070
|
id: `zhivex-harness-${options.config.runtime}-oci`,
|
|
@@ -3800,7 +3119,7 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3800
3119
|
}
|
|
3801
3120
|
};
|
|
3802
3121
|
const binding = createAgentExecutionEnvironmentBinding(manifest);
|
|
3803
|
-
const environmentRoot =
|
|
3122
|
+
const environmentRoot = path5.join(options.stateDirectory, "environments");
|
|
3804
3123
|
const reviewSnapshot = async (request, allowAbsent = false) => {
|
|
3805
3124
|
let stateDirectory;
|
|
3806
3125
|
try {
|
|
@@ -3812,9 +3131,9 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3812
3131
|
}
|
|
3813
3132
|
const scopeKey = executionScopeKey(request.scope);
|
|
3814
3133
|
const identity = executionIdentity(options.workspace.root, stateDirectory, request.runId, scopeKey);
|
|
3815
|
-
const root =
|
|
3816
|
-
const directory =
|
|
3817
|
-
for (const candidate of [root, directory,
|
|
3134
|
+
const root = path5.join(stateDirectory, "environments");
|
|
3135
|
+
const directory = path5.join(root, runHash(identity));
|
|
3136
|
+
for (const candidate of [root, directory, path5.join(directory, "base"), path5.join(directory, "workspace")]) {
|
|
3818
3137
|
let entry;
|
|
3819
3138
|
try {
|
|
3820
3139
|
entry = await lstat5(candidate);
|
|
@@ -3827,7 +3146,7 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3827
3146
|
throw new Error("Unsafe execution review directory.");
|
|
3828
3147
|
}
|
|
3829
3148
|
}
|
|
3830
|
-
const file = await readRegularFileNoFollow(
|
|
3149
|
+
const file = await readRegularFileNoFollow(path5.join(directory, "environment.json"), {
|
|
3831
3150
|
label: "Execution metadata",
|
|
3832
3151
|
maxBytes: 1024 * 1024
|
|
3833
3152
|
});
|
|
@@ -3835,8 +3154,8 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3835
3154
|
if (metadata.schemaVersion !== HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION || metadata.runId !== request.runId || metadata.scopeKey !== scopeKey || metadata.stateDirectory !== stateDirectory || metadata.executionIdentity !== identity || metadata.hostWorkspace !== options.workspace.root || metadata.binding.fingerprint !== binding.fingerprint || metadata.binding.workspaceId !== binding.workspaceId) {
|
|
3836
3155
|
throw Object.assign(new Error("Execution review binding changed."), { diagnosticCode: "OCI_EXECUTION_BINDING_CHANGED" });
|
|
3837
3156
|
}
|
|
3838
|
-
const base = await Workspace.open(
|
|
3839
|
-
const current = await Workspace.open(
|
|
3157
|
+
const base = await Workspace.open(path5.join(directory, "base"));
|
|
3158
|
+
const current = await Workspace.open(path5.join(directory, "workspace"));
|
|
3840
3159
|
const patch = await createEnvironmentPatch(request.runId, base, current, options.config.maxFileWriteBytes, options.config.maxWorkspaceBytes, undefined, identity);
|
|
3841
3160
|
deliveryPaths(metadata.deliveryPaths);
|
|
3842
3161
|
return { metadata, patch, current };
|
|
@@ -3895,11 +3214,11 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3895
3214
|
const stateDirectory = await realpath3(options.stateDirectory);
|
|
3896
3215
|
const scopeKey = executionScopeKey(request.scope);
|
|
3897
3216
|
const identity = executionIdentity(options.workspace.root, stateDirectory, request.runId, scopeKey);
|
|
3898
|
-
const directory =
|
|
3217
|
+
const directory = path5.join(environmentRoot, runHash(identity));
|
|
3899
3218
|
await privateDirectory(directory);
|
|
3900
|
-
const metadataPath =
|
|
3901
|
-
const baseRoot =
|
|
3902
|
-
const snapshotRoot =
|
|
3219
|
+
const metadataPath = path5.join(directory, "environment.json");
|
|
3220
|
+
const baseRoot = path5.join(directory, "base");
|
|
3221
|
+
const snapshotRoot = path5.join(directory, "workspace");
|
|
3903
3222
|
let metadata;
|
|
3904
3223
|
try {
|
|
3905
3224
|
const metadataFile = await readRegularFileNoFollow(metadataPath, {
|
|
@@ -3914,7 +3233,7 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3914
3233
|
if (error.code !== "ENOENT")
|
|
3915
3234
|
throw error;
|
|
3916
3235
|
try {
|
|
3917
|
-
await lstat5(
|
|
3236
|
+
await lstat5(path5.join(environmentRoot, runHash(request.runId)));
|
|
3918
3237
|
throw new Error("Unscoped legacy OCI artifact requires its original runtime; start a new run.");
|
|
3919
3238
|
} catch (legacyError) {
|
|
3920
3239
|
if (legacyError.code !== "ENOENT")
|
|
@@ -3941,9 +3260,9 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3941
3260
|
delete metadata.releasedAt;
|
|
3942
3261
|
delete metadata.status;
|
|
3943
3262
|
await atomicJson(metadataPath, metadata);
|
|
3944
|
-
const workspace = await Workspace.open(snapshotRoot, { auditPath:
|
|
3263
|
+
const workspace = await Workspace.open(snapshotRoot, { auditPath: path5.join(directory, "mutation-audit.json") });
|
|
3945
3264
|
const base = await Workspace.open(baseRoot);
|
|
3946
|
-
const dependencyPath =
|
|
3265
|
+
const dependencyPath = path5.join(options.workspace.root, "node_modules");
|
|
3947
3266
|
let dependencyRoot;
|
|
3948
3267
|
try {
|
|
3949
3268
|
const entry = await lstat5(dependencyPath);
|
|
@@ -4022,7 +3341,7 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
4022
3341
|
});
|
|
4023
3342
|
for (const check of matching)
|
|
4024
3343
|
if (check.kind === "package-script") {
|
|
4025
|
-
const file = await readRegularFileNoFollow(
|
|
3344
|
+
const file = await readRegularFileNoFollow(path5.join(workspace.root, "package.json"), { label: "Acceptance package manifest", maxBytes: 256 * 1024, requireSingleLink: true });
|
|
4026
3345
|
const manifestDocument = JSON.parse(file.contents.toString("utf8"));
|
|
4027
3346
|
const resolved = await resolvePackageCheckCommand(workspace.root, manifestDocument, check.script, check.expectedScript, [check.script]);
|
|
4028
3347
|
if (JSON.stringify(resolved.command) !== JSON.stringify(argv))
|
|
@@ -4157,7 +3476,7 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
4157
3476
|
async runCheck(check, expectedScript, allowedChecks, context) {
|
|
4158
3477
|
let manifestDocument;
|
|
4159
3478
|
try {
|
|
4160
|
-
const manifestFile = await readRegularFileNoFollow(
|
|
3479
|
+
const manifestFile = await readRegularFileNoFollow(path5.join(snapshotRoot, "package.json"), {
|
|
4161
3480
|
label: "Snapshot package.json",
|
|
4162
3481
|
maxBytes: 1024 * 1024
|
|
4163
3482
|
});
|
|
@@ -4261,7 +3580,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
4261
3580
|
}
|
|
4262
3581
|
};
|
|
4263
3582
|
var cleanupHarnessExecutionArtifacts = async (stateDirectory, before, selection) => {
|
|
4264
|
-
const root =
|
|
3583
|
+
const root = path5.join(stateDirectory, "environments");
|
|
4265
3584
|
const result = { scanned: 0, deleted: 0, skipped: 0 };
|
|
4266
3585
|
let entries;
|
|
4267
3586
|
try {
|
|
@@ -4279,7 +3598,7 @@ var cleanupHarnessExecutionArtifacts = async (stateDirectory, before, selection)
|
|
|
4279
3598
|
continue;
|
|
4280
3599
|
}
|
|
4281
3600
|
result.scanned += 1;
|
|
4282
|
-
const directory =
|
|
3601
|
+
const directory = path5.join(root, entry.name);
|
|
4283
3602
|
try {
|
|
4284
3603
|
const resolved = await realpath3(directory);
|
|
4285
3604
|
if (!isInside2(root, resolved))
|
|
@@ -4288,7 +3607,7 @@ var cleanupHarnessExecutionArtifacts = async (stateDirectory, before, selection)
|
|
|
4288
3607
|
if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
|
|
4289
3608
|
throw new Error("unsafe execution artifact directory");
|
|
4290
3609
|
}
|
|
4291
|
-
const metadataPath =
|
|
3610
|
+
const metadataPath = path5.join(directory, "environment.json");
|
|
4292
3611
|
const metadataFile = await readRegularFileNoFollow(metadataPath, {
|
|
4293
3612
|
label: "Execution cleanup metadata",
|
|
4294
3613
|
maxBytes: 1024 * 1024
|
|
@@ -4307,7 +3626,7 @@ var cleanupHarnessExecutionArtifacts = async (stateDirectory, before, selection)
|
|
|
4307
3626
|
result.skipped += 1;
|
|
4308
3627
|
continue;
|
|
4309
3628
|
}
|
|
4310
|
-
const stagedDirectory =
|
|
3629
|
+
const stagedDirectory = path5.join(root, `.cleanup-${artifactName}-${randomUUID2()}`);
|
|
4311
3630
|
let staged = false;
|
|
4312
3631
|
try {
|
|
4313
3632
|
await rename2(directory, stagedDirectory);
|
|
@@ -4316,7 +3635,7 @@ var cleanupHarnessExecutionArtifacts = async (stateDirectory, before, selection)
|
|
|
4316
3635
|
if (movedEntry.isSymbolicLink() || !movedEntry.isDirectory() || movedEntry.dev !== directoryEntry.dev || movedEntry.ino !== directoryEntry.ino) {
|
|
4317
3636
|
throw new Error("execution artifact directory changed before cleanup");
|
|
4318
3637
|
}
|
|
4319
|
-
const recheckedMetadata = await readRegularFileNoFollow(
|
|
3638
|
+
const recheckedMetadata = await readRegularFileNoFollow(path5.join(stagedDirectory, "environment.json"), { label: "Execution cleanup metadata", maxBytes: 1024 * 1024 });
|
|
4320
3639
|
if (!recheckedMetadata.contents.equals(metadataFile.contents)) {
|
|
4321
3640
|
throw new Error("execution artifact metadata changed before cleanup");
|
|
4322
3641
|
}
|
|
@@ -4343,4 +3662,4 @@ var executionFingerprintInput = (execution, environment) => execution.backend ==
|
|
|
4343
3662
|
};
|
|
4344
3663
|
var describeOciCommand = (command, args) => commandDisplay([command, ...args]);
|
|
4345
3664
|
|
|
4346
|
-
export {
|
|
3665
|
+
export { withWorkspaceMutation, EnvironmentPatchDriftError, boundedBatches, resolvePackageManager, resolvePackageCheckCommand, replacementEditSchema, runPortableProcess, isHardIgnored, Workspace, HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION, HARNESS_OCI_LABEL, HARNESS_OCI_LABEL_VALUE, CliOciRuntimeAdapter, collectSnapshotInventory, readSnapshotFile, harnessExecutionSession, createHarnessOciExecutionEnvironment, cleanupHarnessExecutionArtifacts, executionFingerprintInput, describeOciCommand };
|