@zhivex-ai/harness 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -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 +36 -36
- package/dist/acp-cli.js +54 -20
- package/dist/approvals/approval-diff.d.ts +2 -1
- package/dist/approvals/approval-preview.d.ts +1 -1
- 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-kvw7ng34.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-entry-75j3r49b.js +484 -0
- package/dist/cli-entry-928p8h41.js +10067 -0
- package/dist/cli-entry-h2yc9x37.js +10350 -0
- package/dist/{cli-entry-20w6nh0k.js → cli-entry-hdtfef27.js} +439 -19
- package/dist/{cli-entry-w0hterjs.js → cli-entry-q8bgkn9m.js} +40 -13
- package/dist/{cli-2h37qyvg.js → cli-vyrytn3m.js} +253 -644
- 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 +1232 -564
- package/dist/{index-8zjmnyw5.js → index-1265j7t2.js} +73 -6
- package/dist/{index-nmjdwd4p.js → index-4gt045j0.js} +109 -1
- package/dist/{index-pdmevfxv.js → index-5zbq9ztj.js} +5 -259
- package/dist/{index-6dft2vev.js → index-63gbp0cz.js} +5 -3
- package/dist/index-6dxrbnxm.js +719 -0
- package/dist/{index-pp474137.js → index-6fp7kdpx.js} +769 -933
- package/dist/{index-61gthdz6.js → index-8sm2a1wa.js} +1 -1
- package/dist/{index-rkbf7dqg.js → index-96ezrvg2.js} +57 -13
- package/dist/index-bt2p8x7r.js +329 -0
- package/dist/index-c39197wq.js +552 -0
- package/dist/{index-m82cfspw.js → index-eps3c3g6.js} +877 -963
- package/dist/{index-s2k2edts.js → index-fqqz1yt8.js} +110 -1544
- package/dist/{index-m34q25jj.js → index-ggstwr0v.js} +192 -838
- package/dist/index-jpdvn09j.js +490 -0
- package/dist/{index-xqxeypt5.js → index-mzydbzp8.js} +76 -4
- package/dist/index-pq3kz1r8.js +269 -0
- package/dist/{index-wg1sd3at.js → index-qfsrrnz6.js} +1 -1
- package/dist/{index-937cb7y8.js → index-vc7yxz50.js} +561 -35
- package/dist/{index-5jhgatga.js → index-vvzc5ezz.js} +7 -6
- package/dist/index-wwthv10a.js +1507 -0
- package/dist/{index-tfzta544.js → index-xfedewpt.js} +17 -9
- package/dist/index-yx52bt4a.js +511 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +93 -77
- package/dist/integrations/mcp-stdio-tools.d.ts +4 -1
- 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/budget-ledger-scope.d.ts +14 -0
- 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-v6qptg9x.js} +400 -305
- package/dist/tools/workspace.d.ts +2 -2
- package/dist/verify-historical-migrations.js +1237 -566
- package/dist/workspace/edit-contracts.d.ts +2 -2
- package/dist/workspace/path-policy.d.ts +2 -1
- package/dist/workspace/workspace.d.ts +13 -0
- 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 +59 -7
- 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 +8 -4
- package/docs/FIRST_USE.md +6 -6
- package/docs/MINIMUM_TASK_AUTHORITY.md +3 -3
- package/docs/PROJECT_MEMORY.md +1 -1
- package/docs/PUBLIC_SECURITY.md +14 -1
- package/docs/README.md +6 -1
- package/docs/REPOSITORY_EDITING.md +1 -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/TOOL_POLICY.md +4 -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 +7 -4
- 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 +19 -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-6dxrbnxm.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,17 +399,17 @@ 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"]);
|
|
896
408
|
var isSensitiveName = (name) => {
|
|
897
|
-
const normalized = name.
|
|
409
|
+
const normalized = name.toLowerCase();
|
|
898
410
|
return normalized === ".env" || normalized.startsWith(".env.") && normalized !== ".env.example" || normalized === ".npmrc" || normalized === "id_rsa" || normalized === "id_ed25519" || [".key", ".pem", ".p12", ".pfx"].some((extension) => normalized.endsWith(extension));
|
|
899
411
|
};
|
|
900
|
-
var isHardIgnored = (relativePath) => relativePath.split("/").some((segment) => HARD_IGNORES.has(segment.
|
|
412
|
+
var isHardIgnored = (relativePath) => relativePath.split("/").some((segment) => HARD_IGNORES.has(segment.toLowerCase()) || isSensitiveName(segment));
|
|
901
413
|
|
|
902
414
|
// src/workspace/workspace.ts
|
|
903
415
|
var IGNORE_FILES = [".gitignore", ".zhivex-harnessignore"];
|
|
@@ -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,12 +684,12 @@ 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
|
-
const blocked = relative.split("/").find((segment) => HARD_IGNORES.has(segment.
|
|
692
|
+
const blocked = relative.split("/").find((segment) => HARD_IGNORES.has(segment.toLowerCase()) || isSensitiveName(segment));
|
|
1181
693
|
throw new HarnessWorkspaceError(`The path is protected by the harness policy: ${blocked}`);
|
|
1182
694
|
}
|
|
1183
695
|
return candidate;
|
|
@@ -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,41 +1665,76 @@ 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();
|
|
2160
1672
|
const audit = await this.audit({ operation: "quarantine", path: parsed.path, beforeDigest: source.digest, quarantineId });
|
|
2161
1673
|
return { quarantineId, path: parsed.path, digest: source.digest, audit };
|
|
2162
1674
|
}
|
|
2163
|
-
async
|
|
2164
|
-
return withWorkspaceMutation(this.root, () => this.restoreQuarantinedLocked(input));
|
|
2165
|
-
}
|
|
2166
|
-
async restoreQuarantinedLocked(input) {
|
|
2167
|
-
const parsed = restoreFileInputSchema.parse(input);
|
|
2168
|
-
this.checkAuditCapacity();
|
|
1675
|
+
async readRestorableQuarantine(quarantineId) {
|
|
2169
1676
|
const directory = await this.secureQuarantineDirectory(false);
|
|
2170
1677
|
let manifest;
|
|
2171
1678
|
try {
|
|
2172
|
-
const manifestFile = await this.readInternalRegularFile(
|
|
1679
|
+
const manifestFile = await this.readInternalRegularFile(path3.join(directory, `${quarantineId}.json`), "Quarantine manifest", 64 * 1024);
|
|
2173
1680
|
manifest = JSON.parse(manifestFile.contents.toString("utf8"));
|
|
2174
1681
|
} catch {
|
|
2175
|
-
throw new HarnessWorkspaceError(`Quarantine entry was not found: ${
|
|
1682
|
+
throw new HarnessWorkspaceError(`Quarantine entry was not found: ${quarantineId}.`);
|
|
2176
1683
|
}
|
|
2177
|
-
if (manifest.schemaVersion !== 1 || manifest.quarantineId !==
|
|
2178
|
-
throw new HarnessWorkspaceError(`Quarantine entry is not restorable: ${
|
|
1684
|
+
if (manifest.schemaVersion !== 1 || manifest.quarantineId !== quarantineId || manifest.status !== "quarantined") {
|
|
1685
|
+
throw new HarnessWorkspaceError(`Quarantine entry is not restorable: ${quarantineId}.`);
|
|
2179
1686
|
}
|
|
2180
|
-
const dataPath =
|
|
1687
|
+
const dataPath = path3.join(directory, `${quarantineId}.data`);
|
|
2181
1688
|
const dataFile = await this.readInternalRegularFile(dataPath, "Quarantine payload", MAX_FILE_BYTES);
|
|
2182
|
-
const
|
|
2183
|
-
if (
|
|
1689
|
+
const digest = digestBytes(dataFile.contents);
|
|
1690
|
+
if (digest !== manifest.digest) {
|
|
1691
|
+
throw new HarnessWorkspaceError(`Quarantine digest mismatch: ${quarantineId}.`);
|
|
1692
|
+
}
|
|
1693
|
+
return { manifest, dataPath, dataFile, digest };
|
|
1694
|
+
}
|
|
1695
|
+
async previewRestore(input) {
|
|
1696
|
+
const parsed = restoreFileInputSchema.parse(input);
|
|
1697
|
+
const quarantined = await this.readRestorableQuarantine(parsed.quarantineId);
|
|
1698
|
+
if (parsed.expectedDigest !== quarantined.digest) {
|
|
2184
1699
|
throw new HarnessWorkspaceError(`Quarantine digest mismatch: ${parsed.quarantineId}.`);
|
|
2185
1700
|
}
|
|
2186
|
-
const
|
|
1701
|
+
const destination = await this.safePath(parsed.destination, { allowMissing: true });
|
|
1702
|
+
if (destination.exists)
|
|
1703
|
+
throw new HarnessWorkspaceError(`Restore destination already exists: ${parsed.destination}.`);
|
|
1704
|
+
let after;
|
|
1705
|
+
try {
|
|
1706
|
+
after = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(quarantined.dataFile.contents);
|
|
1707
|
+
} catch {
|
|
1708
|
+
throw new HarnessWorkspaceError("Preview requires valid UTF-8 text.");
|
|
1709
|
+
}
|
|
1710
|
+
return {
|
|
1711
|
+
proposalId: parsed.expectedDigest,
|
|
1712
|
+
files: [{
|
|
1713
|
+
path: parsed.destination,
|
|
1714
|
+
expectedDigest: parsed.expectedDigest,
|
|
1715
|
+
before: null,
|
|
1716
|
+
after,
|
|
1717
|
+
afterDigest: quarantined.digest,
|
|
1718
|
+
operation: "restore"
|
|
1719
|
+
}]
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
async restoreQuarantined(input) {
|
|
1723
|
+
return withWorkspaceMutation(this.root, () => this.restoreQuarantinedLocked(input));
|
|
1724
|
+
}
|
|
1725
|
+
async restoreQuarantinedLocked(input) {
|
|
1726
|
+
const parsed = restoreFileInputSchema.parse(input);
|
|
1727
|
+
this.checkAuditCapacity();
|
|
1728
|
+
const quarantined = await this.readRestorableQuarantine(parsed.quarantineId);
|
|
1729
|
+
if (parsed.expectedDigest !== quarantined.digest) {
|
|
1730
|
+
throw new HarnessWorkspaceError(`Quarantine digest mismatch: ${parsed.quarantineId}.`);
|
|
1731
|
+
}
|
|
1732
|
+
const { manifest, dataPath, dataFile, digest } = quarantined;
|
|
1733
|
+
const destinationPath = parsed.destination;
|
|
2187
1734
|
const destination = await this.safePath(destinationPath, { allowMissing: true });
|
|
2188
1735
|
if (destination.exists)
|
|
2189
1736
|
throw new HarnessWorkspaceError(`Restore destination already exists: ${destinationPath}.`);
|
|
2190
|
-
await this.secureWorkspaceDirectory(
|
|
1737
|
+
await this.secureWorkspaceDirectory(path3.dirname(destination.path));
|
|
2191
1738
|
const destinationRecheck = await this.safePath(destinationPath, { allowMissing: true });
|
|
2192
1739
|
if (destinationRecheck.exists)
|
|
2193
1740
|
throw new HarnessWorkspaceError(`Restore destination already exists: ${destinationPath}.`);
|
|
@@ -2197,7 +1744,7 @@ class Workspace {
|
|
|
2197
1744
|
}
|
|
2198
1745
|
await link(dataPath, destination.path);
|
|
2199
1746
|
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) !==
|
|
1747
|
+
if (linkedDestination.stat.dev !== dataFile.stat.dev || linkedDestination.stat.ino !== dataFile.stat.ino || digestBytes(linkedDestination.contents) !== digest) {
|
|
2201
1748
|
await unlink(destination.path).catch(() => {});
|
|
2202
1749
|
throw new HarnessWorkspaceError("Restore destination did not match the quarantined payload.");
|
|
2203
1750
|
}
|
|
@@ -2230,8 +1777,8 @@ class Workspace {
|
|
|
2230
1777
|
throw rollbackErrors.length ? new AggregateError([error, ...rollbackErrors], "Restore failed and rollback was incomplete.") : error;
|
|
2231
1778
|
}
|
|
2232
1779
|
this.invalidateWorkspaceIndex();
|
|
2233
|
-
const audit = await this.audit({ operation: "restore", path: destinationPath, afterDigest:
|
|
2234
|
-
return { quarantineId: parsed.quarantineId, path: destinationPath, digest
|
|
1780
|
+
const audit = await this.audit({ operation: "restore", path: destinationPath, afterDigest: digest, quarantineId: parsed.quarantineId });
|
|
1781
|
+
return { quarantineId: parsed.quarantineId, path: destinationPath, digest, audit };
|
|
2235
1782
|
}
|
|
2236
1783
|
async runCheck(check, expectedScript, allowedChecks = DEFAULT_CHECKS) {
|
|
2237
1784
|
let manifest;
|
|
@@ -2273,214 +1820,21 @@ class EnvironmentPatchDriftError extends Error {
|
|
|
2273
1820
|
}
|
|
2274
1821
|
}
|
|
2275
1822
|
|
|
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
1823
|
// src/execution/acceptance-snapshot.ts
|
|
2470
|
-
import { createHash as
|
|
1824
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2471
1825
|
import { lstat as lstat4, readdir as readdir2 } from "node:fs/promises";
|
|
2472
|
-
import
|
|
1826
|
+
import path4 from "node:path";
|
|
2473
1827
|
async function acceptanceSnapshotDigest(root, maxBytes) {
|
|
2474
1828
|
const rows = [];
|
|
2475
1829
|
let bytes = 0;
|
|
2476
1830
|
async function visit(relative) {
|
|
2477
|
-
const names = await readdir2(
|
|
1831
|
+
const names = await readdir2(path4.join(root, relative));
|
|
2478
1832
|
names.sort();
|
|
2479
1833
|
for (const name of names) {
|
|
2480
1834
|
const item = relative ? relative + "/" + name : name;
|
|
2481
1835
|
if (rows.length >= 20000)
|
|
2482
1836
|
throw new Error("TASK_ACCEPTANCE_SNAPSHOT_CAPACITY");
|
|
2483
|
-
const absolute =
|
|
1837
|
+
const absolute = path4.join(root, item), entry = await lstat4(absolute);
|
|
2484
1838
|
if (entry.isSymbolicLink())
|
|
2485
1839
|
throw new Error("TASK_ACCEPTANCE_SNAPSHOT_UNSAFE");
|
|
2486
1840
|
if (entry.isDirectory()) {
|
|
@@ -2489,13 +1843,13 @@ async function acceptanceSnapshotDigest(root, maxBytes) {
|
|
|
2489
1843
|
} else if (entry.isFile() && entry.nlink === 1) {
|
|
2490
1844
|
const file = await readRegularFileNoFollow(absolute, { label: "Acceptance snapshot file", maxBytes: maxBytes - bytes, requireSingleLink: true });
|
|
2491
1845
|
bytes += file.contents.byteLength;
|
|
2492
|
-
rows.push([item, "file", file.stat.mode & 511,
|
|
1846
|
+
rows.push([item, "file", file.stat.mode & 511, createHash3("sha256").update(file.contents).digest("hex")]);
|
|
2493
1847
|
} else
|
|
2494
1848
|
throw new Error("TASK_ACCEPTANCE_SNAPSHOT_UNSAFE");
|
|
2495
1849
|
}
|
|
2496
1850
|
}
|
|
2497
1851
|
await visit("");
|
|
2498
|
-
return "sha256:" +
|
|
1852
|
+
return "sha256:" + createHash3("sha256").update(JSON.stringify(rows)).digest("hex");
|
|
2499
1853
|
}
|
|
2500
1854
|
|
|
2501
1855
|
// src/execution/oci-observability.ts
|
|
@@ -2519,9 +1873,9 @@ async function observeOciPhase(operation, action) {
|
|
|
2519
1873
|
}
|
|
2520
1874
|
|
|
2521
1875
|
// src/execution/execution-environment.ts
|
|
2522
|
-
import { createHash as
|
|
1876
|
+
import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
|
|
2523
1877
|
import { spawn as spawn2 } from "node:child_process";
|
|
2524
|
-
import { constants as
|
|
1878
|
+
import { constants as fsConstants } from "node:fs";
|
|
2525
1879
|
import {
|
|
2526
1880
|
chmod,
|
|
2527
1881
|
copyFile,
|
|
@@ -2533,7 +1887,7 @@ import {
|
|
|
2533
1887
|
rm,
|
|
2534
1888
|
writeFile
|
|
2535
1889
|
} from "node:fs/promises";
|
|
2536
|
-
import
|
|
1890
|
+
import path5 from "node:path";
|
|
2537
1891
|
import { createAgentExecutionEnvironmentBinding } from "@zhivex-ai/agents/beta";
|
|
2538
1892
|
var HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION = 1;
|
|
2539
1893
|
var HARNESS_OCI_LABEL = "com.zhivex.harness.execution";
|
|
@@ -2577,7 +1931,7 @@ var BUILT_IN_TOOL_NAMES = new Set([
|
|
|
2577
1931
|
"verify_and_apply_reviewed_edits",
|
|
2578
1932
|
"environment_status"
|
|
2579
1933
|
]);
|
|
2580
|
-
var
|
|
1934
|
+
var digest = (value) => `sha256:${createHash4("sha256").update(value).digest("hex")}`;
|
|
2581
1935
|
var executionScopeKey = (scope) => {
|
|
2582
1936
|
if (scope) {
|
|
2583
1937
|
for (const value of [scope.tenantId, scope.userId, scope.namespace]) {
|
|
@@ -2590,11 +1944,11 @@ var executionScopeKey = (scope) => {
|
|
|
2590
1944
|
}
|
|
2591
1945
|
return JSON.stringify([scope?.tenantId ?? null, scope?.userId ?? null, scope?.namespace ?? null]);
|
|
2592
1946
|
};
|
|
2593
|
-
var executionIdentity = (workspace, stateDirectory, runId, scopeKey) =>
|
|
2594
|
-
var runHash = (runId) =>
|
|
1947
|
+
var executionIdentity = (workspace, stateDirectory, runId, scopeKey) => digest(JSON.stringify(["scoped-oci-v2", workspace, stateDirectory, scopeKey, runId]));
|
|
1948
|
+
var runHash = (runId) => createHash4("sha256").update(runId).digest("hex").slice(0, 24);
|
|
2595
1949
|
var isInside2 = (root, candidate) => {
|
|
2596
|
-
const relative =
|
|
2597
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
1950
|
+
const relative = path5.relative(root, candidate);
|
|
1951
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path5.sep}`) && !path5.isAbsolute(relative);
|
|
2598
1952
|
};
|
|
2599
1953
|
var privateDirectory = async (directory) => {
|
|
2600
1954
|
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
@@ -2733,14 +2087,14 @@ var updateSnapshotMetadata = (target, kind, relative, entry) => {
|
|
|
2733
2087
|
var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspaceBytes, maxFileWriteBytes) => {
|
|
2734
2088
|
let entries = 0;
|
|
2735
2089
|
let totalBytes = 0;
|
|
2736
|
-
const seal =
|
|
2737
|
-
const metadata =
|
|
2090
|
+
const seal = createHash4("sha256");
|
|
2091
|
+
const metadata = createHash4("sha256");
|
|
2738
2092
|
const visit = async (directory, relativeDirectory = "") => {
|
|
2739
2093
|
const children = (await readdir3(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name));
|
|
2740
2094
|
for (const child of children) {
|
|
2741
2095
|
const name = child.name;
|
|
2742
2096
|
const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
|
2743
|
-
const absolute =
|
|
2097
|
+
const absolute = path5.join(directory, name);
|
|
2744
2098
|
if (!isInside2(stagedRoot, absolute) || relative.length > 1024) {
|
|
2745
2099
|
throw new Error(`OCI workspace export contains an invalid path: ${relative}.`);
|
|
2746
2100
|
}
|
|
@@ -2787,18 +2141,18 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
2787
2141
|
if (totalBytes > maxWorkspaceBytes) {
|
|
2788
2142
|
throw new Error(`OCI workspace export exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
|
|
2789
2143
|
}
|
|
2790
|
-
const contentDigest =
|
|
2144
|
+
const contentDigest = digest(contents);
|
|
2791
2145
|
updateSnapshotMetadata(metadata, "file", relative, entry);
|
|
2792
2146
|
seal.update(`file\x00${relative}\x00${entry.mode & 511}\x00${entry.size}\x00${contentDigest}\x00`);
|
|
2793
2147
|
if (entry.size > maxFileWriteBytes) {
|
|
2794
|
-
const previous =
|
|
2148
|
+
const previous = path5.join(previousRoot, ...relative.split("/"));
|
|
2795
2149
|
let unchanged = false;
|
|
2796
2150
|
try {
|
|
2797
2151
|
const previousFile = await readRegularFileNoFollow(previous, {
|
|
2798
2152
|
label: `Previous OCI workspace file ${relative}`,
|
|
2799
2153
|
maxBytes: entry.size
|
|
2800
2154
|
});
|
|
2801
|
-
unchanged = previousFile.stat.size === entry.size &&
|
|
2155
|
+
unchanged = previousFile.stat.size === entry.size && digest(previousFile.contents) === contentDigest;
|
|
2802
2156
|
} catch (error) {
|
|
2803
2157
|
if (error.code !== "ENOENT")
|
|
2804
2158
|
throw error;
|
|
@@ -2820,13 +2174,13 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
2820
2174
|
var snapshotMetadataFingerprint = async (root, maxWorkspaceBytes) => {
|
|
2821
2175
|
let entries = 0;
|
|
2822
2176
|
let totalBytes = 0;
|
|
2823
|
-
const metadata =
|
|
2177
|
+
const metadata = createHash4("sha256");
|
|
2824
2178
|
const visit = async (directory, relativeDirectory = "") => {
|
|
2825
2179
|
for (const name of (await readdir3(directory)).sort()) {
|
|
2826
2180
|
const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
|
2827
2181
|
if (relative === "node_modules")
|
|
2828
2182
|
continue;
|
|
2829
|
-
const absolute =
|
|
2183
|
+
const absolute = path5.join(directory, name);
|
|
2830
2184
|
if (!isInside2(root, absolute) || relative.length > 1024) {
|
|
2831
2185
|
throw new Error(`OCI workspace snapshot contains an invalid path: ${relative}.`);
|
|
2832
2186
|
}
|
|
@@ -2986,7 +2340,7 @@ ${result.stdout}`)) {
|
|
|
2986
2340
|
}
|
|
2987
2341
|
}
|
|
2988
2342
|
requestFingerprint(request) {
|
|
2989
|
-
return
|
|
2343
|
+
return digest(JSON.stringify({
|
|
2990
2344
|
runId: request.runId,
|
|
2991
2345
|
snapshotRoot: request.snapshotRoot,
|
|
2992
2346
|
dependencyRoot: request.dependencyRoot ?? null,
|
|
@@ -3105,7 +2459,7 @@ ${result.stdout}`)) {
|
|
|
3105
2459
|
await this.cli([
|
|
3106
2460
|
"cp",
|
|
3107
2461
|
"-a",
|
|
3108
|
-
`${request.snapshotRoot}${
|
|
2462
|
+
`${request.snapshotRoot}${path5.sep}.`,
|
|
3109
2463
|
`${name}:/workspace`
|
|
3110
2464
|
], 60000);
|
|
3111
2465
|
if (request.dependencyRoot) {
|
|
@@ -3237,7 +2591,7 @@ ${result.stdout}`)) {
|
|
|
3237
2591
|
};
|
|
3238
2592
|
}
|
|
3239
2593
|
const workspaceExportStartedAt = process.hrtime.bigint();
|
|
3240
|
-
const stagedRoot =
|
|
2594
|
+
const stagedRoot = path5.join(path5.dirname(request.snapshotRoot), `.workspace-sync-${randomUUID2()}`);
|
|
3241
2595
|
try {
|
|
3242
2596
|
await privateDirectory(stagedRoot);
|
|
3243
2597
|
await observeOciPhase("oci_export", () => this.cli(["cp", `${session.name}:/workspace/.`, stagedRoot], 60000));
|
|
@@ -3451,7 +2805,7 @@ var collectSnapshotInventory = async (workspace, maxWorkspaceBytes, metrics, max
|
|
|
3451
2805
|
if (metrics)
|
|
3452
2806
|
metrics.inventoryPages += 1;
|
|
3453
2807
|
for await (const candidates of boundedBatches(page.files, async (file) => {
|
|
3454
|
-
const absolute =
|
|
2808
|
+
const absolute = path5.join(workspace.root, ...file.path.split("/"));
|
|
3455
2809
|
if (!isInside2(workspace.root, absolute))
|
|
3456
2810
|
throw new Error(`Snapshot file escaped its workspace: ${file.path}.`);
|
|
3457
2811
|
if (await realpath3(absolute) !== absolute)
|
|
@@ -3472,7 +2826,7 @@ var collectSnapshotInventory = async (workspace, maxWorkspaceBytes, metrics, max
|
|
|
3472
2826
|
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
2827
|
throw new Error(`Snapshot inventory file changed while being read: ${file.path}.`);
|
|
3474
2828
|
}
|
|
3475
|
-
return { path: file.path, digest:
|
|
2829
|
+
return { path: file.path, digest: digest(stable.contents), mode: stable.stat.mode & 511, bytes: stable.contents.byteLength };
|
|
3476
2830
|
}, 4))
|
|
3477
2831
|
for (const file of batch) {
|
|
3478
2832
|
if (files.size >= maxEntries)
|
|
@@ -3485,7 +2839,7 @@ var collectSnapshotInventory = async (workspace, maxWorkspaceBytes, metrics, max
|
|
|
3485
2839
|
return files;
|
|
3486
2840
|
};
|
|
3487
2841
|
var readSnapshotFile = async (workspace, expected, metrics, requireSingleLink = false) => {
|
|
3488
|
-
const absolute =
|
|
2842
|
+
const absolute = path5.join(workspace.root, ...expected.path.split("/"));
|
|
3489
2843
|
if (!isInside2(workspace.root, absolute)) {
|
|
3490
2844
|
throw new Error(`Snapshot file escaped its workspace: ${expected.path}.`);
|
|
3491
2845
|
}
|
|
@@ -3498,7 +2852,7 @@ var readSnapshotFile = async (workspace, expected, metrics, requireSingleLink =
|
|
|
3498
2852
|
});
|
|
3499
2853
|
const { contents } = file;
|
|
3500
2854
|
const entry = file.stat;
|
|
3501
|
-
const actualDigest =
|
|
2855
|
+
const actualDigest = digest(contents);
|
|
3502
2856
|
if (actualDigest !== expected.digest || contents.byteLength !== expected.bytes || (entry.mode & 511) !== expected.mode) {
|
|
3503
2857
|
throw new Error(`Snapshot source changed after inventory: ${expected.path}.`);
|
|
3504
2858
|
}
|
|
@@ -3510,7 +2864,7 @@ var readSnapshotFile = async (workspace, expected, metrics, requireSingleLink =
|
|
|
3510
2864
|
};
|
|
3511
2865
|
var copySnapshotFile = async (source, target, metrics) => {
|
|
3512
2866
|
try {
|
|
3513
|
-
await copyFile(source, target,
|
|
2867
|
+
await copyFile(source, target, fsConstants.COPYFILE_FICLONE_FORCE);
|
|
3514
2868
|
} catch (error) {
|
|
3515
2869
|
const code = error.code;
|
|
3516
2870
|
if (!code || !COPY_ON_WRITE_UNSUPPORTED_CODES.has(code))
|
|
@@ -3532,11 +2886,11 @@ var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceB
|
|
|
3532
2886
|
}
|
|
3533
2887
|
for await (const _batch of boundedBatches([...files.values()], async (expected) => {
|
|
3534
2888
|
const file = await readSnapshotFile(source, expected, metrics);
|
|
3535
|
-
const baseTarget =
|
|
3536
|
-
const snapshotTarget =
|
|
2889
|
+
const baseTarget = path5.join(baseRoot, ...file.path.split("/"));
|
|
2890
|
+
const snapshotTarget = path5.join(snapshotRoot, ...file.path.split("/"));
|
|
3537
2891
|
await Promise.all([
|
|
3538
|
-
mkdir3(
|
|
3539
|
-
mkdir3(
|
|
2892
|
+
mkdir3(path5.dirname(baseTarget), { recursive: true, mode: 448 }),
|
|
2893
|
+
mkdir3(path5.dirname(snapshotTarget), { recursive: true, mode: 448 })
|
|
3540
2894
|
]);
|
|
3541
2895
|
await writeFile(baseTarget, file.contents, { mode: file.mode });
|
|
3542
2896
|
await chmod(baseTarget, file.mode);
|
|
@@ -3552,7 +2906,7 @@ var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceB
|
|
|
3552
2906
|
var deliveryPaths = (value) => {
|
|
3553
2907
|
if (value === undefined)
|
|
3554
2908
|
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") ||
|
|
2909
|
+
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
2910
|
throw new Error("Invalid or oversized OCI delivery path history.");
|
|
3557
2911
|
}
|
|
3558
2912
|
return [...new Set(value)];
|
|
@@ -3613,7 +2967,7 @@ var createEnvironmentPatch = async (runId, base, current, maxFileWriteBytes, max
|
|
|
3613
2967
|
throw new Error(`Environment patch exceeds the ${MAX_EDIT_PROPOSAL_BYTES}-byte import limit.`);
|
|
3614
2968
|
}
|
|
3615
2969
|
const payload = patchPayload(runId, entries);
|
|
3616
|
-
return { entries, totalBytes, patchId:
|
|
2970
|
+
return { entries, totalBytes, patchId: digest(JSON.stringify({ ...payload, executionIdentity: identity ?? null })) };
|
|
3617
2971
|
};
|
|
3618
2972
|
var inspectHostPrecondition = async (workspace, filePath, expectedDigest, expectedMode) => {
|
|
3619
2973
|
try {
|
|
@@ -3679,7 +3033,7 @@ var importPatch = async (runId, expectedPatchId, host, base, current, maxFileWri
|
|
|
3679
3033
|
const rollbackErrors = [];
|
|
3680
3034
|
for (const item of [...quarantined].reverse()) {
|
|
3681
3035
|
try {
|
|
3682
|
-
await host.restoreQuarantined({ quarantineId: item.quarantineId, expectedDigest: item.digest });
|
|
3036
|
+
await host.restoreQuarantined({ quarantineId: item.quarantineId, destination: item.path, expectedDigest: item.digest });
|
|
3683
3037
|
} catch (rollbackError) {
|
|
3684
3038
|
rollbackErrors.push(rollbackError);
|
|
3685
3039
|
}
|
|
@@ -3745,7 +3099,7 @@ var harnessExecutionSession = (context) => {
|
|
|
3745
3099
|
var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
3746
3100
|
const runtime = options.runtime ?? new CliOciRuntimeAdapter(options.config.runtime);
|
|
3747
3101
|
const image = await runtime.inspectImage(options.config.image);
|
|
3748
|
-
const workspaceIdentity =
|
|
3102
|
+
const workspaceIdentity = digest(options.workspace.root);
|
|
3749
3103
|
const manifest = {
|
|
3750
3104
|
schemaVersion: 1,
|
|
3751
3105
|
id: `zhivex-harness-${options.config.runtime}-oci`,
|
|
@@ -3800,7 +3154,7 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3800
3154
|
}
|
|
3801
3155
|
};
|
|
3802
3156
|
const binding = createAgentExecutionEnvironmentBinding(manifest);
|
|
3803
|
-
const environmentRoot =
|
|
3157
|
+
const environmentRoot = path5.join(options.stateDirectory, "environments");
|
|
3804
3158
|
const reviewSnapshot = async (request, allowAbsent = false) => {
|
|
3805
3159
|
let stateDirectory;
|
|
3806
3160
|
try {
|
|
@@ -3812,9 +3166,9 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3812
3166
|
}
|
|
3813
3167
|
const scopeKey = executionScopeKey(request.scope);
|
|
3814
3168
|
const identity = executionIdentity(options.workspace.root, stateDirectory, request.runId, scopeKey);
|
|
3815
|
-
const root =
|
|
3816
|
-
const directory =
|
|
3817
|
-
for (const candidate of [root, directory,
|
|
3169
|
+
const root = path5.join(stateDirectory, "environments");
|
|
3170
|
+
const directory = path5.join(root, runHash(identity));
|
|
3171
|
+
for (const candidate of [root, directory, path5.join(directory, "base"), path5.join(directory, "workspace")]) {
|
|
3818
3172
|
let entry;
|
|
3819
3173
|
try {
|
|
3820
3174
|
entry = await lstat5(candidate);
|
|
@@ -3827,7 +3181,7 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3827
3181
|
throw new Error("Unsafe execution review directory.");
|
|
3828
3182
|
}
|
|
3829
3183
|
}
|
|
3830
|
-
const file = await readRegularFileNoFollow(
|
|
3184
|
+
const file = await readRegularFileNoFollow(path5.join(directory, "environment.json"), {
|
|
3831
3185
|
label: "Execution metadata",
|
|
3832
3186
|
maxBytes: 1024 * 1024
|
|
3833
3187
|
});
|
|
@@ -3835,8 +3189,8 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3835
3189
|
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
3190
|
throw Object.assign(new Error("Execution review binding changed."), { diagnosticCode: "OCI_EXECUTION_BINDING_CHANGED" });
|
|
3837
3191
|
}
|
|
3838
|
-
const base = await Workspace.open(
|
|
3839
|
-
const current = await Workspace.open(
|
|
3192
|
+
const base = await Workspace.open(path5.join(directory, "base"));
|
|
3193
|
+
const current = await Workspace.open(path5.join(directory, "workspace"));
|
|
3840
3194
|
const patch = await createEnvironmentPatch(request.runId, base, current, options.config.maxFileWriteBytes, options.config.maxWorkspaceBytes, undefined, identity);
|
|
3841
3195
|
deliveryPaths(metadata.deliveryPaths);
|
|
3842
3196
|
return { metadata, patch, current };
|
|
@@ -3895,11 +3249,11 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3895
3249
|
const stateDirectory = await realpath3(options.stateDirectory);
|
|
3896
3250
|
const scopeKey = executionScopeKey(request.scope);
|
|
3897
3251
|
const identity = executionIdentity(options.workspace.root, stateDirectory, request.runId, scopeKey);
|
|
3898
|
-
const directory =
|
|
3252
|
+
const directory = path5.join(environmentRoot, runHash(identity));
|
|
3899
3253
|
await privateDirectory(directory);
|
|
3900
|
-
const metadataPath =
|
|
3901
|
-
const baseRoot =
|
|
3902
|
-
const snapshotRoot =
|
|
3254
|
+
const metadataPath = path5.join(directory, "environment.json");
|
|
3255
|
+
const baseRoot = path5.join(directory, "base");
|
|
3256
|
+
const snapshotRoot = path5.join(directory, "workspace");
|
|
3903
3257
|
let metadata;
|
|
3904
3258
|
try {
|
|
3905
3259
|
const metadataFile = await readRegularFileNoFollow(metadataPath, {
|
|
@@ -3914,7 +3268,7 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3914
3268
|
if (error.code !== "ENOENT")
|
|
3915
3269
|
throw error;
|
|
3916
3270
|
try {
|
|
3917
|
-
await lstat5(
|
|
3271
|
+
await lstat5(path5.join(environmentRoot, runHash(request.runId)));
|
|
3918
3272
|
throw new Error("Unscoped legacy OCI artifact requires its original runtime; start a new run.");
|
|
3919
3273
|
} catch (legacyError) {
|
|
3920
3274
|
if (legacyError.code !== "ENOENT")
|
|
@@ -3941,9 +3295,9 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
3941
3295
|
delete metadata.releasedAt;
|
|
3942
3296
|
delete metadata.status;
|
|
3943
3297
|
await atomicJson(metadataPath, metadata);
|
|
3944
|
-
const workspace = await Workspace.open(snapshotRoot, { auditPath:
|
|
3298
|
+
const workspace = await Workspace.open(snapshotRoot, { auditPath: path5.join(directory, "mutation-audit.json") });
|
|
3945
3299
|
const base = await Workspace.open(baseRoot);
|
|
3946
|
-
const dependencyPath =
|
|
3300
|
+
const dependencyPath = path5.join(options.workspace.root, "node_modules");
|
|
3947
3301
|
let dependencyRoot;
|
|
3948
3302
|
try {
|
|
3949
3303
|
const entry = await lstat5(dependencyPath);
|
|
@@ -4022,7 +3376,7 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
4022
3376
|
});
|
|
4023
3377
|
for (const check of matching)
|
|
4024
3378
|
if (check.kind === "package-script") {
|
|
4025
|
-
const file = await readRegularFileNoFollow(
|
|
3379
|
+
const file = await readRegularFileNoFollow(path5.join(workspace.root, "package.json"), { label: "Acceptance package manifest", maxBytes: 256 * 1024, requireSingleLink: true });
|
|
4026
3380
|
const manifestDocument = JSON.parse(file.contents.toString("utf8"));
|
|
4027
3381
|
const resolved = await resolvePackageCheckCommand(workspace.root, manifestDocument, check.script, check.expectedScript, [check.script]);
|
|
4028
3382
|
if (JSON.stringify(resolved.command) !== JSON.stringify(argv))
|
|
@@ -4157,7 +3511,7 @@ var createHarnessOciExecutionEnvironmentUnsafe = async (options) => {
|
|
|
4157
3511
|
async runCheck(check, expectedScript, allowedChecks, context) {
|
|
4158
3512
|
let manifestDocument;
|
|
4159
3513
|
try {
|
|
4160
|
-
const manifestFile = await readRegularFileNoFollow(
|
|
3514
|
+
const manifestFile = await readRegularFileNoFollow(path5.join(snapshotRoot, "package.json"), {
|
|
4161
3515
|
label: "Snapshot package.json",
|
|
4162
3516
|
maxBytes: 1024 * 1024
|
|
4163
3517
|
});
|
|
@@ -4261,7 +3615,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
4261
3615
|
}
|
|
4262
3616
|
};
|
|
4263
3617
|
var cleanupHarnessExecutionArtifacts = async (stateDirectory, before, selection) => {
|
|
4264
|
-
const root =
|
|
3618
|
+
const root = path5.join(stateDirectory, "environments");
|
|
4265
3619
|
const result = { scanned: 0, deleted: 0, skipped: 0 };
|
|
4266
3620
|
let entries;
|
|
4267
3621
|
try {
|
|
@@ -4279,7 +3633,7 @@ var cleanupHarnessExecutionArtifacts = async (stateDirectory, before, selection)
|
|
|
4279
3633
|
continue;
|
|
4280
3634
|
}
|
|
4281
3635
|
result.scanned += 1;
|
|
4282
|
-
const directory =
|
|
3636
|
+
const directory = path5.join(root, entry.name);
|
|
4283
3637
|
try {
|
|
4284
3638
|
const resolved = await realpath3(directory);
|
|
4285
3639
|
if (!isInside2(root, resolved))
|
|
@@ -4288,7 +3642,7 @@ var cleanupHarnessExecutionArtifacts = async (stateDirectory, before, selection)
|
|
|
4288
3642
|
if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
|
|
4289
3643
|
throw new Error("unsafe execution artifact directory");
|
|
4290
3644
|
}
|
|
4291
|
-
const metadataPath =
|
|
3645
|
+
const metadataPath = path5.join(directory, "environment.json");
|
|
4292
3646
|
const metadataFile = await readRegularFileNoFollow(metadataPath, {
|
|
4293
3647
|
label: "Execution cleanup metadata",
|
|
4294
3648
|
maxBytes: 1024 * 1024
|
|
@@ -4307,7 +3661,7 @@ var cleanupHarnessExecutionArtifacts = async (stateDirectory, before, selection)
|
|
|
4307
3661
|
result.skipped += 1;
|
|
4308
3662
|
continue;
|
|
4309
3663
|
}
|
|
4310
|
-
const stagedDirectory =
|
|
3664
|
+
const stagedDirectory = path5.join(root, `.cleanup-${artifactName}-${randomUUID2()}`);
|
|
4311
3665
|
let staged = false;
|
|
4312
3666
|
try {
|
|
4313
3667
|
await rename2(directory, stagedDirectory);
|
|
@@ -4316,7 +3670,7 @@ var cleanupHarnessExecutionArtifacts = async (stateDirectory, before, selection)
|
|
|
4316
3670
|
if (movedEntry.isSymbolicLink() || !movedEntry.isDirectory() || movedEntry.dev !== directoryEntry.dev || movedEntry.ino !== directoryEntry.ino) {
|
|
4317
3671
|
throw new Error("execution artifact directory changed before cleanup");
|
|
4318
3672
|
}
|
|
4319
|
-
const recheckedMetadata = await readRegularFileNoFollow(
|
|
3673
|
+
const recheckedMetadata = await readRegularFileNoFollow(path5.join(stagedDirectory, "environment.json"), { label: "Execution cleanup metadata", maxBytes: 1024 * 1024 });
|
|
4320
3674
|
if (!recheckedMetadata.contents.equals(metadataFile.contents)) {
|
|
4321
3675
|
throw new Error("execution artifact metadata changed before cleanup");
|
|
4322
3676
|
}
|
|
@@ -4343,4 +3697,4 @@ var executionFingerprintInput = (execution, environment) => execution.backend ==
|
|
|
4343
3697
|
};
|
|
4344
3698
|
var describeOciCommand = (command, args) => commandDisplay([command, ...args]);
|
|
4345
3699
|
|
|
4346
|
-
export {
|
|
3700
|
+
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 };
|