@vela-science/canopus 0.4.0-rc.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 +249 -0
- package/LICENSE-APACHE +176 -0
- package/LICENSE-MIT +21 -0
- package/README.md +124 -0
- package/SECURITY.md +35 -0
- package/capsules/erdos1056-k15/README.md +35 -0
- package/capsules/erdos1056-k15/bin/linux-arm64/10428401-10428600/verifier +0 -0
- package/capsules/erdos1056-k15/bin/linux-x86_64/10428401-10428600/verifier +0 -0
- package/capsules/formal-erdos-505-test-dim-one/README.md +20 -0
- package/capsules/formal-erdos-505-test-dim-one/verifier +105 -0
- package/dist/src/activity/events.d.ts +15 -0
- package/dist/src/activity/events.js +24 -0
- package/dist/src/activity/store.d.ts +10 -0
- package/dist/src/activity/store.js +165 -0
- package/dist/src/artifact/freeze.d.ts +21 -0
- package/dist/src/artifact/freeze.js +177 -0
- package/dist/src/artifact/materialize.d.ts +6 -0
- package/dist/src/artifact/materialize.js +29 -0
- package/dist/src/budget/enforce.d.ts +30 -0
- package/dist/src/budget/enforce.js +98 -0
- package/dist/src/candidate/validate.d.ts +3 -0
- package/dist/src/candidate/validate.js +32 -0
- package/dist/src/capability/withdrawal.d.ts +47 -0
- package/dist/src/capability/withdrawal.js +487 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +503 -0
- package/dist/src/contracts/candidate.d.ts +50 -0
- package/dist/src/contracts/candidate.js +93 -0
- package/dist/src/contracts/mission.d.ts +139 -0
- package/dist/src/contracts/mission.js +401 -0
- package/dist/src/contracts/validation.d.ts +25 -0
- package/dist/src/contracts/validation.js +95 -0
- package/dist/src/engines/codex-events.d.ts +25 -0
- package/dist/src/engines/codex-events.js +219 -0
- package/dist/src/engines/codex-exec.d.ts +17 -0
- package/dist/src/engines/codex-exec.js +136 -0
- package/dist/src/engines/codex-home.d.ts +7 -0
- package/dist/src/engines/codex-home.js +50 -0
- package/dist/src/engines/codex-tool-free.d.ts +26 -0
- package/dist/src/engines/codex-tool-free.js +213 -0
- package/dist/src/engines/codex-tools-native.d.ts +30 -0
- package/dist/src/engines/codex-tools-native.js +405 -0
- package/dist/src/engines/engine.d.ts +55 -0
- package/dist/src/engines/engine.js +4 -0
- package/dist/src/engines/fake.d.ts +7 -0
- package/dist/src/engines/fake.js +37 -0
- package/dist/src/evidence/manifests.d.ts +9 -0
- package/dist/src/evidence/manifests.js +56 -0
- package/dist/src/index.d.ts +32 -0
- package/dist/src/index.js +32 -0
- package/dist/src/mission/prepare.d.ts +33 -0
- package/dist/src/mission/prepare.js +375 -0
- package/dist/src/product/custody.d.ts +27 -0
- package/dist/src/product/custody.js +131 -0
- package/dist/src/product/doctor.d.ts +61 -0
- package/dist/src/product/doctor.js +278 -0
- package/dist/src/product/profile-bundle.d.ts +21 -0
- package/dist/src/product/profile-bundle.js +78 -0
- package/dist/src/product/profile.d.ts +67 -0
- package/dist/src/product/profile.js +251 -0
- package/dist/src/product/replay.d.ts +10 -0
- package/dist/src/product/replay.js +71 -0
- package/dist/src/product/run.d.ts +31 -0
- package/dist/src/product/run.js +264 -0
- package/dist/src/product/runtime.d.ts +18 -0
- package/dist/src/product/runtime.js +64 -0
- package/dist/src/product/version.d.ts +1 -0
- package/dist/src/product/version.js +6 -0
- package/dist/src/product/withdraw.d.ts +8 -0
- package/dist/src/product/withdraw.js +255 -0
- package/dist/src/projection/diagnostic.d.ts +12 -0
- package/dist/src/projection/diagnostic.js +61 -0
- package/dist/src/projection/failure.d.ts +26 -0
- package/dist/src/projection/failure.js +63 -0
- package/dist/src/projection/run.d.ts +89 -0
- package/dist/src/projection/run.js +159 -0
- package/dist/src/receipt/map.d.ts +25 -0
- package/dist/src/receipt/map.js +162 -0
- package/dist/src/repair/plan.d.ts +2 -0
- package/dist/src/repair/plan.js +22 -0
- package/dist/src/roles.d.ts +2 -0
- package/dist/src/roles.js +9 -0
- package/dist/src/run.d.ts +94 -0
- package/dist/src/run.js +654 -0
- package/dist/src/util/canonical.d.ts +5 -0
- package/dist/src/util/canonical.js +53 -0
- package/dist/src/util/command.d.ts +26 -0
- package/dist/src/util/command.js +161 -0
- package/dist/src/util/files.d.ts +3 -0
- package/dist/src/util/files.js +65 -0
- package/dist/src/vela/cli.d.ts +71 -0
- package/dist/src/vela/cli.js +718 -0
- package/dist/src/vela/types.d.ts +27 -0
- package/dist/src/vela/types.js +1 -0
- package/dist/src/verifier/run.d.ts +24 -0
- package/dist/src/verifier/run.js +315 -0
- package/dist/src/workspace/prepare.d.ts +30 -0
- package/dist/src/workspace/prepare.js +183 -0
- package/docs/MISSIONS.md +78 -0
- package/docs/RELEASES.md +1036 -0
- package/docs/RUN_RECORD.md +70 -0
- package/missions/erdos1056-k15-next/mission.draft.json +40 -0
- package/missions/formal-erdos-505-test-dim-one/mission.draft.json +40 -0
- package/package.json +89 -0
- package/profiles/erdos1056-k15-10428401-10428600.json +33 -0
- package/profiles/formal-erdos-505-test-dim-one.json +35 -0
- package/runtime/native-worker/config-linux.toml +20 -0
- package/runtime/native-worker/config.toml +17 -0
- package/schemas/candidate.v0.json +125 -0
- package/schemas/engine-manifest.v0.json +45 -0
- package/schemas/engine-output.v0.json +41 -0
- package/schemas/mission.v0.json +175 -0
- package/schemas/mission.v1.json +224 -0
- package/schemas/verifier-manifest.v0.json +48 -0
- package/schemas/verifier-manifest.v1.json +59 -0
- package/scripts/run-hostile-native-custody-fixture.mjs +382 -0
- package/scripts/run-hostile-verifier-fixture.mjs +63 -0
- package/tests/fixtures/hostile-verifier/probe.py +35 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { GIT_OBJECT_RE, SHA256_RE, objectAt, relativePathAt, stringAt, } from "../contracts/validation.js";
|
|
3
|
+
import { MAX_COMMAND_ARGV, isolatedEnvironment, runCommand, } from "../util/command.js";
|
|
4
|
+
import { canonicalJcs, sha256Bytes } from "../util/canonical.js";
|
|
5
|
+
import { MAX_EXECUTABLE_BYTES, readBoundedRegularFile, sha256RegularFile } from "../util/files.js";
|
|
6
|
+
export function retainedArtifactPath(repoRoot, frontier, digest) {
|
|
7
|
+
if (!/^sha256:[0-9a-f]{64}$/u.test(digest)) {
|
|
8
|
+
throw new VelaClientError("malformed_output", "artifact digest is not sha256");
|
|
9
|
+
}
|
|
10
|
+
return path.join(repoRoot, frontier, "records", "artifacts", "sha256", digest.slice("sha256:".length));
|
|
11
|
+
}
|
|
12
|
+
export class VelaClientError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "VelaClientError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function parseJsonObject(stdout, command) {
|
|
21
|
+
let value;
|
|
22
|
+
try {
|
|
23
|
+
value = JSON.parse(stdout.toString("utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw new VelaClientError("malformed_output", `${command} did not return one JSON value: ${String(error)}`);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return objectAt(value, command);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new VelaClientError("malformed_output", String(error));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function safeFailureMessage(value) {
|
|
36
|
+
if (typeof value !== "string" || value.length === 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
const normalized = value
|
|
39
|
+
.replace(/[\u0000-\u001f\u007f]+/gu, " ")
|
|
40
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+\/-]+=*/giu, "Bearer [redacted]")
|
|
41
|
+
.replace(/\b(?:sk|sess|key)-[A-Za-z0-9_-]{8,}\b/gu, "[secret-redacted]")
|
|
42
|
+
.replace(/\s+/gu, " ")
|
|
43
|
+
.trim();
|
|
44
|
+
return [...normalized].slice(0, 512).join("");
|
|
45
|
+
}
|
|
46
|
+
function commandFailureSummary(result) {
|
|
47
|
+
const diagnostics = [];
|
|
48
|
+
for (const bytes of [result.stdout, result.stderr]) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
51
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
52
|
+
continue;
|
|
53
|
+
const object = parsed;
|
|
54
|
+
const direct = safeFailureMessage(object.error) ?? safeFailureMessage(object.message);
|
|
55
|
+
if (direct !== undefined && !diagnostics.includes(direct))
|
|
56
|
+
diagnostics.push(direct);
|
|
57
|
+
const nested = object.error;
|
|
58
|
+
if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) {
|
|
59
|
+
const message = safeFailureMessage(nested.message);
|
|
60
|
+
if (message !== undefined && !diagnostics.includes(message))
|
|
61
|
+
diagnostics.push(message);
|
|
62
|
+
}
|
|
63
|
+
const integrity = object.state_integrity;
|
|
64
|
+
if (typeof integrity === "object" && integrity !== null && !Array.isArray(integrity)) {
|
|
65
|
+
const errors = integrity.structural_errors;
|
|
66
|
+
if (Array.isArray(errors)) {
|
|
67
|
+
for (const error of errors) {
|
|
68
|
+
if (typeof error !== "object" || error === null || Array.isArray(error))
|
|
69
|
+
continue;
|
|
70
|
+
const message = safeFailureMessage(error.message);
|
|
71
|
+
if (message !== undefined && !diagnostics.includes(message))
|
|
72
|
+
diagnostics.push(message);
|
|
73
|
+
if (diagnostics.length === 2)
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Only documented structured error fields are eligible for display.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return [
|
|
84
|
+
...(diagnostics.length === 0 ? ["no structured Vela failure message"] : diagnostics.slice(0, 2)),
|
|
85
|
+
`stdout_sha256=${sha256Bytes(result.stdout)}`,
|
|
86
|
+
`stderr_sha256=${sha256Bytes(result.stderr)}`,
|
|
87
|
+
].join("; ");
|
|
88
|
+
}
|
|
89
|
+
function bool(value, at) {
|
|
90
|
+
if (typeof value !== "boolean") {
|
|
91
|
+
throw new VelaClientError("malformed_output", `${at} must be a boolean`);
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
function nullableCount(value, at) {
|
|
96
|
+
if (value === null)
|
|
97
|
+
return null;
|
|
98
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
99
|
+
throw new VelaClientError("malformed_output", `${at} must be a nonnegative integer or null`);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
function normalizeSha256(value, at) {
|
|
104
|
+
if (typeof value !== "string") {
|
|
105
|
+
throw new VelaClientError("malformed_output", `${at} must be a SHA-256 string`);
|
|
106
|
+
}
|
|
107
|
+
const normalized = value.startsWith("sha256:") ? value : `sha256:${value}`;
|
|
108
|
+
if (!SHA256_RE.test(normalized)) {
|
|
109
|
+
throw new VelaClientError("malformed_output", `${at} is not a full SHA-256 root`);
|
|
110
|
+
}
|
|
111
|
+
return normalized;
|
|
112
|
+
}
|
|
113
|
+
function frontierPath(value) {
|
|
114
|
+
return value === "." ? "." : relativePathAt(value, "frontier");
|
|
115
|
+
}
|
|
116
|
+
function fieldObject(parent, key, at) {
|
|
117
|
+
try {
|
|
118
|
+
return objectAt(parent[key], `${at}.${key}`);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw new VelaClientError("malformed_output", String(error));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function assertEqual(actual, expected, label) {
|
|
125
|
+
if (actual !== expected) {
|
|
126
|
+
throw new VelaClientError("root_mismatch", `${label} mismatch: expected ${expected}, observed ${actual}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function compareRoots(actual, expected) {
|
|
130
|
+
assertEqual(actual.git_commit, expected.git_commit, "Git commit");
|
|
131
|
+
assertEqual(actual.git_tree, expected.git_tree, "Git tree");
|
|
132
|
+
assertEqual(actual.vela_event_log, expected.vela_event_log, "Vela event log");
|
|
133
|
+
assertEqual(actual.vela_snapshot, expected.vela_snapshot, "Vela snapshot");
|
|
134
|
+
}
|
|
135
|
+
function nonnegativeInteger(value, at) {
|
|
136
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
137
|
+
throw new VelaClientError("malformed_output", `${at} must be a nonnegative integer`);
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
function strictSignalCheck(check) {
|
|
142
|
+
if (!Array.isArray(check.checks)) {
|
|
143
|
+
throw new VelaClientError("malformed_output", "vela check.checks must be an array");
|
|
144
|
+
}
|
|
145
|
+
let signalCheck;
|
|
146
|
+
for (const [index, value] of check.checks.entries()) {
|
|
147
|
+
const entry = fieldObject({ entry: value }, "entry", `vela check.checks[${index}]`);
|
|
148
|
+
const id = stringAt(entry.id, `vela check.checks[${index}].id`, { min: 1, max: 128 });
|
|
149
|
+
const status = stringAt(entry.status, `vela check.checks[${index}].status`, {
|
|
150
|
+
min: 1,
|
|
151
|
+
max: 32,
|
|
152
|
+
});
|
|
153
|
+
if (id === "signals") {
|
|
154
|
+
if (signalCheck !== undefined) {
|
|
155
|
+
throw new VelaClientError("malformed_output", "vela check contains duplicate signals checks");
|
|
156
|
+
}
|
|
157
|
+
signalCheck = entry;
|
|
158
|
+
}
|
|
159
|
+
else if (status !== "pass") {
|
|
160
|
+
throw new VelaClientError("command_failed", `vela strict check failed outside the registered signals baseline: ${id}=${status}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (signalCheck === undefined) {
|
|
164
|
+
throw new VelaClientError("malformed_output", "vela check omitted the signals check");
|
|
165
|
+
}
|
|
166
|
+
return signalCheck;
|
|
167
|
+
}
|
|
168
|
+
export function strictBaselineFromCheck(check) {
|
|
169
|
+
const summary = fieldObject(check, "summary", "vela check");
|
|
170
|
+
if (summary.strict !== true) {
|
|
171
|
+
throw new VelaClientError("malformed_output", "vela check summary is not strict");
|
|
172
|
+
}
|
|
173
|
+
if (nonnegativeInteger(summary.errors, "vela check.summary.errors") !== 0 ||
|
|
174
|
+
nonnegativeInteger(summary.invalid_findings, "vela check.summary.invalid_findings") !== 0) {
|
|
175
|
+
throw new VelaClientError("command_failed", "vela strict check contains structural errors or invalid findings");
|
|
176
|
+
}
|
|
177
|
+
const signalCheck = strictSignalCheck(check);
|
|
178
|
+
if (!Array.isArray(signalCheck.blockers)) {
|
|
179
|
+
throw new VelaClientError("malformed_output", "vela check signals.blockers must be an array");
|
|
180
|
+
}
|
|
181
|
+
const blockers = signalCheck.blockers.map((value, index) => fieldObject({ blocker: value }, "blocker", `vela check signals.blockers[${index}]`));
|
|
182
|
+
const blocker_count = nonnegativeInteger(signalCheck.failed, "vela check signals.failed");
|
|
183
|
+
if (blockers.length !== blocker_count) {
|
|
184
|
+
throw new VelaClientError("malformed_output", "vela check signals blocker array and failed count disagree");
|
|
185
|
+
}
|
|
186
|
+
const counts = new Map();
|
|
187
|
+
for (const [index, blocker] of blockers.entries()) {
|
|
188
|
+
const kind = stringAt(blocker.kind, `vela check signals.blockers[${index}].kind`, {
|
|
189
|
+
min: 1,
|
|
190
|
+
max: 128,
|
|
191
|
+
pattern: /^[a-z][a-z0-9_]*$/u,
|
|
192
|
+
});
|
|
193
|
+
counts.set(kind, (counts.get(kind) ?? 0) + 1);
|
|
194
|
+
}
|
|
195
|
+
const canonicalBlockers = blockers
|
|
196
|
+
.map((blocker) => canonicalJcs(blocker))
|
|
197
|
+
.sort()
|
|
198
|
+
.map((serialized) => JSON.parse(serialized));
|
|
199
|
+
const status = blocker_count === 0 ? "pass" : "fail";
|
|
200
|
+
const signalStatus = stringAt(signalCheck.status, "vela check signals.status", {
|
|
201
|
+
min: 1,
|
|
202
|
+
max: 32,
|
|
203
|
+
});
|
|
204
|
+
if (signalStatus !== status || summary.status !== status || bool(check.ok, "vela check.ok") !== (status === "pass")) {
|
|
205
|
+
throw new VelaClientError("malformed_output", "vela strict status fields disagree");
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
status,
|
|
209
|
+
blocker_count,
|
|
210
|
+
blockers_root: sha256Bytes(canonicalJcs(canonicalBlockers)),
|
|
211
|
+
rule_counts: [...counts.entries()]
|
|
212
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
213
|
+
.map(([rule, count]) => ({ rule, count })),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function assertStrictBaseline(actual, expected) {
|
|
217
|
+
if (expected === undefined) {
|
|
218
|
+
if (actual.status !== "pass") {
|
|
219
|
+
throw new VelaClientError("command_failed", `vela strict check has ${actual.blocker_count} unregistered blockers`);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (canonicalJcs(actual) !== canonicalJcs(expected)) {
|
|
224
|
+
throw new VelaClientError("root_mismatch", `strict baseline mismatch: expected ${canonicalJcs(expected)}, observed ${canonicalJcs(actual)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export function validateLandResult(mission, raw) {
|
|
228
|
+
if (raw.ok !== true || raw.command !== "land") {
|
|
229
|
+
throw new VelaClientError("malformed_output", "vela land did not return its success envelope");
|
|
230
|
+
}
|
|
231
|
+
const rawRoute = stringAt(raw.route, "vela land.route", { min: 1, max: 64 });
|
|
232
|
+
const route = rawRoute === "policy_admitted"
|
|
233
|
+
? "permit"
|
|
234
|
+
: rawRoute === "deferred"
|
|
235
|
+
? "defer"
|
|
236
|
+
: rawRoute === "exact_retry"
|
|
237
|
+
? "exact_retry"
|
|
238
|
+
: undefined;
|
|
239
|
+
if (route === undefined) {
|
|
240
|
+
throw new VelaClientError("malformed_output", `unknown landing route ${rawRoute}`);
|
|
241
|
+
}
|
|
242
|
+
const detail = stringAt(raw.detail, "vela land.detail", { max: 8192 });
|
|
243
|
+
const rawOriginalRoute = raw.original_route;
|
|
244
|
+
const originalRoute = route === "exact_retry"
|
|
245
|
+
? rawOriginalRoute === "policy_admitted"
|
|
246
|
+
? "permit"
|
|
247
|
+
: rawOriginalRoute === "deferred"
|
|
248
|
+
? "defer"
|
|
249
|
+
: undefined
|
|
250
|
+
: rawOriginalRoute === null
|
|
251
|
+
? null
|
|
252
|
+
: undefined;
|
|
253
|
+
if (originalRoute === undefined) {
|
|
254
|
+
throw new VelaClientError("malformed_output", "landing returned an invalid structured original_route");
|
|
255
|
+
}
|
|
256
|
+
const effectiveRoute = route === "exact_retry" ? originalRoute : route;
|
|
257
|
+
if (effectiveRoute === null) {
|
|
258
|
+
throw new VelaClientError("malformed_output", "landing has no effective route");
|
|
259
|
+
}
|
|
260
|
+
if (!mission.landing.expected_routes.includes(effectiveRoute)) {
|
|
261
|
+
throw new VelaClientError("unexpected_route", `landing route ${effectiveRoute} was not registered by the mission`);
|
|
262
|
+
}
|
|
263
|
+
const before = nullableCount(raw.accepted_event_count_before, "vela land.accepted_event_count_before");
|
|
264
|
+
const after = nullableCount(raw.accepted_event_count_after, "vela land.accepted_event_count_after");
|
|
265
|
+
const delta = nullableCount(raw.accepted_event_delta, "vela land.accepted_event_delta");
|
|
266
|
+
if ((before === null) !== (after === null) || (before === null) !== (delta === null)) {
|
|
267
|
+
throw new VelaClientError("malformed_output", "landing returned a partial accepted-event count tuple");
|
|
268
|
+
}
|
|
269
|
+
if (before !== null && after !== null && delta !== null && (after < before || delta !== after - before)) {
|
|
270
|
+
throw new VelaClientError("malformed_output", "landing accepted-event delta is inconsistent");
|
|
271
|
+
}
|
|
272
|
+
if (delta !== null && delta > mission.landing.max_accepted_delta) {
|
|
273
|
+
throw new VelaClientError("unexpected_route", `accepted-event delta ${delta} exceeds registered maximum`);
|
|
274
|
+
}
|
|
275
|
+
const expectedDelta = effectiveRoute === "permit" ? 1 : 0;
|
|
276
|
+
if (delta !== null && delta !== expectedDelta) {
|
|
277
|
+
throw new VelaClientError("malformed_output", `${effectiveRoute} durable landing requires accepted-event delta ${expectedDelta}`);
|
|
278
|
+
}
|
|
279
|
+
if (route !== "exact_retry" && delta === null) {
|
|
280
|
+
throw new VelaClientError("malformed_output", "new landing omitted accepted-event counts");
|
|
281
|
+
}
|
|
282
|
+
const publication = fieldObject(raw, "publication", "vela land");
|
|
283
|
+
const publicationState = stringAt(publication.state, "vela land.publication.state", {
|
|
284
|
+
min: 1,
|
|
285
|
+
max: 64,
|
|
286
|
+
});
|
|
287
|
+
if (publicationState !== "committed_local" &&
|
|
288
|
+
publicationState !== "pushed" &&
|
|
289
|
+
publicationState !== "unchanged") {
|
|
290
|
+
throw new VelaClientError("command_failed", `vela land was durable but not Git-published: ${publicationState}`);
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
operationId: stringAt(raw.operation_id, "vela land.operation_id", { min: 1, max: 256 }),
|
|
294
|
+
receiptRoot: normalizeSha256(raw.receipt_root, "vela land.receipt_root"),
|
|
295
|
+
recordId: stringAt(raw.record_id, "vela land.record_id", { min: 1, max: 256 }),
|
|
296
|
+
proposalId: stringAt(raw.proposal_id, "vela land.proposal_id", { min: 1, max: 256 }),
|
|
297
|
+
findingId: stringAt(raw.finding_id, "vela land.finding_id", { min: 1, max: 256 }),
|
|
298
|
+
route,
|
|
299
|
+
originalRoute,
|
|
300
|
+
rawRoute,
|
|
301
|
+
detail,
|
|
302
|
+
acceptedEventCountBefore: before,
|
|
303
|
+
acceptedEventCountAfter: after,
|
|
304
|
+
acceptedEventDelta: delta,
|
|
305
|
+
publication,
|
|
306
|
+
raw,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
export class VelaClient {
|
|
310
|
+
#binary;
|
|
311
|
+
#expectedVersion;
|
|
312
|
+
#expectedSha256;
|
|
313
|
+
#home;
|
|
314
|
+
#maxOutputBytes;
|
|
315
|
+
#timeoutMs;
|
|
316
|
+
#env;
|
|
317
|
+
#runner;
|
|
318
|
+
constructor(options) {
|
|
319
|
+
this.#binary = options.binary;
|
|
320
|
+
this.#expectedVersion = options.expectedVersion;
|
|
321
|
+
this.#expectedSha256 = options.expectedSha256;
|
|
322
|
+
this.#home = options.home;
|
|
323
|
+
this.#maxOutputBytes = options.maxOutputBytes ?? 16 * 1024 * 1024;
|
|
324
|
+
this.#timeoutMs = options.timeoutMs ?? 30_000;
|
|
325
|
+
// Deliberately do not accept caller-supplied environment entries. In
|
|
326
|
+
// particular, this prevents a harness integration from forwarding human
|
|
327
|
+
// key variables into the Vela control lane. Agent actors auto-mint only an
|
|
328
|
+
// agent session key beneath this isolated HOME.
|
|
329
|
+
this.#env = isolatedEnvironment(options.home);
|
|
330
|
+
this.#runner = options.runner ?? runCommand;
|
|
331
|
+
}
|
|
332
|
+
async #execute(argv, cwd) {
|
|
333
|
+
const result = await this.#runner({
|
|
334
|
+
argv,
|
|
335
|
+
cwd,
|
|
336
|
+
env: this.#env,
|
|
337
|
+
timeoutMs: this.#timeoutMs,
|
|
338
|
+
maxOutputBytes: this.#maxOutputBytes,
|
|
339
|
+
});
|
|
340
|
+
if (result.exitCode !== 0) {
|
|
341
|
+
throw new VelaClientError("command_failed", `${argv[0]} ${argv.slice(1).join(" ")} exited ${result.exitCode}: ${commandFailureSummary(result)}`);
|
|
342
|
+
}
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
async #json(args, cwd, label) {
|
|
346
|
+
const result = await this.#execute([this.#binary, ...args], cwd);
|
|
347
|
+
return parseJsonObject(result.stdout, label);
|
|
348
|
+
}
|
|
349
|
+
async #strictCheck(frontier, cwd, baseline) {
|
|
350
|
+
const argv = [this.#binary, "check", frontier, "--strict", "--json"];
|
|
351
|
+
const result = await this.#runner({
|
|
352
|
+
argv,
|
|
353
|
+
cwd,
|
|
354
|
+
env: this.#env,
|
|
355
|
+
timeoutMs: this.#timeoutMs,
|
|
356
|
+
maxOutputBytes: this.#maxOutputBytes,
|
|
357
|
+
});
|
|
358
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
359
|
+
throw new VelaClientError("command_failed", `${argv[0]} ${argv.slice(1).join(" ")} exited ${result.exitCode}: ${commandFailureSummary(result)}`);
|
|
360
|
+
}
|
|
361
|
+
const check = parseJsonObject(result.stdout, "vela check");
|
|
362
|
+
const actual = strictBaselineFromCheck(check);
|
|
363
|
+
if ((result.exitCode === 0) !== (actual.status === "pass")) {
|
|
364
|
+
throw new VelaClientError("malformed_output", "vela strict exit code and status disagree");
|
|
365
|
+
}
|
|
366
|
+
assertStrictBaseline(actual, baseline);
|
|
367
|
+
return check;
|
|
368
|
+
}
|
|
369
|
+
async observeStrictBaseline(repoRoot, frontier) {
|
|
370
|
+
const safeFrontier = frontierPath(frontier);
|
|
371
|
+
const argv = [this.#binary, "check", safeFrontier, "--strict", "--json"];
|
|
372
|
+
const result = await this.#runner({
|
|
373
|
+
argv,
|
|
374
|
+
cwd: repoRoot,
|
|
375
|
+
env: this.#env,
|
|
376
|
+
timeoutMs: this.#timeoutMs,
|
|
377
|
+
maxOutputBytes: this.#maxOutputBytes,
|
|
378
|
+
});
|
|
379
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
380
|
+
throw new VelaClientError("command_failed", `${argv[0]} ${argv.slice(1).join(" ")} exited ${result.exitCode}: ${commandFailureSummary(result)}`);
|
|
381
|
+
}
|
|
382
|
+
const check = parseJsonObject(result.stdout, "vela check");
|
|
383
|
+
const baseline = strictBaselineFromCheck(check);
|
|
384
|
+
if ((result.exitCode === 0) !== (baseline.status === "pass")) {
|
|
385
|
+
throw new VelaClientError("malformed_output", "vela strict exit code and status disagree");
|
|
386
|
+
}
|
|
387
|
+
return baseline;
|
|
388
|
+
}
|
|
389
|
+
async assertVersion(cwd) {
|
|
390
|
+
const binaryDigest = await sha256RegularFile(this.#binary, MAX_EXECUTABLE_BYTES);
|
|
391
|
+
if (binaryDigest !== this.#expectedSha256) {
|
|
392
|
+
throw new VelaClientError("version_mismatch", `Vela binary digest mismatch: expected ${this.#expectedSha256}, observed ${binaryDigest}`);
|
|
393
|
+
}
|
|
394
|
+
const result = await this.#execute([this.#binary, "--version"], cwd);
|
|
395
|
+
if (result.stderr.length !== 0) {
|
|
396
|
+
throw new VelaClientError("malformed_output", "vela --version wrote to stderr");
|
|
397
|
+
}
|
|
398
|
+
const observed = result.stdout.toString("utf8").trim();
|
|
399
|
+
const expected = `vela ${this.#expectedVersion}`;
|
|
400
|
+
if (observed !== expected) {
|
|
401
|
+
throw new VelaClientError("version_mismatch", `expected ${expected}, observed ${JSON.stringify(observed)}`);
|
|
402
|
+
}
|
|
403
|
+
return this.#expectedVersion;
|
|
404
|
+
}
|
|
405
|
+
async #gitObject(repoRoot, expression) {
|
|
406
|
+
const result = await this.#execute(["git", "rev-parse", "--verify", expression], repoRoot);
|
|
407
|
+
const observed = result.stdout.toString("utf8").trim();
|
|
408
|
+
if (!GIT_OBJECT_RE.test(observed)) {
|
|
409
|
+
throw new VelaClientError("malformed_output", `git rev-parse returned a non-full object ID for ${expression}`);
|
|
410
|
+
}
|
|
411
|
+
return observed;
|
|
412
|
+
}
|
|
413
|
+
async inspect(repoRoot, frontier, strictBaseline) {
|
|
414
|
+
const safeFrontier = frontierPath(frontier);
|
|
415
|
+
const version = await this.assertVersion(repoRoot);
|
|
416
|
+
const [gitCommit, gitTree, check] = await Promise.all([
|
|
417
|
+
this.#gitObject(repoRoot, "HEAD^{commit}"),
|
|
418
|
+
this.#gitObject(repoRoot, "HEAD^{tree}"),
|
|
419
|
+
this.#strictCheck(safeFrontier, repoRoot, strictBaseline),
|
|
420
|
+
]);
|
|
421
|
+
// Vela 0.9 makes the compact status projection the ordinary root reader.
|
|
422
|
+
// Historical binaries retain proof verification for exact Mission v0/v1
|
|
423
|
+
// replay, but a minimal 0.9 frontier intentionally has no proof bundle.
|
|
424
|
+
const replay = typeof check.replay === "object" && check.replay !== null
|
|
425
|
+
? fieldObject(check, "replay", "vela check")
|
|
426
|
+
: fieldObject(fieldObject(check, "state_integrity", "vela check"), "replay", "vela check.state_integrity");
|
|
427
|
+
const checkEvent = normalizeSha256(replay.event_log_hash, "vela check.replay.event_log_hash");
|
|
428
|
+
const checkCurrent = normalizeSha256(replay.current_hash, "vela check.replay.current_hash");
|
|
429
|
+
if (replay.replayed_hash !== undefined) {
|
|
430
|
+
assertEqual(normalizeSha256(replay.replayed_hash, "vela check.replay.replayed_hash"), checkCurrent, "Vela replayed snapshot");
|
|
431
|
+
}
|
|
432
|
+
if (replay.source_hash !== undefined) {
|
|
433
|
+
assertEqual(normalizeSha256(replay.source_hash, "vela check.replay.source_hash"), checkCurrent, "Vela source snapshot");
|
|
434
|
+
}
|
|
435
|
+
const proof = version === "0.900.0" || version === "0.900.1" || version === "0.900.2" || version === "0.901.0"
|
|
436
|
+
? {
|
|
437
|
+
ok: true,
|
|
438
|
+
command: "status_root_projection",
|
|
439
|
+
event_log_hash: checkEvent,
|
|
440
|
+
snapshot_hash: checkCurrent,
|
|
441
|
+
}
|
|
442
|
+
: await this.#json(["proof", "verify", safeFrontier, "--json"], repoRoot, "vela proof verify");
|
|
443
|
+
if (!bool(proof.ok, "vela root projection.ok")) {
|
|
444
|
+
throw new VelaClientError("command_failed", "vela root projection returned ok=false");
|
|
445
|
+
}
|
|
446
|
+
const proofEvent = normalizeSha256(proof.event_log_hash, "vela root projection.event_log_hash");
|
|
447
|
+
const proofSnapshot = normalizeSha256(proof.snapshot_hash, "vela root projection.snapshot_hash");
|
|
448
|
+
assertEqual(proofEvent, checkEvent, "check/proof event log");
|
|
449
|
+
assertEqual(proofSnapshot, checkCurrent, "check/proof snapshot");
|
|
450
|
+
if (proof.proof !== undefined) {
|
|
451
|
+
const proofBody = fieldObject(proof, "proof", "vela proof verify");
|
|
452
|
+
assertEqual(normalizeSha256(proofBody.event_log_hash, "vela proof verify.proof.event_log_hash"), checkEvent, "proof body event log");
|
|
453
|
+
assertEqual(normalizeSha256(proofBody.frontier_hash, "vela proof verify.proof.frontier_hash"), checkCurrent, "proof body snapshot");
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
version,
|
|
457
|
+
roots: {
|
|
458
|
+
git_commit: gitCommit,
|
|
459
|
+
git_tree: gitTree,
|
|
460
|
+
vela_event_log: checkEvent,
|
|
461
|
+
vela_snapshot: checkCurrent,
|
|
462
|
+
},
|
|
463
|
+
check,
|
|
464
|
+
proof,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
async assertRoots(repoRoot, frontier, expected, strictBaseline) {
|
|
468
|
+
const inspection = await this.inspect(repoRoot, frontier, strictBaseline);
|
|
469
|
+
compareRoots(inspection.roots, expected);
|
|
470
|
+
return inspection;
|
|
471
|
+
}
|
|
472
|
+
async next(mission, repoRoot) {
|
|
473
|
+
if (mission.vela_version !== this.#expectedVersion) {
|
|
474
|
+
throw new VelaClientError("version_mismatch", "mission and client Vela versions differ");
|
|
475
|
+
}
|
|
476
|
+
return await this.offer(repoRoot, mission.frontier, mission.roots, mission.schema === "canopus.mission.v1" ? mission.strict_baseline : undefined);
|
|
477
|
+
}
|
|
478
|
+
async offer(repoRoot, frontier, roots, strictBaseline, limit = 128) {
|
|
479
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 128) {
|
|
480
|
+
throw new VelaClientError("malformed_output", "vela next limit must be 1..128");
|
|
481
|
+
}
|
|
482
|
+
const safeFrontier = frontierPath(frontier);
|
|
483
|
+
await this.assertRoots(repoRoot, safeFrontier, roots, strictBaseline);
|
|
484
|
+
const value = await this.#json(["next", safeFrontier, "--limit", String(limit), "--json"], repoRoot, "vela next");
|
|
485
|
+
if (value.ok === false) {
|
|
486
|
+
throw new VelaClientError("command_failed", "vela next returned ok=false");
|
|
487
|
+
}
|
|
488
|
+
return { ok: true, value };
|
|
489
|
+
}
|
|
490
|
+
async work(mission, repoRoot, target, expected) {
|
|
491
|
+
stringAt(target, "target", { min: 1, max: 256 });
|
|
492
|
+
await this.assertRoots(repoRoot, mission.frontier, expected, mission.schema === "canopus.mission.v1" ? mission.strict_baseline : undefined);
|
|
493
|
+
const value = await this.#json(["work", target, "--frontier", mission.frontier, "--as", mission.actor, "--json"], repoRoot, "vela work");
|
|
494
|
+
if (value.ok === false) {
|
|
495
|
+
throw new VelaClientError("command_failed", "vela work returned ok=false");
|
|
496
|
+
}
|
|
497
|
+
return { ok: true, value };
|
|
498
|
+
}
|
|
499
|
+
async withdraw(repoRoot, frontier, proposalId, actor, reason) {
|
|
500
|
+
if (!/^vpr_[0-9a-f]{16}$/u.test(proposalId)) {
|
|
501
|
+
throw new VelaClientError("malformed_output", "withdrawal requires a full proposal id");
|
|
502
|
+
}
|
|
503
|
+
if (!actor.startsWith("agent:") || reason.trim().length === 0) {
|
|
504
|
+
throw new VelaClientError("malformed_output", "withdrawal requires an agent actor and reason");
|
|
505
|
+
}
|
|
506
|
+
const value = await this.#json(["review", "withdraw", frontierPath(frontier), proposalId, "--as", actor, "--reason", reason, "--json"], repoRoot, "vela review withdraw");
|
|
507
|
+
if (value.ok !== true || value.command !== "review.withdraw") {
|
|
508
|
+
throw new VelaClientError("malformed_output", "vela review withdraw returned an invalid envelope");
|
|
509
|
+
}
|
|
510
|
+
if (value.proposal_id !== proposalId || value.idempotent !== false || value.key_read !== true) {
|
|
511
|
+
throw new VelaClientError("malformed_output", "vela review withdraw did not bind one new key-backed withdrawal");
|
|
512
|
+
}
|
|
513
|
+
const eventId = stringAt(value.withdrawal_event_id, "vela review withdraw.withdrawal_event_id", {
|
|
514
|
+
max: 20,
|
|
515
|
+
pattern: /^vev_[0-9a-f]{16}$/u,
|
|
516
|
+
});
|
|
517
|
+
const withdrawnAt = stringAt(value.withdrawn_at, "vela review withdraw.withdrawn_at", {
|
|
518
|
+
min: 20,
|
|
519
|
+
max: 64,
|
|
520
|
+
});
|
|
521
|
+
if (!Number.isFinite(Date.parse(withdrawnAt))) {
|
|
522
|
+
throw new VelaClientError("malformed_output", "vela review withdraw returned an invalid timestamp");
|
|
523
|
+
}
|
|
524
|
+
const stateRootBefore = normalizeSha256(value.state_root_before, "vela review withdraw.state_root_before");
|
|
525
|
+
const stateRootAfter = normalizeSha256(value.state_root_after, "vela review withdraw.state_root_after");
|
|
526
|
+
if (stateRootBefore === stateRootAfter) {
|
|
527
|
+
throw new VelaClientError("malformed_output", "new withdrawal did not change the event-log root");
|
|
528
|
+
}
|
|
529
|
+
const publication = fieldObject(value, "publication", "vela review withdraw");
|
|
530
|
+
const publicationState = stringAt(publication.state, "vela review withdraw.publication.state", {
|
|
531
|
+
min: 1,
|
|
532
|
+
max: 32,
|
|
533
|
+
});
|
|
534
|
+
if (publicationState !== "committed_local" && publicationState !== "pushed") {
|
|
535
|
+
throw new VelaClientError("command_failed", `withdrawal publication is ${publicationState}, not committed`);
|
|
536
|
+
}
|
|
537
|
+
const publicationCommit = stringAt(publication.commit, "vela review withdraw.publication.commit", { max: 64, pattern: GIT_OBJECT_RE });
|
|
538
|
+
return {
|
|
539
|
+
proposal_id: proposalId,
|
|
540
|
+
withdrawal_event_id: eventId,
|
|
541
|
+
withdrawn_at: withdrawnAt,
|
|
542
|
+
state_root_before: stateRootBefore,
|
|
543
|
+
state_root_after: stateRootAfter,
|
|
544
|
+
publication_commit: publicationCommit,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
async landAuthoredCommand(mission, repoRoot, input, _expected) {
|
|
548
|
+
if ((input.predictedObservable === undefined) === (input.notApplicable === undefined)) {
|
|
549
|
+
throw new VelaClientError("malformed_output", "authored receipt requires exactly one prediction mode");
|
|
550
|
+
}
|
|
551
|
+
// The caller performs the strict exact-root check immediately before it
|
|
552
|
+
// installs the candidate artifacts. Those files legitimately stale the
|
|
553
|
+
// derived artifacts hash until `vela land` records them atomically, so a
|
|
554
|
+
// second strict check here would make the normal porcelain path unusable.
|
|
555
|
+
const args = [
|
|
556
|
+
"land",
|
|
557
|
+
"--frontier",
|
|
558
|
+
mission.frontier,
|
|
559
|
+
"--claim",
|
|
560
|
+
input.claim,
|
|
561
|
+
"--type",
|
|
562
|
+
input.claimType,
|
|
563
|
+
"--replayability",
|
|
564
|
+
input.replayability,
|
|
565
|
+
];
|
|
566
|
+
for (const artifact of input.artifacts) {
|
|
567
|
+
args.push("--artifact", `${relativePathAt(artifact.path, "artifact.path")}:${artifact.kind}`);
|
|
568
|
+
}
|
|
569
|
+
for (const caveat of input.caveats)
|
|
570
|
+
args.push("--caveat", caveat);
|
|
571
|
+
if (input.predictedObservable !== undefined) {
|
|
572
|
+
args.push("--predicted-observable", input.predictedObservable);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
args.push("--not-applicable");
|
|
576
|
+
}
|
|
577
|
+
args.push("--performed-test", input.performedTest, "--result", input.result);
|
|
578
|
+
for (const evidence of input.evidence)
|
|
579
|
+
args.push("--evidence", evidence);
|
|
580
|
+
for (const counterevidence of input.counterevidence) {
|
|
581
|
+
args.push("--counterevidence", counterevidence);
|
|
582
|
+
}
|
|
583
|
+
if (input.executionBinding !== undefined) {
|
|
584
|
+
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);
|
|
585
|
+
}
|
|
586
|
+
if (input.work !== undefined)
|
|
587
|
+
args.push("--work", input.work);
|
|
588
|
+
args.push("--as", mission.actor, "--json");
|
|
589
|
+
const argv = [this.#binary, ...args];
|
|
590
|
+
if (argv.length > MAX_COMMAND_ARGV) {
|
|
591
|
+
throw new VelaClientError("malformed_output", `authored Receipt requires ${argv.length} argv entries; maximum is ${MAX_COMMAND_ARGV}`);
|
|
592
|
+
}
|
|
593
|
+
const result = await this.#runner({
|
|
594
|
+
argv,
|
|
595
|
+
cwd: repoRoot,
|
|
596
|
+
env: this.#env,
|
|
597
|
+
timeoutMs: this.#timeoutMs,
|
|
598
|
+
maxOutputBytes: this.#maxOutputBytes,
|
|
599
|
+
});
|
|
600
|
+
return {
|
|
601
|
+
argv,
|
|
602
|
+
exit_code: result.exitCode,
|
|
603
|
+
stdout: result.stdout.toString("utf8"),
|
|
604
|
+
stderr: result.stderr.toString("utf8"),
|
|
605
|
+
stdout_digest: sha256Bytes(result.stdout),
|
|
606
|
+
stderr_digest: sha256Bytes(result.stderr),
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
parseLandCommand(observation) {
|
|
610
|
+
if (observation.exit_code !== 0) {
|
|
611
|
+
throw new VelaClientError("command_failed", `vela land exited ${observation.exit_code}: ${observation.stderr}`);
|
|
612
|
+
}
|
|
613
|
+
return parseJsonObject(Buffer.from(observation.stdout), "vela land");
|
|
614
|
+
}
|
|
615
|
+
validateLandResult(mission, raw) {
|
|
616
|
+
return validateLandResult(mission, raw);
|
|
617
|
+
}
|
|
618
|
+
async verifyReceiptBinding(mission, repoRoot, landing, input, artifacts) {
|
|
619
|
+
const hex = landing.receiptRoot.slice("sha256:".length);
|
|
620
|
+
const receiptPath = path.join(repoRoot, mission.frontier, "records", "receipts", "sha256", `${hex}.json`);
|
|
621
|
+
const bytes = await readBoundedRegularFile(receiptPath, 8 * 1024 * 1024);
|
|
622
|
+
let parsed;
|
|
623
|
+
try {
|
|
624
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
throw new VelaClientError("malformed_output", `retained receipt is not JSON: ${String(error)}`);
|
|
628
|
+
}
|
|
629
|
+
const receipt = objectAt(parsed, "retained receipt");
|
|
630
|
+
if (sha256Bytes(canonicalJcs(receipt)) !== landing.receiptRoot) {
|
|
631
|
+
throw new VelaClientError("root_mismatch", "retained Receipt v1 root does not match vela land");
|
|
632
|
+
}
|
|
633
|
+
const recordPath = path.join(repoRoot, mission.frontier, "records", `${landing.recordId}.json`);
|
|
634
|
+
let recordValue;
|
|
635
|
+
try {
|
|
636
|
+
recordValue = JSON.parse((await readBoundedRegularFile(recordPath, 8 * 1024 * 1024)).toString("utf8"));
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
throw new VelaClientError("malformed_output", `retained activity record is unavailable or malformed: ${String(error)}`);
|
|
640
|
+
}
|
|
641
|
+
const record = objectAt(recordValue, "retained activity record");
|
|
642
|
+
const expectedReceiptPath = `records/receipts/sha256/${hex}.json`;
|
|
643
|
+
if (record.id !== landing.recordId ||
|
|
644
|
+
record.receipt_digest !== landing.receiptRoot ||
|
|
645
|
+
record.receipt_path !== expectedReceiptPath) {
|
|
646
|
+
throw new VelaClientError("root_mismatch", "retained activity record does not bind the landed Receipt v1");
|
|
647
|
+
}
|
|
648
|
+
for (const [actual, expected, label] of [
|
|
649
|
+
[receipt.claim, input.claim, "claim"],
|
|
650
|
+
[receipt.type, input.claimType, "claim type"],
|
|
651
|
+
[receipt.replayability, input.replayability, "replayability"],
|
|
652
|
+
]) {
|
|
653
|
+
if (actual !== expected) {
|
|
654
|
+
throw new VelaClientError("malformed_output", `retained receipt ${label} drifted`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (JSON.stringify(receipt.caveats) !== JSON.stringify(input.caveats)) {
|
|
658
|
+
throw new VelaClientError("malformed_output", "retained receipt caveats drifted");
|
|
659
|
+
}
|
|
660
|
+
const rawArtifacts = receipt.artifacts;
|
|
661
|
+
if (!Array.isArray(rawArtifacts) || rawArtifacts.length !== artifacts.length) {
|
|
662
|
+
throw new VelaClientError("malformed_output", "retained receipt artifact set drifted");
|
|
663
|
+
}
|
|
664
|
+
const recordArtifacts = record.artifacts;
|
|
665
|
+
if (!Array.isArray(recordArtifacts) || recordArtifacts.length !== artifacts.length) {
|
|
666
|
+
throw new VelaClientError("malformed_output", "activity record artifact set drifted");
|
|
667
|
+
}
|
|
668
|
+
for (const [index, expected] of artifacts.entries()) {
|
|
669
|
+
const rawArtifact = objectAt(rawArtifacts[index], `retained receipt.artifacts[${index}]`);
|
|
670
|
+
if (rawArtifact.path !== expected.path ||
|
|
671
|
+
rawArtifact.kind !== expected.kind ||
|
|
672
|
+
rawArtifact.sha256 !== expected.digest.slice("sha256:".length)) {
|
|
673
|
+
throw new VelaClientError("malformed_output", `retained receipt artifact ${index} drifted`);
|
|
674
|
+
}
|
|
675
|
+
const recordArtifact = objectAt(recordArtifacts[index], `retained activity record.artifacts[${index}]`);
|
|
676
|
+
const hexDigest = expected.digest.slice("sha256:".length);
|
|
677
|
+
if (recordArtifact.kind !== expected.kind ||
|
|
678
|
+
recordArtifact.sha256 !== hexDigest ||
|
|
679
|
+
recordArtifact.locator !== `records/artifacts/sha256/${hexDigest}` ||
|
|
680
|
+
recordArtifact.size_bytes !== expected.bytes ||
|
|
681
|
+
recordArtifact.locator_integrity !== "immutable" ||
|
|
682
|
+
recordArtifact.availability !== "available") {
|
|
683
|
+
throw new VelaClientError("malformed_output", `activity record artifact ${index} is not an immutable available blob binding`);
|
|
684
|
+
}
|
|
685
|
+
const installed = await readBoundedRegularFile(retainedArtifactPath(repoRoot, mission.frontier, expected.digest), mission.budgets.max_artifact_bytes);
|
|
686
|
+
if (installed.length !== expected.bytes || sha256Bytes(installed) !== expected.digest) {
|
|
687
|
+
throw new VelaClientError("root_mismatch", `retained artifact ${expected.path} drifted`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const environment = fieldObject(receipt, "environment", "retained receipt");
|
|
691
|
+
if (input.executionBinding === undefined) {
|
|
692
|
+
if (environment["vela:execution_binding"] !== undefined) {
|
|
693
|
+
throw new VelaClientError("malformed_output", "retained receipt invented an exact execution binding");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
const binding = fieldObject(environment, "vela:execution_binding", "retained receipt.environment");
|
|
698
|
+
if (canonicalJcs(binding) !== canonicalJcs(input.executionBinding)) {
|
|
699
|
+
throw new VelaClientError("root_mismatch", "retained exact execution binding drifted");
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const chain = fieldObject(environment, "vela:scientific_chain", "retained receipt.environment");
|
|
703
|
+
const expectedChain = {
|
|
704
|
+
schema: "vela.scientific-chain.producer.v1",
|
|
705
|
+
authority: "producer",
|
|
706
|
+
...(input.predictedObservable === undefined
|
|
707
|
+
? { not_applicable: true }
|
|
708
|
+
: { predicted_observable: input.predictedObservable, not_applicable: false }),
|
|
709
|
+
performed_test: input.performedTest,
|
|
710
|
+
result: input.result,
|
|
711
|
+
evidence: input.evidence,
|
|
712
|
+
counterevidence: input.counterevidence,
|
|
713
|
+
};
|
|
714
|
+
if (canonicalJcs(chain) !== canonicalJcs(expectedChain)) {
|
|
715
|
+
throw new VelaClientError("malformed_output", "retained scientific-chain assertion drifted");
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|