@zhivex-ai/harness 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -10
- package/README.md +7 -5
- package/SUPPORT.md +2 -2
- package/contracts/client-protocol-v1.schema.json +593 -0
- package/contracts/engine-api.json +156 -0
- package/contracts/public-api.json +12 -0
- package/contracts/stable-api-signatures.json +33 -33
- package/dist/acp-cli.js +54 -20
- package/dist/audit/index.d.ts +3 -0
- package/dist/audit/index.js +482 -0
- package/dist/audit/pack.d.ts +72 -0
- package/dist/audit-pack-e4vaq7np.js +532 -0
- package/dist/cli/arguments.d.ts +3 -1
- package/dist/cli/audit-pack.d.ts +24 -0
- package/dist/cli/cli-credentials.d.ts +6 -0
- package/dist/cli/cli-profiles.d.ts +4 -2
- package/dist/cli/node-engine.d.ts +2 -0
- package/dist/{cli-2h37qyvg.js → cli-1cf4dcnj.js} +245 -642
- package/dist/{cli-entry-w0hterjs.js → cli-entry-3s63zn4w.js} +40 -13
- package/dist/cli-entry-7f0cen32.js +9990 -0
- package/dist/cli-entry-a890x3kq.js +10348 -0
- package/dist/{cli-entry-20w6nh0k.js → cli-entry-hdtfef27.js} +439 -19
- package/dist/cli-entry-rgs6bxmy.js +484 -0
- package/dist/cli.js +27 -2
- package/dist/client/json-contracts.d.ts +4 -0
- package/dist/client/protocol.d.ts +133 -0
- package/dist/client/task-control-host.d.ts +400 -0
- package/dist/client/task-projection-host.d.ts +12 -0
- package/dist/client/task-projection.d.ts +168 -0
- package/dist/compat/code-support.js +28 -22
- package/dist/{config-qt69hkqj.js → config-r9fdhce6.js} +3 -3
- package/dist/{config-j9624t6t.js → config-vyajmksy.js} +1 -1
- package/dist/engine/acp.js +15 -11
- package/dist/engine/client.js +14 -10
- package/dist/engine/desktop-providers-v1.d.ts +2 -0
- package/dist/engine/desktop-providers-v1.js +23 -3
- package/dist/engine/desktop-state-v1.js +11 -8
- package/dist/engine/index.d.ts +3 -0
- package/dist/engine/index.js +101 -554
- package/dist/engine/mcp-stdio-v1.js +8 -6
- package/dist/engine/models.js +76 -4
- package/dist/engine/protocol.d.ts +2 -0
- package/dist/engine/protocol.js +110 -0
- package/dist/engine/service.js +15 -11
- package/dist/hostile-repository-demo.js +1095 -505
- package/dist/{index-937cb7y8.js → index-02pqtbbg.js} +559 -33
- package/dist/{index-pp474137.js → index-0zs2dz26.js} +766 -931
- package/dist/{index-8zjmnyw5.js → index-1265j7t2.js} +73 -6
- package/dist/index-4fx5f3k5.js +269 -0
- package/dist/{index-nmjdwd4p.js → index-4gt045j0.js} +109 -1
- package/dist/{index-6dft2vev.js → index-6599mpmy.js} +5 -3
- package/dist/{index-tfzta544.js → index-74kkpyqk.js} +9 -7
- package/dist/{index-61gthdz6.js → index-8sm2a1wa.js} +1 -1
- package/dist/index-c39197wq.js +552 -0
- package/dist/{index-5jhgatga.js → index-crta5wzv.js} +7 -6
- package/dist/{index-m34q25jj.js → index-d9zkmhz3.js} +143 -824
- package/dist/{index-pdmevfxv.js → index-dtb2c6ds.js} +5 -259
- package/dist/index-f6mqa5gk.js +1535 -0
- package/dist/index-j6zxnc8m.js +490 -0
- package/dist/{index-rkbf7dqg.js → index-mv8sb1ns.js} +5 -3
- package/dist/{index-xqxeypt5.js → index-mzydbzp8.js} +76 -4
- package/dist/{index-wg1sd3at.js → index-qfsrrnz6.js} +1 -1
- package/dist/{index-m82cfspw.js → index-sk3cap35.js} +877 -963
- package/dist/index-sstgcsr0.js +511 -0
- package/dist/index-wm9xeq1n.js +329 -0
- package/dist/index-xdbkcccf.js +719 -0
- package/dist/{index-s2k2edts.js → index-zxg4ve4j.js} +41 -1548
- package/dist/index.d.ts +3 -0
- package/dist/index.js +93 -77
- package/dist/internal/desktop/providers.d.ts +2 -0
- package/dist/models/catalog.d.ts +2 -0
- package/dist/persistence/sqlite-database.d.ts +1 -0
- package/dist/providers/azure-openai.d.ts +14 -0
- package/dist/providers/bedrock.d.ts +23 -0
- package/dist/providers/providers.d.ts +1 -1
- package/dist/runtime/durable-review-group.d.ts +33 -0
- package/dist/runtime/harness.d.ts +11 -0
- package/dist/runtime/run-interruption.d.ts +7 -0
- package/dist/runtime/shared-budget.d.ts +30 -0
- package/dist/runtime/task-continuity.d.ts +300 -77
- package/dist/service-cli.js +15 -11
- package/dist/{service-client-cli-fn54ev6w.js → service-client-cli-eqrdc9zq.js} +400 -305
- package/dist/verify-historical-migrations.js +1095 -505
- package/dist/zhx.js +27 -2
- package/docs/AUDIT_PACK.md +99 -0
- package/docs/AZURE_OPENAI.md +53 -0
- package/docs/BEDROCK.md +56 -0
- package/docs/CLI.md +15 -0
- package/docs/CLIENT_PROTOCOL.md +178 -0
- package/docs/CODE_RELEASE.md +31 -1
- package/docs/CODE_WEB_TASKS.md +116 -0
- package/docs/CONTEXT_ENGINEERING.md +59 -0
- package/docs/CREDENTIALS.md +21 -0
- package/docs/DURABLE_OPERATIONS.md +93 -1
- package/docs/ENGINE_API.md +5 -2
- package/docs/EXTENSIBILITY.md +2 -2
- package/docs/FIRST_USE.md +6 -6
- package/docs/MINIMUM_TASK_AUTHORITY.md +3 -3
- package/docs/PROJECT_MEMORY.md +1 -1
- package/docs/README.md +6 -1
- package/docs/STABILITY.md +3 -1
- package/docs/SUPPORT_MATRIX.md +1 -1
- package/docs/SUPPORT_MATRIX_1_1.md +1 -1
- package/docs/USAGE.md +3 -1
- package/docs/VERTEX.md +2 -2
- package/docs/WEB_LIMITS.md +4 -3
- package/docs/adr/0002-isolated-mcp-stdio.md +6 -3
- package/docs/support-matrix-1.1.json +3 -1
- package/docs/support-matrix.json +6 -4
- package/evaluations/code-task-ui.json +20 -0
- package/evaluations/representative-assembly-matrix.json +10 -1
- package/evaluations/task-projection.json +10 -0
- package/evaluations/task-recovery-lab.json +60 -0
- package/examples/README.md +3 -3
- package/package.json +23 -13
- package/dist/cli-entry-4ngptr62.js +0 -19900
- package/dist/index-m49jx72n.js +0 -220
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
// src/workspace/edit-contracts.ts
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var EDIT_CONTRACT_SCHEMA_VERSION = 1;
|
|
6
|
+
var EDIT_DIGEST_ALGORITHM = "sha256";
|
|
7
|
+
var MAX_EDIT_CHANGES = 50;
|
|
8
|
+
var MAX_EDIT_FILE_BYTES = 1024 * 1024;
|
|
9
|
+
var MAX_EDIT_PROPOSAL_BYTES = 4 * 1024 * 1024;
|
|
10
|
+
var SHA256_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
11
|
+
var fileDigestSchema = z.string().regex(SHA256_DIGEST_PATTERN, "Digest must use the format sha256:<64 lowercase hexadecimal characters>.");
|
|
12
|
+
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.");
|
|
13
|
+
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) => {
|
|
14
|
+
const segments = value.split(/[\\/]+/);
|
|
15
|
+
return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
|
|
16
|
+
}, "Path must be normalized and cannot contain empty, dot, or parent segments.");
|
|
17
|
+
var editChangeSchema = z.strictObject({
|
|
18
|
+
path: workspaceFilePathSchema,
|
|
19
|
+
expectedDigest: expectedEditDigestSchema,
|
|
20
|
+
content: z.string()
|
|
21
|
+
}).superRefine((change, context) => {
|
|
22
|
+
if (Buffer.byteLength(change.content, "utf8") > MAX_EDIT_FILE_BYTES) {
|
|
23
|
+
context.addIssue({
|
|
24
|
+
code: "custom",
|
|
25
|
+
path: ["content"],
|
|
26
|
+
message: `Content exceeds the ${MAX_EDIT_FILE_BYTES}-byte per-file limit.`
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
var editChangesSchema = z.array(editChangeSchema).min(1).max(MAX_EDIT_CHANGES).superRefine((changes, context) => {
|
|
31
|
+
const paths = new Set;
|
|
32
|
+
let totalBytes = 0;
|
|
33
|
+
for (let index = 0;index < changes.length; index += 1) {
|
|
34
|
+
const change = changes[index];
|
|
35
|
+
if (!change) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (paths.has(change.path)) {
|
|
39
|
+
context.addIssue({
|
|
40
|
+
code: "custom",
|
|
41
|
+
path: [index, "path"],
|
|
42
|
+
message: `Duplicate target path: ${change.path}.`
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
paths.add(change.path);
|
|
46
|
+
totalBytes += Buffer.byteLength(change.content, "utf8");
|
|
47
|
+
}
|
|
48
|
+
if (totalBytes > MAX_EDIT_PROPOSAL_BYTES) {
|
|
49
|
+
context.addIssue({
|
|
50
|
+
code: "custom",
|
|
51
|
+
message: `Proposal exceeds the ${MAX_EDIT_PROPOSAL_BYTES}-byte aggregate limit.`
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
var editProposalInputSchema = z.strictObject({
|
|
56
|
+
changes: editChangesSchema
|
|
57
|
+
});
|
|
58
|
+
var editProposalChangeSchema = z.strictObject({
|
|
59
|
+
path: workspaceFilePathSchema,
|
|
60
|
+
expectedDigest: expectedEditDigestSchema,
|
|
61
|
+
contentDigest: fileDigestSchema,
|
|
62
|
+
bytes: z.number().int().min(0).max(MAX_EDIT_FILE_BYTES)
|
|
63
|
+
});
|
|
64
|
+
var editProposalSchema = z.strictObject({
|
|
65
|
+
schemaVersion: z.literal(EDIT_CONTRACT_SCHEMA_VERSION),
|
|
66
|
+
kind: z.literal("edit-proposal"),
|
|
67
|
+
proposalId: fileDigestSchema,
|
|
68
|
+
digestAlgorithm: z.literal(EDIT_DIGEST_ALGORITHM),
|
|
69
|
+
changes: z.array(editProposalChangeSchema).min(1).max(MAX_EDIT_CHANGES)
|
|
70
|
+
});
|
|
71
|
+
var applyEditProposalInputSchema = z.strictObject({
|
|
72
|
+
proposalId: fileDigestSchema,
|
|
73
|
+
changes: editChangesSchema
|
|
74
|
+
}).superRefine((input, context) => {
|
|
75
|
+
if (!editProposalInputSchema.safeParse({ changes: input.changes }).success)
|
|
76
|
+
return;
|
|
77
|
+
if (createEditProposal({ changes: input.changes }).proposalId !== input.proposalId) {
|
|
78
|
+
context.addIssue({
|
|
79
|
+
code: "custom",
|
|
80
|
+
path: ["proposalId"],
|
|
81
|
+
message: "proposalId does not match the supplied paths, preconditions, and content. Read proposalId from the matching propose_edits result; do not invent it."
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
var moveFileInputSchema = z.strictObject({
|
|
86
|
+
source: workspaceFilePathSchema,
|
|
87
|
+
destination: workspaceFilePathSchema,
|
|
88
|
+
expectedDigest: fileDigestSchema
|
|
89
|
+
}).refine((input) => input.source !== input.destination, {
|
|
90
|
+
path: ["destination"],
|
|
91
|
+
message: "Move destination must differ from source."
|
|
92
|
+
});
|
|
93
|
+
var quarantineFileInputSchema = z.strictObject({
|
|
94
|
+
path: workspaceFilePathSchema,
|
|
95
|
+
expectedDigest: fileDigestSchema
|
|
96
|
+
});
|
|
97
|
+
var restoreFileInputSchema = z.strictObject({
|
|
98
|
+
quarantineId: z.string().min(1).max(200).regex(/^[A-Za-z0-9._:-]+$/),
|
|
99
|
+
destination: workspaceFilePathSchema.optional(),
|
|
100
|
+
expectedDigest: fileDigestSchema.optional()
|
|
101
|
+
});
|
|
102
|
+
var mutationOperationSchema = z.enum(["create", "update", "move", "quarantine", "restore"]);
|
|
103
|
+
var mutationAuditEntrySchema = z.strictObject({
|
|
104
|
+
id: z.string().min(1).max(200),
|
|
105
|
+
operation: mutationOperationSchema,
|
|
106
|
+
path: workspaceFilePathSchema,
|
|
107
|
+
destination: workspaceFilePathSchema.optional(),
|
|
108
|
+
beforeDigest: fileDigestSchema.optional(),
|
|
109
|
+
afterDigest: fileDigestSchema.optional(),
|
|
110
|
+
timestamp: z.iso.datetime(),
|
|
111
|
+
quarantineId: z.string().min(1).max(200).optional()
|
|
112
|
+
});
|
|
113
|
+
var digest = (value) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
114
|
+
var canonicalProposalPayload = (changes) => ({
|
|
115
|
+
schemaVersion: EDIT_CONTRACT_SCHEMA_VERSION,
|
|
116
|
+
kind: "edit-proposal",
|
|
117
|
+
digestAlgorithm: EDIT_DIGEST_ALGORITHM,
|
|
118
|
+
changes: [...changes].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0).map((change) => ({
|
|
119
|
+
path: change.path,
|
|
120
|
+
expectedDigest: change.expectedDigest,
|
|
121
|
+
contentDigest: digest(change.content),
|
|
122
|
+
bytes: Buffer.byteLength(change.content, "utf8")
|
|
123
|
+
}))
|
|
124
|
+
});
|
|
125
|
+
var createEditProposal = (input) => {
|
|
126
|
+
const parsed = editProposalInputSchema.parse(input);
|
|
127
|
+
const payload = canonicalProposalPayload(parsed.changes);
|
|
128
|
+
return editProposalSchema.parse({
|
|
129
|
+
...payload,
|
|
130
|
+
proposalId: digest(JSON.stringify(payload))
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
var validateEditProposal = (input) => {
|
|
134
|
+
const parsed = applyEditProposalInputSchema.parse(input);
|
|
135
|
+
const proposal = createEditProposal({ changes: parsed.changes });
|
|
136
|
+
if (proposal.proposalId !== parsed.proposalId) {
|
|
137
|
+
throw new Error("proposalId does not match the supplied paths, preconditions, and content.");
|
|
138
|
+
}
|
|
139
|
+
return parsed;
|
|
140
|
+
};
|
|
141
|
+
var editContractDocument = (kind, result) => ({
|
|
142
|
+
schemaVersion: EDIT_CONTRACT_SCHEMA_VERSION,
|
|
143
|
+
kind,
|
|
144
|
+
result
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// src/persistence/persistence-secrets.ts
|
|
148
|
+
var protectedValues = new Set;
|
|
149
|
+
function protectPersistenceSecret(secret) {
|
|
150
|
+
if (!secret)
|
|
151
|
+
return;
|
|
152
|
+
protectedValues.add(secret);
|
|
153
|
+
protectedValues.add(JSON.stringify(secret).slice(1, -1));
|
|
154
|
+
}
|
|
155
|
+
function assertNoPersistenceSecret(value) {
|
|
156
|
+
if (!protectedValues.size)
|
|
157
|
+
return;
|
|
158
|
+
const seen = new WeakSet;
|
|
159
|
+
const visit = (item) => {
|
|
160
|
+
if (typeof item === "string") {
|
|
161
|
+
for (const secret of protectedValues)
|
|
162
|
+
if (item.includes(secret))
|
|
163
|
+
throw new Error("PERSISTENCE_SECRET_REJECTED");
|
|
164
|
+
} else if (item instanceof Uint8Array) {
|
|
165
|
+
const bytes = Buffer.from(item.buffer, item.byteOffset, item.byteLength);
|
|
166
|
+
for (const secret of protectedValues)
|
|
167
|
+
if (bytes.includes(Buffer.from(secret)))
|
|
168
|
+
throw new Error("PERSISTENCE_SECRET_REJECTED");
|
|
169
|
+
} else if (item && typeof item === "object" && !seen.has(item)) {
|
|
170
|
+
seen.add(item);
|
|
171
|
+
for (const [key, entry] of Object.entries(item)) {
|
|
172
|
+
visit(key);
|
|
173
|
+
visit(entry);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
visit(value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/persistence/sqlite-access.ts
|
|
181
|
+
import { closeSync, constants, fchmodSync, fstatSync, lstatSync, openSync, realpathSync } from "node:fs";
|
|
182
|
+
import path2 from "node:path";
|
|
183
|
+
var O_SHLOCK = 16;
|
|
184
|
+
var O_EXLOCK = 32;
|
|
185
|
+
var held = new WeakMap;
|
|
186
|
+
var activeDescriptors = new Set;
|
|
187
|
+
function filename(databasePath) {
|
|
188
|
+
const directory = realpathSync(path2.dirname(path2.resolve(databasePath)));
|
|
189
|
+
return path2.join(directory, `.${path2.basename(databasePath)}.access-lock`);
|
|
190
|
+
}
|
|
191
|
+
function releaseDescriptor(state) {
|
|
192
|
+
closeSync(state.fd);
|
|
193
|
+
activeDescriptors.delete(state.fd);
|
|
194
|
+
}
|
|
195
|
+
function makeLease(lock, fd, exclusive) {
|
|
196
|
+
const state = { filename: lock, fd, closed: false, borrowers: 0, exclusive };
|
|
197
|
+
const lease = Object.freeze({ fd, close() {
|
|
198
|
+
if (!state.closed) {
|
|
199
|
+
state.closed = true;
|
|
200
|
+
if (state.borrowers === 0)
|
|
201
|
+
releaseDescriptor(state);
|
|
202
|
+
}
|
|
203
|
+
} });
|
|
204
|
+
held.set(lease, state);
|
|
205
|
+
activeDescriptors.add(fd);
|
|
206
|
+
return lease;
|
|
207
|
+
}
|
|
208
|
+
function acquireSqliteAccess(databasePath, exclusive = false) {
|
|
209
|
+
if (databasePath === ":memory:" || process.platform !== "darwin")
|
|
210
|
+
return;
|
|
211
|
+
let fd;
|
|
212
|
+
try {
|
|
213
|
+
const lock = filename(databasePath);
|
|
214
|
+
fd = openSync(lock, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW | constants.O_NONBLOCK | (exclusive ? O_EXLOCK : O_SHLOCK), 384);
|
|
215
|
+
const info = fstatSync(fd);
|
|
216
|
+
const current = lstatSync(lock);
|
|
217
|
+
if (!info.isFile() || info.nlink !== 1 || info.uid !== process.getuid?.() || info.size !== 0 || current.isSymbolicLink() || current.dev !== info.dev || current.ino !== info.ino)
|
|
218
|
+
throw new Error;
|
|
219
|
+
if (info.mode & 63)
|
|
220
|
+
fchmodSync(fd, 384);
|
|
221
|
+
if (fstatSync(fd).mode & 63)
|
|
222
|
+
throw new Error;
|
|
223
|
+
return makeLease(lock, fd, exclusive);
|
|
224
|
+
} catch {
|
|
225
|
+
if (fd !== undefined)
|
|
226
|
+
closeSync(fd);
|
|
227
|
+
throw new Error("SQLITE_ACCESS_UNAVAILABLE");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function retainSqliteAccess(lease, databasePath) {
|
|
231
|
+
validateSqliteAccess(lease, databasePath);
|
|
232
|
+
const state = held.get(lease);
|
|
233
|
+
state.borrowers++;
|
|
234
|
+
let released = false;
|
|
235
|
+
return () => {
|
|
236
|
+
if (!released) {
|
|
237
|
+
released = true;
|
|
238
|
+
state.borrowers--;
|
|
239
|
+
if (state.closed && state.borrowers === 0)
|
|
240
|
+
releaseDescriptor(state);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function exclusiveSqliteAccessDescriptor(lease, databasePath) {
|
|
245
|
+
validateSqliteAccess(lease, databasePath);
|
|
246
|
+
const state = held.get(lease);
|
|
247
|
+
if (!state.exclusive || state.borrowers !== 0)
|
|
248
|
+
throw new Error("SQLITE_ACCESS_UNAVAILABLE");
|
|
249
|
+
return state.fd;
|
|
250
|
+
}
|
|
251
|
+
function adoptExclusiveSqliteAccess(databasePath, fd) {
|
|
252
|
+
try {
|
|
253
|
+
if (process.platform !== "darwin" || !Number.isInteger(fd) || fd < 4 || fd > 603 || activeDescriptors.has(fd))
|
|
254
|
+
throw new Error;
|
|
255
|
+
const lock = filename(databasePath), current = lstatSync(lock), owned = fstatSync(fd);
|
|
256
|
+
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)
|
|
257
|
+
throw new Error;
|
|
258
|
+
return makeLease(lock, fd, true);
|
|
259
|
+
} catch {
|
|
260
|
+
throw new Error("SQLITE_ACCESS_UNAVAILABLE");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function validateSqliteAccess(lease, databasePath) {
|
|
264
|
+
try {
|
|
265
|
+
const state = held.get(lease);
|
|
266
|
+
if (!state || state.closed || state.filename !== filename(databasePath))
|
|
267
|
+
throw new Error;
|
|
268
|
+
const current = lstatSync(state.filename), owned = fstatSync(state.fd);
|
|
269
|
+
if (current.isSymbolicLink() || !current.isFile() || current.nlink !== 1 || current.dev !== owned.dev || current.ino !== owned.ino)
|
|
270
|
+
throw new Error;
|
|
271
|
+
} catch {
|
|
272
|
+
throw new Error("SQLITE_ACCESS_UNAVAILABLE");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/persistence/sqlite-database.ts
|
|
277
|
+
import { createRequire } from "node:module";
|
|
278
|
+
var require2 = createRequire(process.execPath);
|
|
279
|
+
var DatabaseSyncConstructor;
|
|
280
|
+
var SQLITE_EXPERIMENTAL = "SQLite is an experimental feature and might change at any time";
|
|
281
|
+
var installSqliteExperimentalWarningFilter = () => {
|
|
282
|
+
const current = process.emitWarning;
|
|
283
|
+
if (current.zhivexSqliteFilter)
|
|
284
|
+
return;
|
|
285
|
+
const emit = process.emitWarning.bind(process);
|
|
286
|
+
const filtered = (warning, type, code, ctor) => {
|
|
287
|
+
const message = typeof warning === "string" ? warning : warning.message;
|
|
288
|
+
const resolved = typeof type === "string" ? type : type?.type;
|
|
289
|
+
if ((resolved === "ExperimentalWarning" || typeof warning !== "string" && warning.name === "ExperimentalWarning") && message.includes(SQLITE_EXPERIMENTAL))
|
|
290
|
+
return;
|
|
291
|
+
if (typeof type === "string")
|
|
292
|
+
return emit(warning, type, code, ctor);
|
|
293
|
+
return emit(warning, type);
|
|
294
|
+
};
|
|
295
|
+
filtered.zhivexSqliteFilter = true;
|
|
296
|
+
process.emitWarning = filtered;
|
|
297
|
+
};
|
|
298
|
+
var loadDatabaseSync = () => {
|
|
299
|
+
installSqliteExperimentalWarningFilter();
|
|
300
|
+
DatabaseSyncConstructor ??= require2("node:sqlite").DatabaseSync;
|
|
301
|
+
return DatabaseSyncConstructor;
|
|
302
|
+
};
|
|
303
|
+
var normalizeNumberedParameters = (sql) => {
|
|
304
|
+
const indexes = new Set;
|
|
305
|
+
const normalizedSql = sql.replace(/\?([1-9]\d*)/g, (_match, rawIndex) => {
|
|
306
|
+
const index = Number(rawIndex);
|
|
307
|
+
indexes.add(index);
|
|
308
|
+
return `$__zhivex_${index}`;
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
sql: normalizedSql,
|
|
312
|
+
indexes: [...indexes].sort((left, right) => left - right)
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
var numberedBindings = (indexes, parameters) => {
|
|
316
|
+
if (indexes.length === 0)
|
|
317
|
+
return;
|
|
318
|
+
const maximumIndex = indexes.at(-1);
|
|
319
|
+
if (parameters.length !== maximumIndex) {
|
|
320
|
+
throw new Error(`SQLite numbered parameters require exactly ${maximumIndex} binding${maximumIndex === 1 ? "" : "s"}; received ${parameters.length}.`);
|
|
321
|
+
}
|
|
322
|
+
return Object.fromEntries(indexes.map((index) => [`$__zhivex_${index}`, parameters[index - 1]]));
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
class SqliteDatabase {
|
|
326
|
+
#database;
|
|
327
|
+
#access;
|
|
328
|
+
#ownedAccess;
|
|
329
|
+
#releaseBorrowed;
|
|
330
|
+
#databasePath;
|
|
331
|
+
constructor(databasePath, options = {}) {
|
|
332
|
+
options.create;
|
|
333
|
+
options.strict;
|
|
334
|
+
if (options.accessLease)
|
|
335
|
+
validateSqliteAccess(options.accessLease, databasePath);
|
|
336
|
+
this.#access = options.accessLease ?? acquireSqliteAccess(databasePath);
|
|
337
|
+
this.#ownedAccess = options.accessLease === undefined;
|
|
338
|
+
this.#databasePath = databasePath;
|
|
339
|
+
this.#releaseBorrowed = options.accessLease ? retainSqliteAccess(options.accessLease, databasePath) : undefined;
|
|
340
|
+
try {
|
|
341
|
+
const DatabaseSync = loadDatabaseSync();
|
|
342
|
+
this.#database = new DatabaseSync(databasePath, {
|
|
343
|
+
...options.readonly === undefined ? {} : { readOnly: options.readonly }
|
|
344
|
+
});
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (this.#ownedAccess)
|
|
347
|
+
this.#access?.close();
|
|
348
|
+
this.#releaseBorrowed?.();
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
exec(sql) {
|
|
353
|
+
if (this.#access)
|
|
354
|
+
validateSqliteAccess(this.#access, this.#databasePath);
|
|
355
|
+
assertNoPersistenceSecret(sql);
|
|
356
|
+
this.#database.exec(sql);
|
|
357
|
+
}
|
|
358
|
+
query(sql) {
|
|
359
|
+
assertNoPersistenceSecret(sql);
|
|
360
|
+
if (this.#access)
|
|
361
|
+
validateSqliteAccess(this.#access, this.#databasePath);
|
|
362
|
+
const normalized = normalizeNumberedParameters(sql);
|
|
363
|
+
const statement = this.#database.prepare(normalized.sql);
|
|
364
|
+
const invoke = (method, parameters) => {
|
|
365
|
+
if (this.#access)
|
|
366
|
+
validateSqliteAccess(this.#access, this.#databasePath);
|
|
367
|
+
assertNoPersistenceSecret(sql);
|
|
368
|
+
assertNoPersistenceSecret(parameters);
|
|
369
|
+
const bindings = numberedBindings(normalized.indexes, parameters);
|
|
370
|
+
return bindings ? method(bindings) : method(...parameters);
|
|
371
|
+
};
|
|
372
|
+
return {
|
|
373
|
+
run: (...parameters) => invoke(statement.run.bind(statement), parameters),
|
|
374
|
+
get: (...parameters) => invoke(statement.get.bind(statement), parameters),
|
|
375
|
+
all: (...parameters) => invoke(statement.all.bind(statement), parameters)
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
close(throwOnError = true) {
|
|
379
|
+
try {
|
|
380
|
+
this.#database.close();
|
|
381
|
+
if (this.#ownedAccess)
|
|
382
|
+
this.#access?.close();
|
|
383
|
+
this.#releaseBorrowed?.();
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (throwOnError) {
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/workspace/file-security.ts
|
|
393
|
+
import { constants as fsConstants } from "node:fs";
|
|
394
|
+
import { open } from "node:fs/promises";
|
|
395
|
+
import path3 from "node:path";
|
|
396
|
+
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;
|
|
397
|
+
|
|
398
|
+
class UnsafeFileTypeError extends Error {
|
|
399
|
+
constructor(label) {
|
|
400
|
+
super(`${label} must be a regular file and must not be a symbolic link.`);
|
|
401
|
+
this.name = "UnsafeFileTypeError";
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
class FileSizeLimitError extends Error {
|
|
406
|
+
label;
|
|
407
|
+
maxBytes;
|
|
408
|
+
constructor(label, maxBytes) {
|
|
409
|
+
super(`${label} exceeds the ${maxBytes}-byte limit.`);
|
|
410
|
+
this.label = label;
|
|
411
|
+
this.maxBytes = maxBytes;
|
|
412
|
+
this.name = "FileSizeLimitError";
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
class FileChangedWhileReadingError extends Error {
|
|
417
|
+
constructor(label) {
|
|
418
|
+
super(`${label} changed while it was being read.`);
|
|
419
|
+
this.name = "FileChangedWhileReadingError";
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
var invalidFileError = (label) => new UnsafeFileTypeError(label);
|
|
423
|
+
var openRegularFileNoFollow = async (target, label) => {
|
|
424
|
+
try {
|
|
425
|
+
const flags = fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK;
|
|
426
|
+
let absolute = path3.resolve(target);
|
|
427
|
+
if (process.platform === "darwin") {
|
|
428
|
+
absolute = absolute.replace(/^\/(tmp|var|etc)(\/|$)/, "/private/$1$2");
|
|
429
|
+
return await open(absolute, fsConstants.O_RDONLY | fsConstants.O_NONBLOCK | 536870912, 384);
|
|
430
|
+
}
|
|
431
|
+
if (process.platform !== "linux")
|
|
432
|
+
throw new Error("Safe descriptor-relative reads require Linux or macOS.");
|
|
433
|
+
const segments = absolute.split("/").filter(Boolean);
|
|
434
|
+
const leaf = segments.pop();
|
|
435
|
+
if (!leaf)
|
|
436
|
+
throw invalidFileError(label);
|
|
437
|
+
let directory = await open("/", fsConstants.O_RDONLY | fsConstants.O_DIRECTORY);
|
|
438
|
+
try {
|
|
439
|
+
for (const segment of segments) {
|
|
440
|
+
const next = await open(`/proc/self/fd/${directory.fd}/${segment}`, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
|
|
441
|
+
await directory.close();
|
|
442
|
+
directory = next;
|
|
443
|
+
}
|
|
444
|
+
return await open(`/proc/self/fd/${directory.fd}/${leaf}`, flags);
|
|
445
|
+
} finally {
|
|
446
|
+
await directory.close();
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
const code = error.code;
|
|
450
|
+
if (code === "ELOOP" || code === "EMLINK" || code === "ENOTDIR")
|
|
451
|
+
throw invalidFileError(label);
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
var statRegularFileNoFollow = async (target, options) => {
|
|
456
|
+
const handle = await openRegularFileNoFollow(target, options.label);
|
|
457
|
+
try {
|
|
458
|
+
const stat = await handle.stat();
|
|
459
|
+
if (!stat.isFile() || options.requireSingleLink && stat.nlink !== 1) {
|
|
460
|
+
throw invalidFileError(options.label);
|
|
461
|
+
}
|
|
462
|
+
return stat;
|
|
463
|
+
} finally {
|
|
464
|
+
await handle.close();
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
var readRegularFileNoFollow = async (target, options) => {
|
|
468
|
+
const handle = await openRegularFileNoFollow(target, options.label);
|
|
469
|
+
try {
|
|
470
|
+
const before = await handle.stat();
|
|
471
|
+
if (!before.isFile() || options.requireSingleLink && before.nlink !== 1) {
|
|
472
|
+
throw invalidFileError(options.label);
|
|
473
|
+
}
|
|
474
|
+
if (!Number.isSafeInteger(before.size) || before.size < 0) {
|
|
475
|
+
throw new Error(`${options.label} has an unsupported size.`);
|
|
476
|
+
}
|
|
477
|
+
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
|
|
478
|
+
throw new Error(`${options.label} has an invalid byte limit.`);
|
|
479
|
+
}
|
|
480
|
+
if (before.size > options.maxBytes) {
|
|
481
|
+
throw new FileSizeLimitError(options.label, options.maxBytes);
|
|
482
|
+
}
|
|
483
|
+
const contents = Buffer.alloc(before.size);
|
|
484
|
+
let offset = 0;
|
|
485
|
+
while (offset < contents.byteLength) {
|
|
486
|
+
const { bytesRead } = await handle.read(contents, offset, contents.byteLength - offset, offset);
|
|
487
|
+
if (bytesRead === 0)
|
|
488
|
+
break;
|
|
489
|
+
offset += bytesRead;
|
|
490
|
+
}
|
|
491
|
+
const probe = Buffer.allocUnsafe(1);
|
|
492
|
+
const { bytesRead: extraBytes } = await handle.read(probe, 0, 1, before.size);
|
|
493
|
+
const after = await handle.stat();
|
|
494
|
+
if (offset !== before.size || extraBytes !== 0 || !sameOpenFile(before, after) || options.requireSingleLink && after.nlink !== 1) {
|
|
495
|
+
throw new FileChangedWhileReadingError(options.label);
|
|
496
|
+
}
|
|
497
|
+
return { contents, stat: after };
|
|
498
|
+
} finally {
|
|
499
|
+
await handle.close();
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
async function* readRegularFileChunksNoFollow(target, options) {
|
|
503
|
+
const handle = await openRegularFileNoFollow(target, options.label);
|
|
504
|
+
try {
|
|
505
|
+
const before = await handle.stat();
|
|
506
|
+
if (!before.isFile() || options.requireSingleLink && before.nlink !== 1)
|
|
507
|
+
throw invalidFileError(options.label);
|
|
508
|
+
if (options.requirePrivate && (before.mode & 63) !== 0)
|
|
509
|
+
throw new Error(`${options.label} permissions must not grant group or other access.`);
|
|
510
|
+
let offset = 0;
|
|
511
|
+
while (offset < before.size) {
|
|
512
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, before.size - offset));
|
|
513
|
+
const { bytesRead: bytesRead2 } = await handle.read(buffer, 0, buffer.length, offset);
|
|
514
|
+
if (!bytesRead2)
|
|
515
|
+
throw new FileChangedWhileReadingError(options.label);
|
|
516
|
+
offset += bytesRead2;
|
|
517
|
+
yield buffer.subarray(0, bytesRead2);
|
|
518
|
+
}
|
|
519
|
+
const probe = Buffer.allocUnsafe(1);
|
|
520
|
+
const { bytesRead } = await handle.read(probe, 0, 1, offset);
|
|
521
|
+
if (bytesRead || !sameOpenFile(before, await handle.stat()))
|
|
522
|
+
throw new FileChangedWhileReadingError(options.label);
|
|
523
|
+
} finally {
|
|
524
|
+
await handle.close();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/runtime/task-acceptance-checks.ts
|
|
529
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
530
|
+
import { z as z2 } from "zod";
|
|
531
|
+
var sha = z2.string().regex(/^sha256:[a-f0-9]{64}$/);
|
|
532
|
+
var taskCheckReceiptSchema = z2.strictObject({
|
|
533
|
+
schemaVersion: z2.literal(1),
|
|
534
|
+
checkId: z2.string().max(128),
|
|
535
|
+
contractDigest: sha,
|
|
536
|
+
contractRevision: z2.number().int().positive(),
|
|
537
|
+
runId: z2.string().max(512),
|
|
538
|
+
executionIdentity: z2.string().max(512),
|
|
539
|
+
patchId: sha,
|
|
540
|
+
snapshotDigest: sha,
|
|
541
|
+
argvDigest: sha,
|
|
542
|
+
exitCode: z2.number().int(),
|
|
543
|
+
timedOut: z2.boolean(),
|
|
544
|
+
unchanged: z2.boolean(),
|
|
545
|
+
source: z2.strictObject({ id: z2.string().min(1).max(512), name: z2.string().min(1).max(128), startedAt: z2.number().finite().nonnegative().optional() }).optional()
|
|
546
|
+
});
|
|
547
|
+
var argvDigest = (command, args) => "sha256:" + createHash2("sha256").update(JSON.stringify([command, ...args])).digest("hex");
|
|
548
|
+
function createTaskAcceptanceChecks(ledger, previous = [], journal = []) {
|
|
549
|
+
const current = ledger.revisions.at(-1);
|
|
550
|
+
const receipts = new Map;
|
|
551
|
+
const parsed = z2.array(taskCheckReceiptSchema).max(32).safeParse(previous);
|
|
552
|
+
if (parsed.success)
|
|
553
|
+
for (const receipt of parsed.data) {
|
|
554
|
+
const check = current.contract.requiredChecks.find((c) => c.id === receipt.checkId);
|
|
555
|
+
if (check && receipt.contractDigest === current.digest && receipt.contractRevision === current.revision && receipt.argvDigest === argvDigest(check.command, check.args) && reconciledReceipt(receipt, journal))
|
|
556
|
+
receipts.set(check.id, receipt);
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
matching(command, args) {
|
|
560
|
+
return current.contract.requiredChecks.filter((check) => argvDigest(check.command, check.args) === argvDigest(command, args));
|
|
561
|
+
},
|
|
562
|
+
begin(checkIds) {
|
|
563
|
+
for (const id of checkIds)
|
|
564
|
+
receipts.delete(id);
|
|
565
|
+
},
|
|
566
|
+
record(checkId, before, after, result, source) {
|
|
567
|
+
const check = current.contract.requiredChecks.find((c) => c.id === checkId);
|
|
568
|
+
if (!check)
|
|
569
|
+
throw new Error("TASK_ACCEPTANCE_UNKNOWN_CHECK");
|
|
570
|
+
const receipt = taskCheckReceiptSchema.parse({
|
|
571
|
+
schemaVersion: 1,
|
|
572
|
+
checkId,
|
|
573
|
+
contractDigest: current.digest,
|
|
574
|
+
contractRevision: current.revision,
|
|
575
|
+
...before,
|
|
576
|
+
...source ? { source: { id: source.id, name: source.name, ...source.startedAt !== undefined ? { startedAt: source.startedAt } : {} } } : {},
|
|
577
|
+
argvDigest: argvDigest(check.command, check.args),
|
|
578
|
+
exitCode: result.exitCode,
|
|
579
|
+
timedOut: result.timedOut,
|
|
580
|
+
unchanged: before.runId === after.runId && before.executionIdentity === after.executionIdentity && before.patchId === after.patchId && before.snapshotDigest === after.snapshotDigest
|
|
581
|
+
});
|
|
582
|
+
receipts.set(checkId, receipt);
|
|
583
|
+
return structuredClone(receipt);
|
|
584
|
+
},
|
|
585
|
+
missing(binding) {
|
|
586
|
+
return current.contract.requiredChecks.filter((check) => {
|
|
587
|
+
const receipt = receipts.get(check.id);
|
|
588
|
+
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;
|
|
589
|
+
}).map((check) => check.id);
|
|
590
|
+
},
|
|
591
|
+
snapshot() {
|
|
592
|
+
return [...receipts.values()].map((receipt) => structuredClone(receipt));
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
597
|
+
var commandDigest = (value) => {
|
|
598
|
+
const input = object(value);
|
|
599
|
+
return typeof input?.command === "string" && Array.isArray(input.args) && input.args.every((arg) => typeof arg === "string") ? argvDigest(input.command, input.args) : undefined;
|
|
600
|
+
};
|
|
601
|
+
function reconciledReceipt(receipt, journal) {
|
|
602
|
+
if (!receipt.source)
|
|
603
|
+
return false;
|
|
604
|
+
const matches = journal.filter((row2) => row2.runId === receipt.runId && row2.providerToolCallId === receipt.source.id && row2.toolName === receipt.source.name);
|
|
605
|
+
if (matches.length !== 1)
|
|
606
|
+
return false;
|
|
607
|
+
const row = matches[0];
|
|
608
|
+
const startedAt = receipt.source.startedAt ?? row.startedAt;
|
|
609
|
+
if (row.status !== "completed" || !Number.isFinite(startedAt) || !Number.isFinite(row.completedAt) || row.completedAt < startedAt)
|
|
610
|
+
return false;
|
|
611
|
+
if (!["run_environment_command", "run_check", "verify_and_apply_environment_patch", "verify_and_apply_reviewed_edits"].includes(row.toolName))
|
|
612
|
+
return false;
|
|
613
|
+
const output = object(row.output), result = object(output?.verification) ?? output;
|
|
614
|
+
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)
|
|
615
|
+
return false;
|
|
616
|
+
if (row.toolName !== "run_check" && commandDigest(row.input) !== receipt.argvDigest)
|
|
617
|
+
return false;
|
|
618
|
+
for (const attempt of journal) {
|
|
619
|
+
if (attempt === row || attempt.runId !== receipt.runId)
|
|
620
|
+
continue;
|
|
621
|
+
if (attempt.status === "pending" || attempt.status === "running")
|
|
622
|
+
return false;
|
|
623
|
+
const input = object(attempt.input);
|
|
624
|
+
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);
|
|
625
|
+
if (sameCheck && (!Number.isFinite(attempt.completedAt) || attempt.completedAt >= startedAt))
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/runtime/task-acceptance-delivery.ts
|
|
632
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
633
|
+
import { z as z3 } from "zod";
|
|
634
|
+
var scopes = new AsyncLocalStorage;
|
|
635
|
+
function withTaskAcceptanceDelivery(workspace, ledger, work, previousEvidence, journal = []) {
|
|
636
|
+
const latest = ledger.revisions.at(-1);
|
|
637
|
+
const previous = z3.object({ contractDigest: z3.literal(latest.digest), contractRevision: z3.literal(latest.revision), diagnostic: z3.strictObject({
|
|
638
|
+
code: z3.literal("TASK_ACCEPTANCE_SCOPE_VIOLATION"),
|
|
639
|
+
runId: z3.string().max(512),
|
|
640
|
+
patchId: z3.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
641
|
+
paths: z3.array(z3.string().max(1024)).max(256)
|
|
642
|
+
}) }).safeParse(previousEvidence);
|
|
643
|
+
const previousDelivery = z3.object({ contractDigest: z3.literal(latest.digest), contractRevision: z3.literal(latest.revision), delivery: z3.strictObject({
|
|
644
|
+
runId: z3.string().min(1).max(512),
|
|
645
|
+
executionIdentity: z3.string().min(1).max(512),
|
|
646
|
+
patchId: z3.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
647
|
+
snapshotDigest: z3.string().regex(/^sha256:[a-f0-9]{64}$/)
|
|
648
|
+
}) }).safeParse(previousEvidence);
|
|
649
|
+
return scopes.run({
|
|
650
|
+
...previousDelivery.success ? { previousDelivery: previousDelivery.data.delivery } : {},
|
|
651
|
+
workspace,
|
|
652
|
+
revision: latest.revision,
|
|
653
|
+
digest: latest.digest,
|
|
654
|
+
checks: createTaskAcceptanceChecks(ledger, previousEvidence && typeof previousEvidence === "object" && "checks" in previousEvidence ? previousEvidence.checks : [], journal),
|
|
655
|
+
allowed: new Set(latest.contract.allowedWritePaths),
|
|
656
|
+
protected: new Set(latest.contract.protectedFiles.map((file) => file.toLowerCase())),
|
|
657
|
+
...previous.success ? { violation: previous.data.diagnostic } : {}
|
|
658
|
+
}, work);
|
|
659
|
+
}
|
|
660
|
+
function assertTaskAcceptanceImport(input) {
|
|
661
|
+
const scope = scopes.getStore();
|
|
662
|
+
if (!scope)
|
|
663
|
+
return;
|
|
664
|
+
const blocked = input.entries.filter((entry) => {
|
|
665
|
+
const candidate = entry.path.toLowerCase();
|
|
666
|
+
return input.workspace !== scope.workspace || !scope.allowed.has(entry.path) || [...scope.protected].some((file) => candidate === file || candidate.startsWith(file + "/") || file.startsWith(candidate + "/"));
|
|
667
|
+
});
|
|
668
|
+
if (blocked.length) {
|
|
669
|
+
scope.violation = { code: "TASK_ACCEPTANCE_SCOPE_VIOLATION", runId: input.runId, patchId: input.patchId, paths: blocked.map((entry) => entry.path).slice(0, 256) };
|
|
670
|
+
throw new Error("TASK_ACCEPTANCE_SCOPE_VIOLATION: the OCI patch touches files outside the task contract; no host import was performed.");
|
|
671
|
+
}
|
|
672
|
+
if (!input.executionIdentity || !input.snapshotDigest || scope.checks.missing({ runId: input.runId, patchId: input.patchId, executionIdentity: input.executionIdentity, snapshotDigest: input.snapshotDigest }).length) {
|
|
673
|
+
throw new Error("TASK_ACCEPTANCE_CHECKS_MISSING: every required check must succeed on the exact final snapshot and patch before import.");
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
var taskAcceptanceChecks = () => scopes.getStore()?.checks;
|
|
677
|
+
function taskAcceptanceDeliveryDiagnostic(digest2, revision) {
|
|
678
|
+
const scope = scopes.getStore();
|
|
679
|
+
return scope?.digest === digest2 && scope.revision === revision && scope.violation ? structuredClone(scope.violation) : undefined;
|
|
680
|
+
}
|
|
681
|
+
function confirmTaskAcceptanceImport(binding, validate) {
|
|
682
|
+
const scope = scopes.getStore();
|
|
683
|
+
if (scope)
|
|
684
|
+
scope.imported = { binding: structuredClone(binding), validate };
|
|
685
|
+
}
|
|
686
|
+
async function evaluateTaskAcceptanceOutcome(state, ledger) {
|
|
687
|
+
const current = ledger.revisions.at(-1), scope = scopes.getStore();
|
|
688
|
+
const terminal = ["completed", "failed", "cancelled", "timed_out"].includes(state.status);
|
|
689
|
+
if (!scope || scope.digest !== current.digest || scope.revision !== current.revision) {
|
|
690
|
+
return { status: terminal ? "incomplete" : "pending", reason: "TASK_ACCEPTANCE_NO_DELIVERY_EVIDENCE" };
|
|
691
|
+
}
|
|
692
|
+
if (scope.violation)
|
|
693
|
+
return { status: "failed", reason: scope.violation.code };
|
|
694
|
+
if (state.status !== "completed")
|
|
695
|
+
return { status: state.status === "failed" ? "failed" : terminal ? "incomplete" : "pending", reason: "TASK_ACCEPTANCE_RUN_NOT_COMPLETED" };
|
|
696
|
+
if (state.finishReason !== "stop")
|
|
697
|
+
return { status: "incomplete", reason: "TASK_ACCEPTANCE_RUN_NOT_FINISHED" };
|
|
698
|
+
const imported = scope.imported;
|
|
699
|
+
if (!imported || imported.binding.runId !== state.runId)
|
|
700
|
+
return { status: "incomplete", reason: "TASK_ACCEPTANCE_IMPORT_UNCONFIRMED" };
|
|
701
|
+
const delivery = { ...imported.binding };
|
|
702
|
+
if (scope.checks.missing(delivery).length)
|
|
703
|
+
return { status: "incomplete", reason: "TASK_ACCEPTANCE_CHECKS_MISSING", delivery };
|
|
704
|
+
try {
|
|
705
|
+
if (!await imported.validate())
|
|
706
|
+
return { status: "incomplete", reason: "TASK_ACCEPTANCE_DELIVERY_DRIFT", delivery };
|
|
707
|
+
} catch {
|
|
708
|
+
return { status: "incomplete", reason: "TASK_ACCEPTANCE_DELIVERY_INSPECTION_FAILED", delivery };
|
|
709
|
+
}
|
|
710
|
+
return { status: current.contract.humanReview.length ? "pending_review" : "verified", reason: current.contract.humanReview.length ? "TASK_ACCEPTANCE_HUMAN_REVIEW_REQUIRED" : "TASK_ACCEPTANCE_VERIFIED", delivery };
|
|
711
|
+
}
|
|
712
|
+
async function taskAcceptanceOutcome(state, ledger) {
|
|
713
|
+
const outcome = await evaluateTaskAcceptanceOutcome(state, ledger);
|
|
714
|
+
const scope = scopes.getStore(), latest = ledger.revisions.at(-1);
|
|
715
|
+
const delivery = scope?.digest === latest.digest && scope.revision === latest.revision ? scope.imported?.binding ?? scope.previousDelivery : undefined;
|
|
716
|
+
return { ...outcome, ...delivery && delivery.runId === state.runId ? { delivery: structuredClone(delivery) } : {} };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export { EDIT_CONTRACT_SCHEMA_VERSION, EDIT_DIGEST_ALGORITHM, MAX_EDIT_CHANGES, MAX_EDIT_FILE_BYTES, MAX_EDIT_PROPOSAL_BYTES, fileDigestSchema, workspaceFilePathSchema, editChangeSchema, editChangesSchema, editProposalInputSchema, editProposalChangeSchema, editProposalSchema, applyEditProposalInputSchema, moveFileInputSchema, quarantineFileInputSchema, restoreFileInputSchema, mutationOperationSchema, mutationAuditEntrySchema, createEditProposal, validateEditProposal, editContractDocument, protectPersistenceSecret, acquireSqliteAccess, exclusiveSqliteAccessDescriptor, adoptExclusiveSqliteAccess, SqliteDatabase, UnsafeFileTypeError, FileSizeLimitError, FileChangedWhileReadingError, statRegularFileNoFollow, readRegularFileNoFollow, readRegularFileChunksNoFollow, taskCheckReceiptSchema, createTaskAcceptanceChecks, withTaskAcceptanceDelivery, assertTaskAcceptanceImport, taskAcceptanceChecks, taskAcceptanceDeliveryDiagnostic, confirmTaskAcceptanceImport, taskAcceptanceOutcome };
|