@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.
Files changed (120) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +67 -139
  3. package/SECURITY.md +3 -2
  4. package/capsules/erdos1056-k15/bin/linux-arm64/{10428801-10429000 → 10429201-10429400}/verifier +0 -0
  5. package/capsules/erdos1056-k15/bin/linux-x86_64/{10428801-10429000 → 10429201-10429400}/verifier +0 -0
  6. package/compatibility.json +25 -0
  7. package/dist/src/activity/events.d.ts +4 -1
  8. package/dist/src/activity/events.js +8 -2
  9. package/dist/src/activity/store.d.ts +2 -2
  10. package/dist/src/activity/store.js +4 -3
  11. package/dist/src/{receipt/map.d.ts → candidate/finalize.d.ts} +2 -9
  12. package/dist/src/candidate/finalize.js +71 -0
  13. package/dist/src/cli.js +88 -229
  14. package/dist/src/contracts/mission.d.ts +1 -13
  15. package/dist/src/contracts/mission.js +4 -42
  16. package/dist/src/contracts/validation.d.ts +1 -25
  17. package/dist/src/contracts/validation.js +1 -95
  18. package/dist/src/engines/codex-tools-native.js +2 -2
  19. package/dist/src/index.d.ts +5 -34
  20. package/dist/src/index.js +4 -34
  21. package/dist/src/mission/prepare.d.ts +10 -1
  22. package/dist/src/mission/prepare.js +29 -30
  23. package/dist/src/product/coverage.d.ts +17 -0
  24. package/dist/src/product/coverage.js +137 -0
  25. package/dist/src/product/doctor.d.ts +1 -2
  26. package/dist/src/product/doctor.js +29 -8
  27. package/dist/src/product/profile.js +8 -2
  28. package/dist/src/product/replay.js +11 -12
  29. package/dist/src/product/run.d.ts +5 -9
  30. package/dist/src/product/run.js +17 -88
  31. package/dist/src/product/submission.d.ts +40 -0
  32. package/dist/src/product/submission.js +217 -0
  33. package/dist/src/product/submit.d.ts +22 -0
  34. package/dist/src/product/submit.js +264 -0
  35. package/dist/src/product/version.d.ts +2 -2
  36. package/dist/src/product/version.js +27 -4
  37. package/dist/src/projection/current-run.d.ts +12 -0
  38. package/dist/src/projection/current-run.js +94 -0
  39. package/dist/src/projection/failure.d.ts +17 -7
  40. package/dist/src/projection/failure.js +43 -12
  41. package/dist/src/projection/retained-mission.d.ts +6 -0
  42. package/dist/src/projection/retained-mission.js +41 -0
  43. package/dist/src/projection/retained-run.d.ts +7 -0
  44. package/dist/src/projection/retained-run.js +50 -0
  45. package/dist/src/run.d.ts +43 -41
  46. package/dist/src/run.js +34 -412
  47. package/dist/src/util/canonical.d.ts +1 -5
  48. package/dist/src/util/canonical.js +1 -53
  49. package/dist/src/vela/cli.d.ts +7 -51
  50. package/dist/src/vela/cli.js +102 -523
  51. package/dist/src/vela/types.d.ts +3 -19
  52. package/dist/src/verifier/run.js +1 -1
  53. package/dist/src/workspace/prepare.d.ts +1 -1
  54. package/dist/src/workspace/prepare.js +15 -15
  55. package/docs/EVALUATION.md +142 -0
  56. package/docs/MISSIONS.md +42 -80
  57. package/docs/RUN_RECORD.md +77 -99
  58. package/docs/adr/0010-nonmutating-runs-and-explicit-submission.md +191 -0
  59. package/missions/erdos1056-k15-next/mission.draft.json +10 -8
  60. package/package.json +18 -22
  61. package/profiles/{erdos1056-k15-10428801-10429000.json → erdos1056-k15-10429201-10429400.json} +10 -10
  62. package/schemas/failure.v1.json +17 -0
  63. package/schemas/mission.v0.json +3 -4
  64. package/schemas/mission.v1.json +7 -28
  65. package/schemas/{run.v1.json → run.v2.json} +58 -116
  66. package/schemas/submission-bundle.v1.json +63 -0
  67. package/toolchain.lock.json +31 -0
  68. package/BUILD_WEEK.md +0 -198
  69. package/advisories/erdos1056-claim-fidelity/output.schema.json +0 -90
  70. package/advisories/erdos1056-claim-fidelity/registration.json +0 -42
  71. package/advisories/erdos1056-claim-fidelity/results/assessment.json +0 -1
  72. package/advisories/erdos1056-claim-fidelity/results/verification.json +0 -1
  73. package/capsules/formal-erdos-505-test-dim-one/verifier +0 -105
  74. package/capsules/quantum-10-1-4/verifier.py +0 -152
  75. package/capsules/sidon-a24/bin/linux-arm64/verifier +0 -0
  76. package/capsules/sidon-a24/bin/linux-x86_64/verifier +0 -0
  77. package/dist/src/capability/withdrawal.d.ts +0 -47
  78. package/dist/src/capability/withdrawal.js +0 -487
  79. package/dist/src/engines/codex-exec.d.ts +0 -17
  80. package/dist/src/engines/codex-exec.js +0 -136
  81. package/dist/src/engines/codex-tool-free.d.ts +0 -26
  82. package/dist/src/engines/codex-tool-free.js +0 -213
  83. package/dist/src/product/withdraw.d.ts +0 -8
  84. package/dist/src/product/withdraw.js +0 -255
  85. package/dist/src/projection/diagnostic.d.ts +0 -12
  86. package/dist/src/projection/diagnostic.js +0 -61
  87. package/dist/src/projection/public-run.d.ts +0 -59
  88. package/dist/src/projection/public-run.js +0 -106
  89. package/dist/src/projection/publication.d.ts +0 -36
  90. package/dist/src/projection/publication.js +0 -47
  91. package/dist/src/projection/run.d.ts +0 -95
  92. package/dist/src/projection/run.js +0 -173
  93. package/dist/src/receipt/map.js +0 -168
  94. package/dist/src/repair/plan.d.ts +0 -2
  95. package/dist/src/repair/plan.js +0 -22
  96. package/dist/src/roles.d.ts +0 -2
  97. package/dist/src/roles.js +0 -9
  98. package/docs/RELEASES.md +0 -1389
  99. package/evidence/build-week/run_eb6bcd46-cffd-4ae8-b630-2681bd84da71.public.json +0 -1
  100. package/evidence/build-week/run_f68e4cfc-e5c7-4c73-86cb-d79807c47ec4.public.json +0 -1
  101. package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/pending-commands.json +0 -1
  102. package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/public-run.json +0 -1
  103. package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/root-manifest.json +0 -1
  104. package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/web-import.json +0 -1
  105. package/evidence/erdos/run_b3b0cf07-b149-4d53-8258-76ba0e1fc0a5/pending-commands.json +0 -1
  106. package/evidence/erdos/run_b3b0cf07-b149-4d53-8258-76ba0e1fc0a5/public-run.json +0 -1
  107. package/evidence/erdos/run_b3b0cf07-b149-4d53-8258-76ba0e1fc0a5/root-manifest.json +0 -1
  108. package/evidence/erdos/run_b3b0cf07-b149-4d53-8258-76ba0e1fc0a5/web-import.json +0 -1
  109. package/missions/formal-erdos-505-test-dim-one-gpt56/mission.draft.json +0 -40
  110. package/missions/quantum-10-1-4-retry/mission.draft.json +0 -40
  111. package/missions/sidon-a24-at-least-7194-gpt56/mission.draft.json +0 -42
  112. package/missions/sidon-a24-at-least-7194-gpt56-v2/mission.draft.json +0 -42
  113. package/missions/sidon-a24-at-least-7194-gpt56-v3/mission.draft.json +0 -42
  114. package/profiles/formal-erdos-505-test-dim-one-gpt56.json +0 -35
  115. package/profiles/quantum-10-1-4-stabilizer-retry.json +0 -35
  116. package/profiles/sidon-a24-at-least-7194-gpt56-v2.json +0 -35
  117. package/profiles/sidon-a24-at-least-7194-gpt56-v3.json +0 -35
  118. package/profiles/sidon-a24-at-least-7194-gpt56.json +0 -35
  119. package/schemas/canopus.public-run.v1.json +0 -116
  120. package/scripts/run-claim-fidelity-advisory.mjs +0 -235
