@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
@@ -2,11 +2,9 @@ import { mkdtemp, realpath, rm } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { BudgetTracker } from "../budget/enforce.js";
5
- import { parseMission } from "../contracts/mission.js";
6
5
  import { validateMissionBundle } from "../mission/prepare.js";
7
- import { parseDiagnosticRunRecord } from "../projection/diagnostic.js";
8
- import { parseRunRecord } from "../projection/run.js";
9
- import { contentDigest } from "../util/canonical.js";
6
+ import { parseRetainedMission } from "../projection/retained-mission.js";
7
+ import { parseRetainedRunRecord } from "../projection/retained-run.js";
10
8
  import { readBoundedRegularFile } from "../util/files.js";
11
9
  import { runVerifier } from "../verifier/run.js";
12
10
  import { cleanupWorkspace, prepareWorkspace } from "../workspace/prepare.js";
@@ -17,16 +15,17 @@ export async function replayProduct(runFile, dockerBinary = "docker") {
17
15
  const schema = typeof raw === "object" && raw !== null && !Array.isArray(raw)
18
16
  ? raw.schema
19
17
  : undefined;
20
- const record = schema === "canopus.diagnostic-run.v1"
21
- ? parseDiagnosticRunRecord(raw)
22
- : parseRunRecord(raw);
18
+ if (schema !== "canopus.run.v2") {
19
+ throw new Error("current Canopus replays only canopus.run.v2; use the exact historical release for older Run schemas");
20
+ }
21
+ const record = parseRetainedRunRecord(raw).record;
23
22
  const bundleRoot = await realpath(path.join(runRoot, "..", "mission"));
24
- const mission = parseMission(JSON.parse((await readBoundedRegularFile(path.join(bundleRoot, "mission.json"), 8 * 1024 * 1024)).toString("utf8")));
25
- if (mission.schema !== "canopus.mission.v1")
26
- throw new Error("product replay requires Mission v1");
27
- await validateMissionBundle(mission, bundleRoot);
28
- if (contentDigest(mission) !== record.mission.digest)
23
+ const retainedMission = parseRetainedMission(JSON.parse((await readBoundedRegularFile(path.join(bundleRoot, "mission.json"), 8 * 1024 * 1024)).toString("utf8")));
24
+ const mission = retainedMission.mission;
25
+ await validateMissionBundle(mission, bundleRoot, retainedMission.exactRoot);
26
+ if (retainedMission.exactRoot !== record.mission.digest) {
29
27
  throw new Error("run and mission roots disagree");
28
+ }
30
29
  const artifacts = record.candidate.artifacts.map((artifact) => ({
31
30
  artifact,
32
31
  frozenPath: path.join(runRoot, "artifacts", artifact.digest.slice("sha256:".length)),
@@ -1,22 +1,19 @@
1
- import { type CanopusDiagnosticRunResult, type CanopusRunResult } from "../run.js";
1
+ import { type CanopusCurrentRunResult } from "../run.js";
2
2
  import { type CommandRunner } from "../util/command.js";
3
3
  import { type ProductDoctorResult } from "./doctor.js";
4
4
  export interface ProductRunResult {
5
- run: CanopusRunResult | CanopusDiagnosticRunResult;
5
+ run: CanopusCurrentRunResult;
6
6
  doctor: ProductDoctorResult;
7
7
  output_root: string;
8
8
  bundle_root: string;
9
9
  evidence_manifest: string;
10
10
  evidence_root: string;
11
- source_publication: {
12
- state: "committed_local";
13
- commit: string;
14
- tree: string;
15
- } | {
16
- state: "unchanged_no_land";
11
+ source_state: {
12
+ state: "unchanged";
17
13
  commit: string;
18
14
  tree: string;
19
15
  };
16
+ submission: null;
20
17
  }
21
18
  export declare function defaultProductOutput(frontier: string): string;
22
19
  export declare function assertToolUsingMissionPlatform(platform?: NodeJS.Platform): void;
@@ -26,6 +23,5 @@ export declare function runProduct(options: {
26
23
  requestedTarget?: string;
27
24
  outputRoot?: string;
28
25
  codexHome?: string;
29
- noLand?: boolean;
30
26
  runner?: CommandRunner;
31
27
  }): Promise<ProductRunResult>;
@@ -6,12 +6,12 @@ import { CodexToolsNativeEngine } from "../engines/codex-tools-native.js";
6
6
  import { prepareMission } from "../mission/prepare.js";
7
7
  import { runCanopus, } from "../run.js";
8
8
  import { canonicalJson, contentDigest, sha256Bytes } from "../util/canonical.js";
9
- import { isolatedEnvironment, runCommand } from "../util/command.js";
9
+ import { runCommand } from "../util/command.js";
10
10
  import { readBoundedRegularFile } from "../util/files.js";
11
11
  import { VelaClient } from "../vela/cli.js";
12
12
  import { doctorProduct } from "./doctor.js";
13
+ import { assertMissionNotCovered } from "./coverage.js";
13
14
  import { loadProfileDraft, loadProfileResultContract, packagedWorkerProfile, stageProfileCapsule, } from "./profile.js";
14
- import { retainWithdrawalCapability } from "../capability/withdrawal.js";
15
15
  function packageFile(relative) {
16
16
  return fileURLToPath(new URL(`../../../${relative}`, import.meta.url));
17
17
  }
@@ -39,58 +39,6 @@ async function assertFreshOutput(outputRoot, sourceRoot) {
39
39
  }
40
40
  await mkdir(output, { recursive: true, mode: 0o700 });
41
41
  }
42
- async function gitText(options) {
43
- const result = await options.runner({
44
- argv: ["git", ...options.argv],
45
- cwd: options.cwd,
46
- env: isolatedEnvironment(options.home),
47
- timeoutMs: 120_000,
48
- maxOutputBytes: 8 * 1024 * 1024,
49
- });
50
- if (result.exitCode !== 0 || result.stderr.length !== 0) {
51
- throw new Error(`git ${options.argv.join(" ")} failed: stdout_sha256=${sha256Bytes(result.stdout)}; ` +
52
- `stderr_sha256=${sha256Bytes(result.stderr)}`);
53
- }
54
- return result.stdout.toString("utf8").trim();
55
- }
56
- async function publishVerifiedLanding(options) {
57
- const [head, status] = await Promise.all([
58
- gitText({ argv: ["rev-parse", "--verify", "HEAD^{commit}"], cwd: options.source, home: options.home, runner: options.runner }),
59
- gitText({ argv: ["status", "--porcelain=v1", "--untracked-files=all"], cwd: options.source, home: options.home, runner: options.runner }),
60
- ]);
61
- if (head !== options.expectedStart || status !== "") {
62
- throw new Error("source frontier changed while the bounded run was executing");
63
- }
64
- const landingHead = await gitText({
65
- argv: ["rev-parse", "--verify", "canopus-landing^{commit}"],
66
- cwd: options.landing,
67
- home: options.home,
68
- runner: options.runner,
69
- });
70
- if (landingHead !== options.expectedFinal)
71
- throw new Error("landing clone final commit drifted");
72
- await gitText({
73
- argv: ["fetch", "--quiet", "--no-tags", options.landing, "canopus-landing"],
74
- cwd: options.source,
75
- home: options.home,
76
- runner: options.runner,
77
- });
78
- await gitText({
79
- argv: ["merge", "--ff-only", "--no-edit", "FETCH_HEAD"],
80
- cwd: options.source,
81
- home: options.home,
82
- runner: options.runner,
83
- });
84
- const [commit, tree, finalStatus] = await Promise.all([
85
- gitText({ argv: ["rev-parse", "--verify", "HEAD^{commit}"], cwd: options.source, home: options.home, runner: options.runner }),
86
- gitText({ argv: ["rev-parse", "--verify", "HEAD^{tree}"], cwd: options.source, home: options.home, runner: options.runner }),
87
- gitText({ argv: ["status", "--porcelain=v1", "--untracked-files=all"], cwd: options.source, home: options.home, runner: options.runner }),
88
- ]);
89
- if (commit !== options.expectedFinal || tree !== options.expectedTree || finalStatus !== "") {
90
- throw new Error("source publication did not reproduce the verified landing roots");
91
- }
92
- return { state: "committed_local", commit, tree };
93
- }
94
42
  async function writeEvidenceManifest(run, missionDigest) {
95
43
  const root = run.paths.root;
96
44
  const files = {
@@ -115,10 +63,8 @@ async function writeEvidenceManifest(run, missionDigest) {
115
63
  files: digests,
116
64
  artifact_roots: run.record.candidate.artifacts.map((artifact) => artifact.digest).sort(),
117
65
  verifier_root: contentDigest(run.record.verifier),
118
- receipt_root: run.record.landing?.receipt_root ?? null,
119
- final_roots: "final_roots" in run.record
120
- ? run.record.final_roots
121
- : run.record.mission.starting_roots,
66
+ submission_root: null,
67
+ final_roots: run.record.mission.starting_roots,
122
68
  };
123
69
  const file = path.join(root, "evidence-manifest.json");
124
70
  await writeFile(file, canonicalJson(manifest), { flag: "wx", mode: 0o600 });
@@ -143,9 +89,6 @@ export async function runProduct(options) {
143
89
  const runner = options.runner ?? runCommand;
144
90
  const source = await realpath(options.frontier);
145
91
  const outputRoot = path.resolve(options.outputRoot ?? defaultProductOutput(source));
146
- await assertFreshOutput(outputRoot, source);
147
- const controlHome = path.join(outputRoot, "control-home");
148
- await mkdir(controlHome, { mode: 0o700 });
149
92
  try {
150
93
  const diagnosis = await doctorProduct({
151
94
  frontier: source,
@@ -158,6 +101,9 @@ export async function runProduct(options) {
158
101
  if (!diagnosis.public.worker.mission_ready || codexRuntime === null || dockerRuntime === null) {
159
102
  throw new Error(diagnosis.public.next_action);
160
103
  }
104
+ const draft = await loadProfileDraft(diagnosis.profile);
105
+ await assertMissionNotCovered({ draft, frontier: source });
106
+ await assertFreshOutput(outputRoot, source);
161
107
  const staging = path.join(outputRoot, ".profile-staging");
162
108
  await mkdir(staging, { mode: 0o700 });
163
109
  await stageProfileCapsule({
@@ -165,7 +111,6 @@ export async function runProduct(options) {
165
111
  stagingRoot: staging,
166
112
  });
167
113
  const bundleRoot = path.join(outputRoot, "mission");
168
- const draft = await loadProfileDraft(diagnosis.profile);
169
114
  const resultContract = await loadProfileResultContract(diagnosis.profile);
170
115
  const prepared = await prepareMission({
171
116
  draft: options.requestedTarget === undefined
@@ -202,6 +147,9 @@ export async function runProduct(options) {
202
147
  expectedVersion: prepared.mission.vela_version,
203
148
  expectedSha256: prepared.mission.vela_sha256,
204
149
  home: path.join(runRoot, "vela-home"),
150
+ ...(process.env.SSH_AUTH_SOCK === undefined
151
+ ? {}
152
+ : { repositoryAuthorityAgentSocket: process.env.SSH_AUTH_SOCK }),
205
153
  runner,
206
154
  });
207
155
  const engine = new CodexToolsNativeEngine({
@@ -220,33 +168,9 @@ export async function runProduct(options) {
220
168
  bundleRoot,
221
169
  dockerBinary: dockerRuntime.binary,
222
170
  verifierRunner: runner,
223
- retainWithdrawalCapability: async (context) => {
224
- await retainWithdrawalCapability({
225
- ...context,
226
- velaBinary: diagnosis.public.runtimes.vela.binary,
227
- });
228
- },
229
171
  };
230
- const run = options.noLand === true
231
- ? await runCanopus({ ...commonRun, noLand: true })
232
- : await runCanopus(commonRun);
172
+ const run = await runCanopus(commonRun);
233
173
  const evidence = await writeEvidenceManifest(run, contentDigest(prepared.mission));
234
- const publication = options.noLand === true
235
- ? {
236
- state: "unchanged_no_land",
237
- commit: prepared.mission.roots.git_commit,
238
- tree: prepared.mission.roots.git_tree,
239
- }
240
- : await publishVerifiedLanding({
241
- source,
242
- landing: run.paths.landing,
243
- expectedStart: prepared.mission.roots.git_commit,
244
- expectedFinal: run.record.final_roots.git_commit,
245
- expectedTree: run.record.final_roots.git_tree,
246
- home: controlHome,
247
- runner,
248
- });
249
- await rm(controlHome, { recursive: true, force: true });
250
174
  return {
251
175
  run,
252
176
  doctor: diagnosis.public,
@@ -254,7 +178,12 @@ export async function runProduct(options) {
254
178
  bundle_root: bundleRoot,
255
179
  evidence_manifest: evidence.file,
256
180
  evidence_root: evidence.root,
257
- source_publication: publication,
181
+ source_state: {
182
+ state: "unchanged",
183
+ commit: prepared.mission.roots.git_commit,
184
+ tree: prepared.mission.roots.git_tree,
185
+ },
186
+ submission: null,
258
187
  };
259
188
  }
260
189
  catch (error) {
@@ -0,0 +1,40 @@
1
+ export { verifySubmission, type IdentityBinding, type SubmissionV1, } from "@vela-science/protocol";
2
+ import { projectCurrentRun } from "../projection/current-run.js";
3
+ export interface SubmissionBundleManifest {
4
+ schema: "canopus.submission-bundle.v1";
5
+ run_id: string;
6
+ run_root: string;
7
+ source: {
8
+ git_commit: string;
9
+ git_tree: string;
10
+ vela_version: string;
11
+ vela_sha256: string;
12
+ };
13
+ producer: string;
14
+ submission_id: string;
15
+ submission_root: string;
16
+ identity_binding_id: string;
17
+ artifacts: Array<{
18
+ source: string;
19
+ frontier_path: string;
20
+ digest: string;
21
+ bytes: number;
22
+ }>;
23
+ }
24
+ export declare function exportSubmission(options: {
25
+ runFile: string;
26
+ outputRoot: string;
27
+ actor?: string;
28
+ correctedClaim?: string;
29
+ scopeLimit?: string;
30
+ now?: Date;
31
+ }): Promise<{
32
+ schema: "canopus.export-result.v1";
33
+ ok: true;
34
+ run_id: string;
35
+ submission_id: string;
36
+ submission_root: string;
37
+ bundle: string;
38
+ manifest: string;
39
+ }>;
40
+ export declare function projectExportableRun(value: unknown): ReturnType<typeof projectCurrentRun>;
@@ -0,0 +1,217 @@
1
+ import { generateKeyPairSync, sign, } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { chmod, copyFile, lstat, mkdir, realpath, rm, writeFile, } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { identityBindingPreimage, submissionPreimage, verifySubmission, } from "@vela-science/protocol";
6
+ export { verifySubmission, } from "@vela-science/protocol";
7
+ import { finalizeWorkerCaveat } from "../candidate/finalize.js";
8
+ import { parseCurrentRunRecord, projectCurrentRun } from "../projection/current-run.js";
9
+ import { parseRetainedMission } from "../projection/retained-mission.js";
10
+ import { parseRetainedRunRecord } from "../projection/retained-run.js";
11
+ import { canonicalJcs, canonicalJson, protocolDigest, sha256Bytes } from "../util/canonical.js";
12
+ import { readBoundedRegularFile } from "../util/files.js";
13
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
14
+ const STALE_VERIFIER_LANGUAGE = /verif(?:y|ication|ier).*(?:pending|has not run|not (?:been )?(?:performed|executed|run))|pending.*verif(?:y|ication|ier)/iu;
15
+ const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/u;
16
+ const CORRECTION_NOTICE = "The Submission wording corrects a stale post-run Claim after verifier passage; the immutable Run remains unchanged.";
17
+ function rawPublicKey(publicKey) {
18
+ const der = publicKey.export({ type: "spki", format: "der" });
19
+ if (!Buffer.isBuffer(der) || der.length !== ED25519_SPKI_PREFIX.length + 32) {
20
+ throw new Error("generated Ed25519 public key has an unexpected encoding");
21
+ }
22
+ if (!der.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
23
+ throw new Error("generated public key is not Ed25519");
24
+ }
25
+ return der.subarray(ED25519_SPKI_PREFIX.length);
26
+ }
27
+ async function assertFreshDirectory(directory) {
28
+ try {
29
+ await lstat(directory);
30
+ throw new Error(`output already exists: ${directory}`);
31
+ }
32
+ catch (error) {
33
+ if (error.code !== "ENOENT")
34
+ throw error;
35
+ }
36
+ await mkdir(directory, { recursive: true, mode: 0o700 });
37
+ }
38
+ function missionFileFor(runFile) {
39
+ const runRoot = path.dirname(runFile);
40
+ return path.join(runRoot, "..", "mission", "mission.json");
41
+ }
42
+ export async function exportSubmission(options) {
43
+ const runFile = await realpath(options.runFile);
44
+ const runRoot = path.dirname(runFile);
45
+ const retainedRun = parseRetainedRunRecord(JSON.parse((await readBoundedRegularFile(runFile, 8 * 1024 * 1024)).toString("utf8")));
46
+ const retainedMission = parseRetainedMission(JSON.parse((await readBoundedRegularFile(missionFileFor(runFile), 8 * 1024 * 1024)).toString("utf8")));
47
+ const record = retainedRun.record;
48
+ const mission = retainedMission.mission;
49
+ if (record.mission.digest !== retainedMission.exactRoot) {
50
+ throw new Error("run and mission roots disagree");
51
+ }
52
+ if (retainedRun.exactStartingRoots !== retainedMission.exactRoots) {
53
+ throw new Error("run and mission exact starting roots disagree");
54
+ }
55
+ if (record.candidate.status !== "success" || record.verifier.status !== "passed") {
56
+ throw new Error("only a successful, verifier-passing Run can export a Submission");
57
+ }
58
+ if (record.candidate.artifacts.length === 0 || record.candidate.caveats.length === 0) {
59
+ throw new Error("Submission export requires at least one Artifact and one caveat");
60
+ }
61
+ if ((options.correctedClaim === undefined) !== (options.scopeLimit === undefined)) {
62
+ throw new Error("Submission correction requires both a corrected Claim and a scope limit");
63
+ }
64
+ const retainedClaimNeedsCorrection = STALE_VERIFIER_LANGUAGE.test(record.candidate.claim) ||
65
+ CONTROL_CHARACTER.test(record.candidate.claim);
66
+ if (retainedClaimNeedsCorrection && options.correctedClaim === undefined) {
67
+ throw new Error("Run Claim is stale after verifier passage or contains control bytes; export requires --claim and --scope-limit to author one corrected bounded Submission without changing the Run");
68
+ }
69
+ if (!retainedClaimNeedsCorrection && options.correctedClaim !== undefined) {
70
+ throw new Error("Submission correction is allowed only for a retained Run Claim that is stale or contains control bytes");
71
+ }
72
+ const assertion = options.correctedClaim ?? record.candidate.claim;
73
+ if (CONTROL_CHARACTER.test(assertion)) {
74
+ throw new Error("Submission Claim contains a control character");
75
+ }
76
+ if (STALE_VERIFIER_LANGUAGE.test(assertion)) {
77
+ throw new Error("Submission Claim contradicts the retained passing verifier outcome");
78
+ }
79
+ if (options.scopeLimit !== undefined && CONTROL_CHARACTER.test(options.scopeLimit)) {
80
+ throw new Error("Submission scope limit contains a control character");
81
+ }
82
+ const caveats = [...new Set([
83
+ ...record.candidate.caveats.map(finalizeWorkerCaveat),
84
+ ...(options.scopeLimit === undefined ? [] : [options.scopeLimit, CORRECTION_NOTICE]),
85
+ ])];
86
+ const actor = options.actor ?? mission.actor;
87
+ if (!actor.startsWith("agent:")) {
88
+ throw new Error("Canopus Submission export requires an agent: producer");
89
+ }
90
+ const outputRoot = path.resolve(options.outputRoot);
91
+ await assertFreshDirectory(outputRoot);
92
+ try {
93
+ const emittedAt = (options.now ?? new Date()).toISOString();
94
+ const keys = generateKeyPairSync("ed25519");
95
+ const publicKey = rawPublicKey(keys.publicKey);
96
+ let binding = {
97
+ schema: "vela.identity_binding.v0.1",
98
+ binding_id: "",
99
+ actor_id: actor,
100
+ actor_class: "agent",
101
+ public_key_hex: publicKey.toString("hex"),
102
+ created_at: emittedAt,
103
+ signature: "",
104
+ };
105
+ const bindingBytes = identityBindingPreimage(binding);
106
+ binding = {
107
+ ...binding,
108
+ binding_id: `vib_${sha256Bytes(bindingBytes).slice(7, 23)}`,
109
+ signature: sign(null, Buffer.from(bindingBytes), keys.privateKey).toString("hex"),
110
+ };
111
+ const artifactDirectory = path.join(outputRoot, "artifacts", "sha256");
112
+ await mkdir(artifactDirectory, { recursive: true, mode: 0o700 });
113
+ const manifestArtifacts = [];
114
+ const submissionArtifacts = [];
115
+ for (const artifact of record.candidate.artifacts) {
116
+ const hex = artifact.digest.slice("sha256:".length);
117
+ const source = path.join(runRoot, "artifacts", hex);
118
+ const observed = sha256Bytes(await readBoundedRegularFile(source, artifact.bytes + 1));
119
+ if (observed !== artifact.digest) {
120
+ throw new Error(`frozen Artifact ${artifact.digest} no longer matches the Run`);
121
+ }
122
+ const target = path.join(artifactDirectory, hex);
123
+ await copyFile(source, target, constants.COPYFILE_EXCL);
124
+ await chmod(target, 0o444);
125
+ const frontierPath = `records/artifacts/sha256/${hex}`;
126
+ manifestArtifacts.push({
127
+ source: `artifacts/sha256/${hex}`,
128
+ frontier_path: frontierPath,
129
+ digest: artifact.digest,
130
+ bytes: artifact.bytes,
131
+ });
132
+ submissionArtifacts.push({
133
+ kind: artifact.kind,
134
+ path: frontierPath,
135
+ digest: artifact.digest,
136
+ });
137
+ }
138
+ let submission = {
139
+ schema: "vela.submission.v1",
140
+ submission_id: "",
141
+ claim: {
142
+ assertion,
143
+ type: mission.claim_type,
144
+ conditions: [mission.completion_condition],
145
+ },
146
+ artifacts: submissionArtifacts,
147
+ caveats,
148
+ replayability: mission.replayability,
149
+ producer_checks: [],
150
+ verification_requirements: [
151
+ `Replay Canopus Run ${record.run_id} with verifier capsule ${mission.verifier.capsule_sha256}.`,
152
+ ],
153
+ requested_change: { kind: "add_claim" },
154
+ provenance: {
155
+ producer: actor,
156
+ source_system: "canopus",
157
+ source_run: record.run_id,
158
+ emitted_at: emittedAt,
159
+ },
160
+ ...(mission.execution_binding === undefined
161
+ ? {}
162
+ : { execution_binding: mission.execution_binding }),
163
+ authentication: {
164
+ algorithm: "ed25519",
165
+ identity_binding: binding,
166
+ signature: "",
167
+ },
168
+ };
169
+ const submissionBytes = submissionPreimage(submission);
170
+ submission = {
171
+ ...submission,
172
+ submission_id: `vsb_${sha256Bytes(submissionBytes).slice(7, 23)}`,
173
+ authentication: {
174
+ ...submission.authentication,
175
+ signature: sign(null, Buffer.from(submissionBytes), keys.privateKey).toString("hex"),
176
+ },
177
+ };
178
+ verifySubmission(submission);
179
+ const submissionRoot = protocolDigest(submission);
180
+ const submissionFile = path.join(outputRoot, "submission.json");
181
+ await writeFile(submissionFile, `${canonicalJcs(submission)}\n`, { flag: "wx", mode: 0o444 });
182
+ const manifest = {
183
+ schema: "canopus.submission-bundle.v1",
184
+ run_id: record.run_id,
185
+ run_root: retainedRun.exactRoot,
186
+ source: {
187
+ git_commit: mission.roots.git_commit,
188
+ git_tree: mission.roots.git_tree,
189
+ vela_version: mission.vela_version,
190
+ vela_sha256: mission.vela_sha256,
191
+ },
192
+ producer: actor,
193
+ submission_id: submission.submission_id,
194
+ submission_root: submissionRoot,
195
+ identity_binding_id: binding.binding_id,
196
+ artifacts: manifestArtifacts,
197
+ };
198
+ const manifestFile = path.join(outputRoot, "manifest.json");
199
+ await writeFile(manifestFile, canonicalJson(manifest), { flag: "wx", mode: 0o444 });
200
+ return {
201
+ schema: "canopus.export-result.v1",
202
+ ok: true,
203
+ run_id: record.run_id,
204
+ submission_id: submission.submission_id,
205
+ submission_root: submissionRoot,
206
+ bundle: outputRoot,
207
+ manifest: manifestFile,
208
+ };
209
+ }
210
+ catch (error) {
211
+ await rm(outputRoot, { recursive: true, force: true });
212
+ throw error;
213
+ }
214
+ }
215
+ export function projectExportableRun(value) {
216
+ return projectCurrentRun(parseCurrentRunRecord(value));
217
+ }
@@ -0,0 +1,22 @@
1
+ import { type CommandRunner } from "../util/command.js";
2
+ export declare function submitBundle(options: {
3
+ bundle: string;
4
+ frontier: string;
5
+ velaBinary?: string;
6
+ runner?: CommandRunner;
7
+ }): Promise<{
8
+ schema: "canopus.submit-result.v1";
9
+ ok: true;
10
+ submission_id: string;
11
+ submission_root: string;
12
+ registration_record_id: string;
13
+ registration_record_root: string;
14
+ proposal_id: string;
15
+ claim_id: string;
16
+ route: "pending_review";
17
+ accepted_event_delta: 0;
18
+ source_commit_before: string;
19
+ source_commit_after: string;
20
+ registration_binary_version: string;
21
+ registration_binary_sha256: string;
22
+ }>;