@vela-science/canopus 0.6.5 → 0.8.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 +97 -0
- package/README.md +67 -139
- package/SECURITY.md +3 -2
- package/capsules/erdos1056-k15/bin/linux-arm64/{10428801-10429000 → 10429201-10429400}/verifier +0 -0
- package/capsules/erdos1056-k15/bin/linux-x86_64/{10428801-10429000 → 10429201-10429400}/verifier +0 -0
- package/compatibility.json +25 -0
- package/dist/src/activity/events.d.ts +4 -1
- package/dist/src/activity/events.js +8 -2
- package/dist/src/activity/store.d.ts +2 -2
- package/dist/src/activity/store.js +4 -3
- package/dist/src/{receipt/map.d.ts → candidate/finalize.d.ts} +2 -9
- package/dist/src/candidate/finalize.js +71 -0
- package/dist/src/cli.js +88 -229
- package/dist/src/contracts/mission.d.ts +1 -13
- package/dist/src/contracts/mission.js +4 -42
- package/dist/src/contracts/validation.d.ts +1 -25
- package/dist/src/contracts/validation.js +1 -95
- package/dist/src/engines/codex-tools-native.js +2 -2
- package/dist/src/index.d.ts +5 -34
- package/dist/src/index.js +4 -34
- package/dist/src/mission/prepare.d.ts +10 -1
- package/dist/src/mission/prepare.js +29 -30
- package/dist/src/product/coverage.d.ts +17 -0
- package/dist/src/product/coverage.js +137 -0
- package/dist/src/product/doctor.d.ts +1 -2
- package/dist/src/product/doctor.js +29 -8
- package/dist/src/product/profile.js +8 -2
- package/dist/src/product/replay.js +11 -12
- package/dist/src/product/run.d.ts +5 -9
- package/dist/src/product/run.js +17 -88
- package/dist/src/product/submission.d.ts +40 -0
- package/dist/src/product/submission.js +217 -0
- package/dist/src/product/submit.d.ts +22 -0
- package/dist/src/product/submit.js +264 -0
- package/dist/src/product/version.d.ts +2 -2
- package/dist/src/product/version.js +27 -4
- package/dist/src/projection/current-run.d.ts +12 -0
- package/dist/src/projection/current-run.js +94 -0
- package/dist/src/projection/failure.d.ts +17 -7
- package/dist/src/projection/failure.js +43 -12
- package/dist/src/projection/retained-mission.d.ts +6 -0
- package/dist/src/projection/retained-mission.js +41 -0
- package/dist/src/projection/retained-run.d.ts +7 -0
- package/dist/src/projection/retained-run.js +50 -0
- package/dist/src/run.d.ts +43 -41
- package/dist/src/run.js +34 -412
- package/dist/src/util/canonical.d.ts +1 -5
- package/dist/src/util/canonical.js +1 -53
- package/dist/src/vela/cli.d.ts +7 -51
- package/dist/src/vela/cli.js +102 -523
- package/dist/src/vela/types.d.ts +3 -19
- package/dist/src/verifier/run.js +1 -1
- package/dist/src/workspace/prepare.d.ts +1 -1
- package/dist/src/workspace/prepare.js +15 -15
- package/docs/EVALUATION.md +142 -0
- package/docs/MISSIONS.md +42 -80
- package/docs/RUN_RECORD.md +77 -99
- package/docs/adr/0010-nonmutating-runs-and-explicit-submission.md +191 -0
- package/missions/erdos1056-k15-next/mission.draft.json +10 -8
- package/package.json +18 -22
- package/profiles/{erdos1056-k15-10428801-10429000.json → erdos1056-k15-10429201-10429400.json} +10 -10
- package/schemas/failure.v1.json +17 -0
- package/schemas/mission.v0.json +3 -4
- package/schemas/mission.v1.json +7 -28
- package/schemas/{run.v1.json → run.v2.json} +58 -116
- package/schemas/submission-bundle.v1.json +63 -0
- package/toolchain.lock.json +31 -0
- package/BUILD_WEEK.md +0 -198
- package/advisories/erdos1056-claim-fidelity/output.schema.json +0 -90
- package/advisories/erdos1056-claim-fidelity/registration.json +0 -42
- package/advisories/erdos1056-claim-fidelity/results/assessment.json +0 -1
- package/advisories/erdos1056-claim-fidelity/results/verification.json +0 -1
- package/capsules/formal-erdos-505-test-dim-one/verifier +0 -105
- package/capsules/quantum-10-1-4/verifier.py +0 -152
- package/capsules/sidon-a24/bin/linux-arm64/verifier +0 -0
- package/capsules/sidon-a24/bin/linux-x86_64/verifier +0 -0
- package/dist/src/capability/withdrawal.d.ts +0 -47
- package/dist/src/capability/withdrawal.js +0 -487
- package/dist/src/engines/codex-exec.d.ts +0 -17
- package/dist/src/engines/codex-exec.js +0 -136
- package/dist/src/engines/codex-tool-free.d.ts +0 -26
- package/dist/src/engines/codex-tool-free.js +0 -213
- package/dist/src/product/withdraw.d.ts +0 -8
- package/dist/src/product/withdraw.js +0 -255
- package/dist/src/projection/diagnostic.d.ts +0 -12
- package/dist/src/projection/diagnostic.js +0 -61
- package/dist/src/projection/public-run.d.ts +0 -59
- package/dist/src/projection/public-run.js +0 -106
- package/dist/src/projection/publication.d.ts +0 -36
- package/dist/src/projection/publication.js +0 -47
- package/dist/src/projection/run.d.ts +0 -95
- package/dist/src/projection/run.js +0 -173
- package/dist/src/receipt/map.js +0 -168
- package/dist/src/repair/plan.d.ts +0 -2
- package/dist/src/repair/plan.js +0 -22
- package/dist/src/roles.d.ts +0 -2
- package/dist/src/roles.js +0 -9
- package/docs/RELEASES.md +0 -1389
- package/evidence/build-week/run_eb6bcd46-cffd-4ae8-b630-2681bd84da71.public.json +0 -1
- package/evidence/build-week/run_f68e4cfc-e5c7-4c73-86cb-d79807c47ec4.public.json +0 -1
- package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/pending-commands.json +0 -1
- package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/public-run.json +0 -1
- package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/root-manifest.json +0 -1
- package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/web-import.json +0 -1
- package/evidence/erdos/run_b3b0cf07-b149-4d53-8258-76ba0e1fc0a5/pending-commands.json +0 -1
- package/evidence/erdos/run_b3b0cf07-b149-4d53-8258-76ba0e1fc0a5/public-run.json +0 -1
- package/evidence/erdos/run_b3b0cf07-b149-4d53-8258-76ba0e1fc0a5/root-manifest.json +0 -1
- package/evidence/erdos/run_b3b0cf07-b149-4d53-8258-76ba0e1fc0a5/web-import.json +0 -1
- package/missions/formal-erdos-505-test-dim-one-gpt56/mission.draft.json +0 -40
- package/missions/quantum-10-1-4-retry/mission.draft.json +0 -40
- package/missions/sidon-a24-at-least-7194-gpt56/mission.draft.json +0 -42
- package/missions/sidon-a24-at-least-7194-gpt56-v2/mission.draft.json +0 -42
- package/missions/sidon-a24-at-least-7194-gpt56-v3/mission.draft.json +0 -42
- package/profiles/formal-erdos-505-test-dim-one-gpt56.json +0 -35
- package/profiles/quantum-10-1-4-stabilizer-retry.json +0 -35
- package/profiles/sidon-a24-at-least-7194-gpt56-v2.json +0 -35
- package/profiles/sidon-a24-at-least-7194-gpt56-v3.json +0 -35
- package/profiles/sidon-a24-at-least-7194-gpt56.json +0 -35
- package/schemas/canopus.public-run.v1.json +0 -116
- package/scripts/run-claim-fidelity-advisory.mjs +0 -235
package/dist/src/vela/cli.js
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { GIT_OBJECT_RE, SHA256_RE, objectAt, relativePathAt, stringAt, } from "../contracts/validation.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { MAX_EXECUTABLE_BYTES,
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
export const DEFAULT_VELA_COMMAND_TIMEOUT_MS =
|
|
12
|
-
export function retainedArtifactPath(repoRoot, frontier, digest) {
|
|
13
|
-
if (!/^sha256:[0-9a-f]{64}$/u.test(digest)) {
|
|
14
|
-
throw new VelaClientError("malformed_output", "artifact digest is not sha256");
|
|
15
|
-
}
|
|
16
|
-
return path.join(repoRoot, frontier, "records", "artifacts", "sha256", digest.slice("sha256:".length));
|
|
17
|
-
}
|
|
3
|
+
import { isolatedEnvironment, runCommand, } from "../util/command.js";
|
|
4
|
+
import { sha256Bytes } from "../util/canonical.js";
|
|
5
|
+
import { MAX_EXECUTABLE_BYTES, sha256RegularFile } from "../util/files.js";
|
|
6
|
+
// Large frontiers can spend several minutes in Vela's recoverable transaction
|
|
7
|
+
// preparation, exact replay, and derived-view materialization. This ceiling is
|
|
8
|
+
// bounded independently from model, verifier, and mission budgets: it prevents
|
|
9
|
+
// Canopus from killing a healthy authority transaction while still stopping a
|
|
10
|
+
// stalled Vela subprocess.
|
|
11
|
+
export const DEFAULT_VELA_COMMAND_TIMEOUT_MS = 600_000;
|
|
18
12
|
export class VelaClientError extends Error {
|
|
19
13
|
code;
|
|
20
14
|
constructor(code, message) {
|
|
@@ -38,13 +32,6 @@ function parseJsonObject(stdout, command) {
|
|
|
38
32
|
throw new VelaClientError("malformed_output", String(error));
|
|
39
33
|
}
|
|
40
34
|
}
|
|
41
|
-
function usesCompactStatusProjection(version) {
|
|
42
|
-
const match = /^0\.(\d+)\.(\d+)$/u.exec(version);
|
|
43
|
-
if (match === null)
|
|
44
|
-
return false;
|
|
45
|
-
const minor = Number(match[1]);
|
|
46
|
-
return Number.isSafeInteger(minor) && minor >= 900 && minor <= 915;
|
|
47
|
-
}
|
|
48
35
|
function safeFailureMessage(value) {
|
|
49
36
|
if (typeof value !== "string" || value.length === 0)
|
|
50
37
|
return undefined;
|
|
@@ -99,20 +86,6 @@ function commandFailureSummary(result) {
|
|
|
99
86
|
`stderr_sha256=${sha256Bytes(result.stderr)}`,
|
|
100
87
|
].join("; ");
|
|
101
88
|
}
|
|
102
|
-
function bool(value, at) {
|
|
103
|
-
if (typeof value !== "boolean") {
|
|
104
|
-
throw new VelaClientError("malformed_output", `${at} must be a boolean`);
|
|
105
|
-
}
|
|
106
|
-
return value;
|
|
107
|
-
}
|
|
108
|
-
function nullableCount(value, at) {
|
|
109
|
-
if (value === null)
|
|
110
|
-
return null;
|
|
111
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
112
|
-
throw new VelaClientError("malformed_output", `${at} must be a nonnegative integer or null`);
|
|
113
|
-
}
|
|
114
|
-
return value;
|
|
115
|
-
}
|
|
116
89
|
function normalizeSha256(value, at) {
|
|
117
90
|
if (typeof value !== "string") {
|
|
118
91
|
throw new VelaClientError("malformed_output", `${at} must be a SHA-256 string`);
|
|
@@ -142,8 +115,7 @@ function assertEqual(actual, expected, label) {
|
|
|
142
115
|
function compareRoots(actual, expected) {
|
|
143
116
|
assertEqual(actual.git_commit, expected.git_commit, "Git commit");
|
|
144
117
|
assertEqual(actual.git_tree, expected.git_tree, "Git tree");
|
|
145
|
-
assertEqual(actual.
|
|
146
|
-
assertEqual(actual.vela_snapshot, expected.vela_snapshot, "Vela snapshot");
|
|
118
|
+
assertEqual(actual.vela_repository, expected.vela_repository, "Vela repository");
|
|
147
119
|
}
|
|
148
120
|
function nonnegativeInteger(value, at) {
|
|
149
121
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
@@ -151,174 +123,6 @@ function nonnegativeInteger(value, at) {
|
|
|
151
123
|
}
|
|
152
124
|
return value;
|
|
153
125
|
}
|
|
154
|
-
function strictSignalCheck(check) {
|
|
155
|
-
if (!Array.isArray(check.checks)) {
|
|
156
|
-
throw new VelaClientError("malformed_output", "vela check.checks must be an array");
|
|
157
|
-
}
|
|
158
|
-
let signalCheck;
|
|
159
|
-
for (const [index, value] of check.checks.entries()) {
|
|
160
|
-
const entry = fieldObject({ entry: value }, "entry", `vela check.checks[${index}]`);
|
|
161
|
-
const id = stringAt(entry.id, `vela check.checks[${index}].id`, { min: 1, max: 128 });
|
|
162
|
-
const status = stringAt(entry.status, `vela check.checks[${index}].status`, {
|
|
163
|
-
min: 1,
|
|
164
|
-
max: 32,
|
|
165
|
-
});
|
|
166
|
-
if (id === "signals") {
|
|
167
|
-
if (signalCheck !== undefined) {
|
|
168
|
-
throw new VelaClientError("malformed_output", "vela check contains duplicate signals checks");
|
|
169
|
-
}
|
|
170
|
-
signalCheck = entry;
|
|
171
|
-
}
|
|
172
|
-
else if (status !== "pass") {
|
|
173
|
-
throw new VelaClientError("command_failed", `vela strict check failed outside the registered signals baseline: ${id}=${status}`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (signalCheck === undefined) {
|
|
177
|
-
throw new VelaClientError("malformed_output", "vela check omitted the signals check");
|
|
178
|
-
}
|
|
179
|
-
return signalCheck;
|
|
180
|
-
}
|
|
181
|
-
export function strictBaselineFromCheck(check) {
|
|
182
|
-
const summary = fieldObject(check, "summary", "vela check");
|
|
183
|
-
if (summary.strict !== true) {
|
|
184
|
-
throw new VelaClientError("malformed_output", "vela check summary is not strict");
|
|
185
|
-
}
|
|
186
|
-
if (nonnegativeInteger(summary.errors, "vela check.summary.errors") !== 0 ||
|
|
187
|
-
nonnegativeInteger(summary.invalid_findings, "vela check.summary.invalid_findings") !== 0) {
|
|
188
|
-
throw new VelaClientError("command_failed", "vela strict check contains structural errors or invalid findings");
|
|
189
|
-
}
|
|
190
|
-
const signalCheck = strictSignalCheck(check);
|
|
191
|
-
if (!Array.isArray(signalCheck.blockers)) {
|
|
192
|
-
throw new VelaClientError("malformed_output", "vela check signals.blockers must be an array");
|
|
193
|
-
}
|
|
194
|
-
const blockers = signalCheck.blockers.map((value, index) => fieldObject({ blocker: value }, "blocker", `vela check signals.blockers[${index}]`));
|
|
195
|
-
const blocker_count = nonnegativeInteger(signalCheck.failed, "vela check signals.failed");
|
|
196
|
-
if (blockers.length !== blocker_count) {
|
|
197
|
-
throw new VelaClientError("malformed_output", "vela check signals blocker array and failed count disagree");
|
|
198
|
-
}
|
|
199
|
-
const counts = new Map();
|
|
200
|
-
for (const [index, blocker] of blockers.entries()) {
|
|
201
|
-
const kind = stringAt(blocker.kind, `vela check signals.blockers[${index}].kind`, {
|
|
202
|
-
min: 1,
|
|
203
|
-
max: 128,
|
|
204
|
-
pattern: /^[a-z][a-z0-9_]*$/u,
|
|
205
|
-
});
|
|
206
|
-
counts.set(kind, (counts.get(kind) ?? 0) + 1);
|
|
207
|
-
}
|
|
208
|
-
const canonicalBlockers = blockers
|
|
209
|
-
.map((blocker) => canonicalJcs(blocker))
|
|
210
|
-
.sort()
|
|
211
|
-
.map((serialized) => JSON.parse(serialized));
|
|
212
|
-
const status = blocker_count === 0 ? "pass" : "fail";
|
|
213
|
-
const signalStatus = stringAt(signalCheck.status, "vela check signals.status", {
|
|
214
|
-
min: 1,
|
|
215
|
-
max: 32,
|
|
216
|
-
});
|
|
217
|
-
if (signalStatus !== status || summary.status !== status || bool(check.ok, "vela check.ok") !== (status === "pass")) {
|
|
218
|
-
throw new VelaClientError("malformed_output", "vela strict status fields disagree");
|
|
219
|
-
}
|
|
220
|
-
return {
|
|
221
|
-
status,
|
|
222
|
-
blocker_count,
|
|
223
|
-
blockers_root: sha256Bytes(canonicalJcs(canonicalBlockers)),
|
|
224
|
-
rule_counts: [...counts.entries()]
|
|
225
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
226
|
-
.map(([rule, count]) => ({ rule, count })),
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
function assertStrictBaseline(actual, expected) {
|
|
230
|
-
if (expected === undefined) {
|
|
231
|
-
if (actual.status !== "pass") {
|
|
232
|
-
throw new VelaClientError("command_failed", `vela strict check has ${actual.blocker_count} unregistered blockers`);
|
|
233
|
-
}
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
if (canonicalJcs(actual) !== canonicalJcs(expected)) {
|
|
237
|
-
throw new VelaClientError("root_mismatch", `strict baseline mismatch: expected ${canonicalJcs(expected)}, observed ${canonicalJcs(actual)}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
export function validateLandResult(mission, raw) {
|
|
241
|
-
if (raw.ok !== true || raw.command !== "land") {
|
|
242
|
-
throw new VelaClientError("malformed_output", "vela land did not return its success envelope");
|
|
243
|
-
}
|
|
244
|
-
const rawRoute = stringAt(raw.route, "vela land.route", { min: 1, max: 64 });
|
|
245
|
-
const route = rawRoute === "policy_admitted"
|
|
246
|
-
? "permit"
|
|
247
|
-
: rawRoute === "deferred"
|
|
248
|
-
? "defer"
|
|
249
|
-
: rawRoute === "exact_retry"
|
|
250
|
-
? "exact_retry"
|
|
251
|
-
: undefined;
|
|
252
|
-
if (route === undefined) {
|
|
253
|
-
throw new VelaClientError("malformed_output", `unknown landing route ${rawRoute}`);
|
|
254
|
-
}
|
|
255
|
-
const detail = stringAt(raw.detail, "vela land.detail", { max: 8192 });
|
|
256
|
-
const rawOriginalRoute = raw.original_route;
|
|
257
|
-
const originalRoute = route === "exact_retry"
|
|
258
|
-
? rawOriginalRoute === "policy_admitted"
|
|
259
|
-
? "permit"
|
|
260
|
-
: rawOriginalRoute === "deferred"
|
|
261
|
-
? "defer"
|
|
262
|
-
: undefined
|
|
263
|
-
: rawOriginalRoute === null
|
|
264
|
-
? null
|
|
265
|
-
: undefined;
|
|
266
|
-
if (originalRoute === undefined) {
|
|
267
|
-
throw new VelaClientError("malformed_output", "landing returned an invalid structured original_route");
|
|
268
|
-
}
|
|
269
|
-
const effectiveRoute = route === "exact_retry" ? originalRoute : route;
|
|
270
|
-
if (effectiveRoute === null) {
|
|
271
|
-
throw new VelaClientError("malformed_output", "landing has no effective route");
|
|
272
|
-
}
|
|
273
|
-
if (!mission.landing.expected_routes.includes(effectiveRoute)) {
|
|
274
|
-
throw new VelaClientError("unexpected_route", `landing route ${effectiveRoute} was not registered by the mission`);
|
|
275
|
-
}
|
|
276
|
-
const before = nullableCount(raw.accepted_event_count_before, "vela land.accepted_event_count_before");
|
|
277
|
-
const after = nullableCount(raw.accepted_event_count_after, "vela land.accepted_event_count_after");
|
|
278
|
-
const delta = nullableCount(raw.accepted_event_delta, "vela land.accepted_event_delta");
|
|
279
|
-
if ((before === null) !== (after === null) || (before === null) !== (delta === null)) {
|
|
280
|
-
throw new VelaClientError("malformed_output", "landing returned a partial accepted-event count tuple");
|
|
281
|
-
}
|
|
282
|
-
if (before !== null && after !== null && delta !== null && (after < before || delta !== after - before)) {
|
|
283
|
-
throw new VelaClientError("malformed_output", "landing accepted-event delta is inconsistent");
|
|
284
|
-
}
|
|
285
|
-
if (delta !== null && delta > mission.landing.max_accepted_delta) {
|
|
286
|
-
throw new VelaClientError("unexpected_route", `accepted-event delta ${delta} exceeds registered maximum`);
|
|
287
|
-
}
|
|
288
|
-
const expectedDelta = effectiveRoute === "permit" ? 1 : 0;
|
|
289
|
-
if (delta !== null && delta !== expectedDelta) {
|
|
290
|
-
throw new VelaClientError("malformed_output", `${effectiveRoute} durable landing requires accepted-event delta ${expectedDelta}`);
|
|
291
|
-
}
|
|
292
|
-
if (route !== "exact_retry" && delta === null) {
|
|
293
|
-
throw new VelaClientError("malformed_output", "new landing omitted accepted-event counts");
|
|
294
|
-
}
|
|
295
|
-
const publication = fieldObject(raw, "publication", "vela land");
|
|
296
|
-
const publicationState = stringAt(publication.state, "vela land.publication.state", {
|
|
297
|
-
min: 1,
|
|
298
|
-
max: 64,
|
|
299
|
-
});
|
|
300
|
-
if (publicationState !== "committed_local" &&
|
|
301
|
-
publicationState !== "pushed" &&
|
|
302
|
-
publicationState !== "unchanged") {
|
|
303
|
-
throw new VelaClientError("command_failed", `vela land was durable but not Git-published: ${publicationState}`);
|
|
304
|
-
}
|
|
305
|
-
return {
|
|
306
|
-
operationId: stringAt(raw.operation_id, "vela land.operation_id", { min: 1, max: 256 }),
|
|
307
|
-
receiptRoot: normalizeSha256(raw.receipt_root, "vela land.receipt_root"),
|
|
308
|
-
recordId: stringAt(raw.record_id, "vela land.record_id", { min: 1, max: 256 }),
|
|
309
|
-
proposalId: stringAt(raw.proposal_id, "vela land.proposal_id", { min: 1, max: 256 }),
|
|
310
|
-
findingId: stringAt(raw.finding_id, "vela land.finding_id", { min: 1, max: 256 }),
|
|
311
|
-
route,
|
|
312
|
-
originalRoute,
|
|
313
|
-
rawRoute,
|
|
314
|
-
detail,
|
|
315
|
-
acceptedEventCountBefore: before,
|
|
316
|
-
acceptedEventCountAfter: after,
|
|
317
|
-
acceptedEventDelta: delta,
|
|
318
|
-
publication,
|
|
319
|
-
raw,
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
126
|
export class VelaClient {
|
|
323
127
|
#binary;
|
|
324
128
|
#expectedVersion;
|
|
@@ -335,11 +139,24 @@ export class VelaClient {
|
|
|
335
139
|
this.#home = options.home;
|
|
336
140
|
this.#maxOutputBytes = options.maxOutputBytes ?? 16 * 1024 * 1024;
|
|
337
141
|
this.#timeoutMs = options.timeoutMs ?? DEFAULT_VELA_COMMAND_TIMEOUT_MS;
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
|
|
142
|
+
// The control lane accepts only the standard repository-authority agent
|
|
143
|
+
// socket. It never accepts caller-supplied environment entries or key
|
|
144
|
+
// bytes, and this environment is not reused by the Codex worker or
|
|
145
|
+
// verifier.
|
|
146
|
+
const baseEnvironment = isolatedEnvironment(options.home);
|
|
147
|
+
if (options.repositoryAuthorityAgentSocket === undefined) {
|
|
148
|
+
this.#env = baseEnvironment;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
if (!path.isAbsolute(options.repositoryAuthorityAgentSocket) ||
|
|
152
|
+
options.repositoryAuthorityAgentSocket.includes("\0")) {
|
|
153
|
+
throw new VelaClientError("malformed_output", "repository authority agent socket must be one absolute path");
|
|
154
|
+
}
|
|
155
|
+
this.#env = {
|
|
156
|
+
...baseEnvironment,
|
|
157
|
+
SSH_AUTH_SOCK: options.repositoryAuthorityAgentSocket,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
343
160
|
this.#runner = options.runner ?? runCommand;
|
|
344
161
|
}
|
|
345
162
|
async #execute(argv, cwd) {
|
|
@@ -359,46 +176,6 @@ export class VelaClient {
|
|
|
359
176
|
const result = await this.#execute([this.#binary, ...args], cwd);
|
|
360
177
|
return parseJsonObject(result.stdout, label);
|
|
361
178
|
}
|
|
362
|
-
async #strictCheck(frontier, cwd, baseline) {
|
|
363
|
-
const argv = [this.#binary, "check", frontier, "--strict", "--json"];
|
|
364
|
-
const result = await this.#runner({
|
|
365
|
-
argv,
|
|
366
|
-
cwd,
|
|
367
|
-
env: this.#env,
|
|
368
|
-
timeoutMs: this.#timeoutMs,
|
|
369
|
-
maxOutputBytes: this.#maxOutputBytes,
|
|
370
|
-
});
|
|
371
|
-
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
372
|
-
throw new VelaClientError("command_failed", `${argv[0]} ${argv.slice(1).join(" ")} exited ${result.exitCode}: ${commandFailureSummary(result)}`);
|
|
373
|
-
}
|
|
374
|
-
const check = parseJsonObject(result.stdout, "vela check");
|
|
375
|
-
const actual = strictBaselineFromCheck(check);
|
|
376
|
-
if ((result.exitCode === 0) !== (actual.status === "pass")) {
|
|
377
|
-
throw new VelaClientError("malformed_output", "vela strict exit code and status disagree");
|
|
378
|
-
}
|
|
379
|
-
assertStrictBaseline(actual, baseline);
|
|
380
|
-
return check;
|
|
381
|
-
}
|
|
382
|
-
async observeStrictBaseline(repoRoot, frontier) {
|
|
383
|
-
const safeFrontier = frontierPath(frontier);
|
|
384
|
-
const argv = [this.#binary, "check", safeFrontier, "--strict", "--json"];
|
|
385
|
-
const result = await this.#runner({
|
|
386
|
-
argv,
|
|
387
|
-
cwd: repoRoot,
|
|
388
|
-
env: this.#env,
|
|
389
|
-
timeoutMs: this.#timeoutMs,
|
|
390
|
-
maxOutputBytes: this.#maxOutputBytes,
|
|
391
|
-
});
|
|
392
|
-
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
393
|
-
throw new VelaClientError("command_failed", `${argv[0]} ${argv.slice(1).join(" ")} exited ${result.exitCode}: ${commandFailureSummary(result)}`);
|
|
394
|
-
}
|
|
395
|
-
const check = parseJsonObject(result.stdout, "vela check");
|
|
396
|
-
const baseline = strictBaselineFromCheck(check);
|
|
397
|
-
if ((result.exitCode === 0) !== (baseline.status === "pass")) {
|
|
398
|
-
throw new VelaClientError("malformed_output", "vela strict exit code and status disagree");
|
|
399
|
-
}
|
|
400
|
-
return baseline;
|
|
401
|
-
}
|
|
402
179
|
async assertVersion(cwd) {
|
|
403
180
|
const binaryDigest = await sha256RegularFile(this.#binary, MAX_EXECUTABLE_BYTES);
|
|
404
181
|
if (binaryDigest !== this.#expectedSha256) {
|
|
@@ -423,62 +200,86 @@ export class VelaClient {
|
|
|
423
200
|
}
|
|
424
201
|
return observed;
|
|
425
202
|
}
|
|
426
|
-
async inspect(repoRoot, frontier
|
|
203
|
+
async inspect(repoRoot, frontier) {
|
|
427
204
|
const safeFrontier = frontierPath(frontier);
|
|
428
205
|
const version = await this.assertVersion(repoRoot);
|
|
429
|
-
const [gitCommit, gitTree,
|
|
206
|
+
const [gitCommit, gitTree, status, repository] = await Promise.all([
|
|
430
207
|
this.#gitObject(repoRoot, "HEAD^{commit}"),
|
|
431
208
|
this.#gitObject(repoRoot, "HEAD^{tree}"),
|
|
432
|
-
this.#
|
|
209
|
+
this.#json(["status", safeFrontier, "--json"], repoRoot, "vela status"),
|
|
210
|
+
this.#json(["repository", "verify", safeFrontier, "--json"], repoRoot, "vela repository verify"),
|
|
433
211
|
]);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
212
|
+
if (status.schema !== "vela.status.v1" ||
|
|
213
|
+
status.ok !== true ||
|
|
214
|
+
status.command !== "status") {
|
|
215
|
+
throw new VelaClientError("malformed_output", "vela status contract identity is invalid");
|
|
216
|
+
}
|
|
217
|
+
if (repository.schema !== "vela.repository-verification.v1" ||
|
|
218
|
+
repository.ok !== true ||
|
|
219
|
+
repository.command !== "repository verify") {
|
|
220
|
+
throw new VelaClientError("malformed_output", "vela repository verification contract identity is invalid");
|
|
221
|
+
}
|
|
222
|
+
const statusGit = fieldObject(status, "git", "vela status");
|
|
223
|
+
const integrity = fieldObject(status, "integrity", "vela status");
|
|
224
|
+
const statusRoots = fieldObject(status, "roots", "vela status");
|
|
225
|
+
if (integrity.replay !== "verified" ||
|
|
226
|
+
integrity.strict !== "pass" ||
|
|
227
|
+
nonnegativeInteger(integrity.blocker_count, "vela status.integrity.blocker_count") !== 0) {
|
|
228
|
+
throw new VelaClientError("command_failed", "Vela repository must replay and pass strict verification before Canopus can run");
|
|
229
|
+
}
|
|
230
|
+
const blockers = fieldObject(integrity, "blockers_by_code", "vela status.integrity");
|
|
231
|
+
if (Object.keys(blockers).length !== 0) {
|
|
232
|
+
throw new VelaClientError("malformed_output", "vela status strict pass contains blocker classifications");
|
|
233
|
+
}
|
|
234
|
+
const statusCommit = stringAt(statusGit.commit, "vela status.git.commit", {
|
|
235
|
+
min: 40,
|
|
236
|
+
max: 64,
|
|
237
|
+
pattern: GIT_OBJECT_RE,
|
|
238
|
+
});
|
|
239
|
+
const statusTree = stringAt(statusGit.tree, "vela status.git.tree", {
|
|
240
|
+
min: 40,
|
|
241
|
+
max: 64,
|
|
242
|
+
pattern: GIT_OBJECT_RE,
|
|
243
|
+
});
|
|
244
|
+
assertEqual(statusCommit, gitCommit, "Git/status commit");
|
|
245
|
+
assertEqual(statusTree, gitTree, "Git/status tree");
|
|
246
|
+
assertEqual(stringAt(repository.git_commit, "vela repository verify.git_commit", {
|
|
247
|
+
min: 40,
|
|
248
|
+
max: 64,
|
|
249
|
+
pattern: GIT_OBJECT_RE,
|
|
250
|
+
}), gitCommit, "Git/repository commit");
|
|
251
|
+
assertEqual(stringAt(repository.git_tree, "vela repository verify.git_tree", {
|
|
252
|
+
min: 40,
|
|
253
|
+
max: 64,
|
|
254
|
+
pattern: GIT_OBJECT_RE,
|
|
255
|
+
}), gitTree, "Git/repository tree");
|
|
256
|
+
const repositoryRoot = normalizeSha256(repository.repository_root, "vela repository verify.repository_root");
|
|
257
|
+
assertEqual(normalizeSha256(statusRoots.repository, "vela status.roots.repository"), repositoryRoot, "status/repository root");
|
|
258
|
+
for (const [statusKey, repositoryKey, label] of [
|
|
259
|
+
["epoch", "epoch_root", "epoch"],
|
|
260
|
+
["authority_keyset", "authority_keyset_root", "authority keyset"],
|
|
261
|
+
["authority_policy", "authority_policy_root", "authority policy"],
|
|
262
|
+
]) {
|
|
263
|
+
assertEqual(normalizeSha256(statusRoots[statusKey], `vela status.roots.${statusKey}`), normalizeSha256(repository[repositoryKey], `vela repository verify.${repositoryKey}`), `status/repository ${label}`);
|
|
467
264
|
}
|
|
265
|
+
const statusFrontier = fieldObject(status, "frontier", "vela status");
|
|
266
|
+
assertEqual(stringAt(statusFrontier.id, "vela status.frontier.id", { min: 1, max: 64 }), stringAt(repository.frontier_id, "vela repository verify.frontier_id", {
|
|
267
|
+
min: 1,
|
|
268
|
+
max: 64,
|
|
269
|
+
}), "status/repository frontier");
|
|
468
270
|
return {
|
|
469
271
|
version,
|
|
470
272
|
roots: {
|
|
471
273
|
git_commit: gitCommit,
|
|
472
274
|
git_tree: gitTree,
|
|
473
|
-
|
|
474
|
-
vela_snapshot: checkCurrent,
|
|
275
|
+
vela_repository: repositoryRoot,
|
|
475
276
|
},
|
|
476
|
-
|
|
477
|
-
|
|
277
|
+
status,
|
|
278
|
+
repository,
|
|
478
279
|
};
|
|
479
280
|
}
|
|
480
|
-
async assertRoots(repoRoot, frontier, expected
|
|
481
|
-
const inspection = await this.inspect(repoRoot, frontier
|
|
281
|
+
async assertRoots(repoRoot, frontier, expected) {
|
|
282
|
+
const inspection = await this.inspect(repoRoot, frontier);
|
|
482
283
|
compareRoots(inspection.roots, expected);
|
|
483
284
|
return inspection;
|
|
484
285
|
}
|
|
@@ -486,246 +287,24 @@ export class VelaClient {
|
|
|
486
287
|
if (mission.vela_version !== this.#expectedVersion) {
|
|
487
288
|
throw new VelaClientError("version_mismatch", "mission and client Vela versions differ");
|
|
488
289
|
}
|
|
489
|
-
return await this.offer(repoRoot, mission.frontier, mission.roots
|
|
290
|
+
return await this.offer(repoRoot, mission.frontier, mission.roots);
|
|
490
291
|
}
|
|
491
|
-
async offer(repoRoot, frontier, roots,
|
|
292
|
+
async offer(repoRoot, frontier, roots, limit = 128) {
|
|
492
293
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 128) {
|
|
493
294
|
throw new VelaClientError("malformed_output", "vela next limit must be 1..128");
|
|
494
295
|
}
|
|
495
296
|
const safeFrontier = frontierPath(frontier);
|
|
496
|
-
await this.assertRoots(repoRoot, safeFrontier, roots
|
|
297
|
+
await this.assertRoots(repoRoot, safeFrontier, roots);
|
|
497
298
|
const value = await this.#json(["next", safeFrontier, "--limit", String(limit), "--json"], repoRoot, "vela next");
|
|
498
299
|
if (value.ok === false) {
|
|
499
300
|
throw new VelaClientError("command_failed", "vela next returned ok=false");
|
|
500
301
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
await this.assertRoots(repoRoot, mission.frontier, expected, mission.schema === "canopus.mission.v1" ? mission.strict_baseline : undefined);
|
|
506
|
-
const value = await this.#json(["work", target, "--frontier", mission.frontier, "--as", mission.actor, "--json"], repoRoot, "vela work");
|
|
507
|
-
if (value.ok === false) {
|
|
508
|
-
throw new VelaClientError("command_failed", "vela work returned ok=false");
|
|
302
|
+
if (value.schema !== "vela.offer.v1" ||
|
|
303
|
+
value.command !== "next" ||
|
|
304
|
+
value.ok !== true) {
|
|
305
|
+
throw new VelaClientError("malformed_output", "vela next contract identity is invalid");
|
|
509
306
|
}
|
|
307
|
+
assertEqual(normalizeSha256(value.repository_root, "vela next.repository_root"), roots.vela_repository, "mission/offer repository");
|
|
510
308
|
return { ok: true, value };
|
|
511
309
|
}
|
|
512
|
-
async withdraw(repoRoot, frontier, proposalId, actor, reason) {
|
|
513
|
-
if (!/^vpr_[0-9a-f]{16}$/u.test(proposalId)) {
|
|
514
|
-
throw new VelaClientError("malformed_output", "withdrawal requires a full proposal id");
|
|
515
|
-
}
|
|
516
|
-
if (!actor.startsWith("agent:") || reason.trim().length === 0) {
|
|
517
|
-
throw new VelaClientError("malformed_output", "withdrawal requires an agent actor and reason");
|
|
518
|
-
}
|
|
519
|
-
const value = await this.#json(["review", "withdraw", frontierPath(frontier), proposalId, "--as", actor, "--reason", reason, "--json"], repoRoot, "vela review withdraw");
|
|
520
|
-
if (value.ok !== true || value.command !== "review.withdraw") {
|
|
521
|
-
throw new VelaClientError("malformed_output", "vela review withdraw returned an invalid envelope");
|
|
522
|
-
}
|
|
523
|
-
if (value.proposal_id !== proposalId || value.idempotent !== false || value.key_read !== true) {
|
|
524
|
-
throw new VelaClientError("malformed_output", "vela review withdraw did not bind one new key-backed withdrawal");
|
|
525
|
-
}
|
|
526
|
-
const eventId = stringAt(value.withdrawal_event_id, "vela review withdraw.withdrawal_event_id", {
|
|
527
|
-
max: 20,
|
|
528
|
-
pattern: /^vev_[0-9a-f]{16}$/u,
|
|
529
|
-
});
|
|
530
|
-
const withdrawnAt = stringAt(value.withdrawn_at, "vela review withdraw.withdrawn_at", {
|
|
531
|
-
min: 20,
|
|
532
|
-
max: 64,
|
|
533
|
-
});
|
|
534
|
-
if (!Number.isFinite(Date.parse(withdrawnAt))) {
|
|
535
|
-
throw new VelaClientError("malformed_output", "vela review withdraw returned an invalid timestamp");
|
|
536
|
-
}
|
|
537
|
-
const stateRootBefore = normalizeSha256(value.state_root_before, "vela review withdraw.state_root_before");
|
|
538
|
-
const stateRootAfter = normalizeSha256(value.state_root_after, "vela review withdraw.state_root_after");
|
|
539
|
-
if (stateRootBefore === stateRootAfter) {
|
|
540
|
-
throw new VelaClientError("malformed_output", "new withdrawal did not change the event-log root");
|
|
541
|
-
}
|
|
542
|
-
const publication = fieldObject(value, "publication", "vela review withdraw");
|
|
543
|
-
const publicationState = stringAt(publication.state, "vela review withdraw.publication.state", {
|
|
544
|
-
min: 1,
|
|
545
|
-
max: 32,
|
|
546
|
-
});
|
|
547
|
-
if (publicationState !== "committed_local" && publicationState !== "pushed") {
|
|
548
|
-
throw new VelaClientError("command_failed", `withdrawal publication is ${publicationState}, not committed`);
|
|
549
|
-
}
|
|
550
|
-
const publicationCommit = stringAt(publication.commit, "vela review withdraw.publication.commit", { max: 64, pattern: GIT_OBJECT_RE });
|
|
551
|
-
return {
|
|
552
|
-
proposal_id: proposalId,
|
|
553
|
-
withdrawal_event_id: eventId,
|
|
554
|
-
withdrawn_at: withdrawnAt,
|
|
555
|
-
state_root_before: stateRootBefore,
|
|
556
|
-
state_root_after: stateRootAfter,
|
|
557
|
-
publication_commit: publicationCommit,
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
async landAuthoredCommand(mission, repoRoot, input, _expected) {
|
|
561
|
-
if ((input.predictedObservable === undefined) === (input.notApplicable === undefined)) {
|
|
562
|
-
throw new VelaClientError("malformed_output", "authored receipt requires exactly one prediction mode");
|
|
563
|
-
}
|
|
564
|
-
// The caller performs the strict exact-root check immediately before it
|
|
565
|
-
// installs the candidate artifacts. Those files legitimately stale the
|
|
566
|
-
// derived artifacts hash until `vela land` records them atomically, so a
|
|
567
|
-
// second strict check here would make the normal porcelain path unusable.
|
|
568
|
-
const args = [
|
|
569
|
-
"land",
|
|
570
|
-
"--frontier",
|
|
571
|
-
mission.frontier,
|
|
572
|
-
"--claim",
|
|
573
|
-
input.claim,
|
|
574
|
-
"--type",
|
|
575
|
-
input.claimType,
|
|
576
|
-
"--replayability",
|
|
577
|
-
input.replayability,
|
|
578
|
-
];
|
|
579
|
-
for (const artifact of input.artifacts) {
|
|
580
|
-
args.push("--artifact", `${relativePathAt(artifact.path, "artifact.path")}:${artifact.kind}`);
|
|
581
|
-
}
|
|
582
|
-
for (const caveat of input.caveats)
|
|
583
|
-
args.push("--caveat", caveat);
|
|
584
|
-
if (input.predictedObservable !== undefined) {
|
|
585
|
-
args.push("--predicted-observable", input.predictedObservable);
|
|
586
|
-
}
|
|
587
|
-
else {
|
|
588
|
-
args.push("--not-applicable");
|
|
589
|
-
}
|
|
590
|
-
args.push("--performed-test", input.performedTest, "--result", input.result);
|
|
591
|
-
for (const evidence of input.evidence)
|
|
592
|
-
args.push("--evidence", evidence);
|
|
593
|
-
for (const counterevidence of input.counterevidence) {
|
|
594
|
-
args.push("--counterevidence", counterevidence);
|
|
595
|
-
}
|
|
596
|
-
if (input.executionBinding !== undefined) {
|
|
597
|
-
args.push("--packet-root", input.executionBinding.packet_root, "--profile-root", input.executionBinding.profile_root, "--verifier-capsule-root", input.executionBinding.verifier_capsule_root, "--result-contract-root", input.executionBinding.result_contract_root);
|
|
598
|
-
}
|
|
599
|
-
if (input.work !== undefined)
|
|
600
|
-
args.push("--work", input.work);
|
|
601
|
-
args.push("--as", mission.actor, "--json");
|
|
602
|
-
const argv = [this.#binary, ...args];
|
|
603
|
-
if (argv.length > MAX_COMMAND_ARGV) {
|
|
604
|
-
throw new VelaClientError("malformed_output", `authored Receipt requires ${argv.length} argv entries; maximum is ${MAX_COMMAND_ARGV}`);
|
|
605
|
-
}
|
|
606
|
-
const result = await this.#runner({
|
|
607
|
-
argv,
|
|
608
|
-
cwd: repoRoot,
|
|
609
|
-
env: this.#env,
|
|
610
|
-
timeoutMs: this.#timeoutMs,
|
|
611
|
-
maxOutputBytes: this.#maxOutputBytes,
|
|
612
|
-
});
|
|
613
|
-
return {
|
|
614
|
-
argv,
|
|
615
|
-
exit_code: result.exitCode,
|
|
616
|
-
stdout: result.stdout.toString("utf8"),
|
|
617
|
-
stderr: result.stderr.toString("utf8"),
|
|
618
|
-
stdout_digest: sha256Bytes(result.stdout),
|
|
619
|
-
stderr_digest: sha256Bytes(result.stderr),
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
parseLandCommand(observation) {
|
|
623
|
-
if (observation.exit_code !== 0) {
|
|
624
|
-
throw new VelaClientError("command_failed", `vela land exited ${observation.exit_code}: ${observation.stderr}`);
|
|
625
|
-
}
|
|
626
|
-
return parseJsonObject(Buffer.from(observation.stdout), "vela land");
|
|
627
|
-
}
|
|
628
|
-
validateLandResult(mission, raw) {
|
|
629
|
-
return validateLandResult(mission, raw);
|
|
630
|
-
}
|
|
631
|
-
async verifyReceiptBinding(mission, repoRoot, landing, input, artifacts) {
|
|
632
|
-
const hex = landing.receiptRoot.slice("sha256:".length);
|
|
633
|
-
const receiptPath = path.join(repoRoot, mission.frontier, "records", "receipts", "sha256", `${hex}.json`);
|
|
634
|
-
const bytes = await readBoundedRegularFile(receiptPath, 8 * 1024 * 1024);
|
|
635
|
-
let parsed;
|
|
636
|
-
try {
|
|
637
|
-
parsed = JSON.parse(bytes.toString("utf8"));
|
|
638
|
-
}
|
|
639
|
-
catch (error) {
|
|
640
|
-
throw new VelaClientError("malformed_output", `retained receipt is not JSON: ${String(error)}`);
|
|
641
|
-
}
|
|
642
|
-
const receipt = objectAt(parsed, "retained receipt");
|
|
643
|
-
if (sha256Bytes(canonicalJcs(receipt)) !== landing.receiptRoot) {
|
|
644
|
-
throw new VelaClientError("root_mismatch", "retained Receipt v1 root does not match vela land");
|
|
645
|
-
}
|
|
646
|
-
const recordPath = path.join(repoRoot, mission.frontier, "records", `${landing.recordId}.json`);
|
|
647
|
-
let recordValue;
|
|
648
|
-
try {
|
|
649
|
-
recordValue = JSON.parse((await readBoundedRegularFile(recordPath, 8 * 1024 * 1024)).toString("utf8"));
|
|
650
|
-
}
|
|
651
|
-
catch (error) {
|
|
652
|
-
throw new VelaClientError("malformed_output", `retained activity record is unavailable or malformed: ${String(error)}`);
|
|
653
|
-
}
|
|
654
|
-
const record = objectAt(recordValue, "retained activity record");
|
|
655
|
-
const expectedReceiptPath = `records/receipts/sha256/${hex}.json`;
|
|
656
|
-
if (record.id !== landing.recordId ||
|
|
657
|
-
record.receipt_digest !== landing.receiptRoot ||
|
|
658
|
-
record.receipt_path !== expectedReceiptPath) {
|
|
659
|
-
throw new VelaClientError("root_mismatch", "retained activity record does not bind the landed Receipt v1");
|
|
660
|
-
}
|
|
661
|
-
for (const [actual, expected, label] of [
|
|
662
|
-
[receipt.claim, input.claim, "claim"],
|
|
663
|
-
[receipt.type, input.claimType, "claim type"],
|
|
664
|
-
[receipt.replayability, input.replayability, "replayability"],
|
|
665
|
-
]) {
|
|
666
|
-
if (actual !== expected) {
|
|
667
|
-
throw new VelaClientError("malformed_output", `retained receipt ${label} drifted`);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
if (JSON.stringify(receipt.caveats) !== JSON.stringify(input.caveats)) {
|
|
671
|
-
throw new VelaClientError("malformed_output", "retained receipt caveats drifted");
|
|
672
|
-
}
|
|
673
|
-
const rawArtifacts = receipt.artifacts;
|
|
674
|
-
if (!Array.isArray(rawArtifacts) || rawArtifacts.length !== artifacts.length) {
|
|
675
|
-
throw new VelaClientError("malformed_output", "retained receipt artifact set drifted");
|
|
676
|
-
}
|
|
677
|
-
const recordArtifacts = record.artifacts;
|
|
678
|
-
if (!Array.isArray(recordArtifacts) || recordArtifacts.length !== artifacts.length) {
|
|
679
|
-
throw new VelaClientError("malformed_output", "activity record artifact set drifted");
|
|
680
|
-
}
|
|
681
|
-
for (const [index, expected] of artifacts.entries()) {
|
|
682
|
-
const rawArtifact = objectAt(rawArtifacts[index], `retained receipt.artifacts[${index}]`);
|
|
683
|
-
if (rawArtifact.path !== expected.path ||
|
|
684
|
-
rawArtifact.kind !== expected.kind ||
|
|
685
|
-
rawArtifact.sha256 !== expected.digest.slice("sha256:".length)) {
|
|
686
|
-
throw new VelaClientError("malformed_output", `retained receipt artifact ${index} drifted`);
|
|
687
|
-
}
|
|
688
|
-
const recordArtifact = objectAt(recordArtifacts[index], `retained activity record.artifacts[${index}]`);
|
|
689
|
-
const hexDigest = expected.digest.slice("sha256:".length);
|
|
690
|
-
if (recordArtifact.kind !== expected.kind ||
|
|
691
|
-
recordArtifact.sha256 !== hexDigest ||
|
|
692
|
-
recordArtifact.locator !== `records/artifacts/sha256/${hexDigest}` ||
|
|
693
|
-
recordArtifact.size_bytes !== expected.bytes ||
|
|
694
|
-
recordArtifact.locator_integrity !== "immutable" ||
|
|
695
|
-
recordArtifact.availability !== "available") {
|
|
696
|
-
throw new VelaClientError("malformed_output", `activity record artifact ${index} is not an immutable available blob binding`);
|
|
697
|
-
}
|
|
698
|
-
const installed = await readBoundedRegularFile(retainedArtifactPath(repoRoot, mission.frontier, expected.digest), mission.budgets.max_artifact_bytes);
|
|
699
|
-
if (installed.length !== expected.bytes || sha256Bytes(installed) !== expected.digest) {
|
|
700
|
-
throw new VelaClientError("root_mismatch", `retained artifact ${expected.path} drifted`);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
const environment = fieldObject(receipt, "environment", "retained receipt");
|
|
704
|
-
if (input.executionBinding === undefined) {
|
|
705
|
-
if (environment["vela:execution_binding"] !== undefined) {
|
|
706
|
-
throw new VelaClientError("malformed_output", "retained receipt invented an exact execution binding");
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
else {
|
|
710
|
-
const binding = fieldObject(environment, "vela:execution_binding", "retained receipt.environment");
|
|
711
|
-
if (canonicalJcs(binding) !== canonicalJcs(input.executionBinding)) {
|
|
712
|
-
throw new VelaClientError("root_mismatch", "retained exact execution binding drifted");
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
const chain = fieldObject(environment, "vela:scientific_chain", "retained receipt.environment");
|
|
716
|
-
const expectedChain = {
|
|
717
|
-
schema: "vela.scientific-chain.producer.v1",
|
|
718
|
-
authority: "producer",
|
|
719
|
-
...(input.predictedObservable === undefined
|
|
720
|
-
? { not_applicable: true }
|
|
721
|
-
: { predicted_observable: input.predictedObservable, not_applicable: false }),
|
|
722
|
-
performed_test: input.performedTest,
|
|
723
|
-
result: input.result,
|
|
724
|
-
evidence: input.evidence,
|
|
725
|
-
counterevidence: input.counterevidence,
|
|
726
|
-
};
|
|
727
|
-
if (canonicalJcs(chain) !== canonicalJcs(expectedChain)) {
|
|
728
|
-
throw new VelaClientError("malformed_output", "retained scientific-chain assertion drifted");
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
310
|
}
|