@@ -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 { 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
- // Real large frontiers can spend tens of seconds in Vela's recoverable
7
- // transaction preparation and derived-view materialization. Keep this well
8
- // below mission wall-time budgets while allowing the measured 38.6-second
9
- // Erdős work operation to reach its atomic commit instead of being killed in
10
- // Prepared state.
11
- export const DEFAULT_VELA_COMMAND_TIMEOUT_MS = 120_000;
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.vela_event_log, expected.vela_event_log, "Vela event log");
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
- // Deliberately do not accept caller-supplied environment entries. In
339
- // particular, this prevents a harness integration from forwarding human
340
- // key variables into the Vela control lane. Agent actors auto-mint only an
341
- // agent session key beneath this isolated HOME.
342
- this.#env = isolatedEnvironment(options.home);
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, strictBaseline) {
203
+ async inspect(repoRoot, frontier) {
427
204
  const safeFrontier = frontierPath(frontier);
428
205
  const version = await this.assertVersion(repoRoot);
429
- const [gitCommit, gitTree, check] = await Promise.all([
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.#strictCheck(safeFrontier, repoRoot, strictBaseline),
209
+ this.#json(["status", safeFrontier, "--json"], repoRoot, "vela status"),
210
+ this.#json(["repository", "verify", safeFrontier, "--json"], repoRoot, "vela repository verify"),
433
211
  ]);
434
- // Vela 0.9 makes the compact status projection the ordinary root reader.
435
- // Historical binaries retain proof verification for exact Mission v0/v1
436
- // replay, but a minimal 0.9 frontier intentionally has no proof bundle.
437
- const replay = typeof check.replay === "object" && check.replay !== null
438
- ? fieldObject(check, "replay", "vela check")
439
- : fieldObject(fieldObject(check, "state_integrity", "vela check"), "replay", "vela check.state_integrity");
440
- const checkEvent = normalizeSha256(replay.event_log_hash, "vela check.replay.event_log_hash");
441
- const checkCurrent = normalizeSha256(replay.current_hash, "vela check.replay.current_hash");
442
- if (replay.replayed_hash !== undefined) {
443
- assertEqual(normalizeSha256(replay.replayed_hash, "vela check.replay.replayed_hash"), checkCurrent, "Vela replayed snapshot");
444
- }
445
- if (replay.source_hash !== undefined) {
446
- assertEqual(normalizeSha256(replay.source_hash, "vela check.replay.source_hash"), checkCurrent, "Vela source snapshot");
447
- }
448
- const proof = usesCompactStatusProjection(version)
449
- ? {
450
- ok: true,
451
- command: "status_root_projection",
452
- event_log_hash: checkEvent,
453
- snapshot_hash: checkCurrent,
454
- }
455
- : await this.#json(["proof", "verify", safeFrontier, "--json"], repoRoot, "vela proof verify");
456
- if (!bool(proof.ok, "vela root projection.ok")) {
457
- throw new VelaClientError("command_failed", "vela root projection returned ok=false");
458
- }
459
- const proofEvent = normalizeSha256(proof.event_log_hash, "vela root projection.event_log_hash");
460
- const proofSnapshot = normalizeSha256(proof.snapshot_hash, "vela root projection.snapshot_hash");
461
- assertEqual(proofEvent, checkEvent, "check/proof event log");
462
- assertEqual(proofSnapshot, checkCurrent, "check/proof snapshot");
463
- if (proof.proof !== undefined) {
464
- const proofBody = fieldObject(proof, "proof", "vela proof verify");
465
- assertEqual(normalizeSha256(proofBody.event_log_hash, "vela proof verify.proof.event_log_hash"), checkEvent, "proof body event log");
466
- assertEqual(normalizeSha256(proofBody.frontier_hash, "vela proof verify.proof.frontier_hash"), checkCurrent, "proof body snapshot");
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
- vela_event_log: checkEvent,
474
- vela_snapshot: checkCurrent,
275
+ vela_repository: repositoryRoot,
475
276
  },
476
- check,
477
- proof,
277
+ status,
278
+ repository,
478
279
  };
479
280
  }
480
- async assertRoots(repoRoot, frontier, expected, strictBaseline) {
481
- const inspection = await this.inspect(repoRoot, frontier, strictBaseline);
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, mission.schema === "canopus.mission.v1" ? mission.strict_baseline : undefined);
290
+ return await this.offer(repoRoot, mission.frontier, mission.roots);
490
291
  }
491
- async offer(repoRoot, frontier, roots, strictBaseline, limit = 128) {
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, strictBaseline);
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
- return { ok: true, value };
502
- }
503
- async work(mission, repoRoot, target, expected) {
504
- stringAt(target, "target", { min: 1, max: 256 });
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
  }