@vela-science/canopus 0.7.0 → 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 (94) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +67 -145
  3. package/capsules/erdos1056-k15/bin/linux-arm64/{10428801-10429000 → 10429201-10429400}/verifier +0 -0
  4. package/capsules/erdos1056-k15/bin/linux-x86_64/{10428801-10429000 → 10429201-10429400}/verifier +0 -0
  5. package/compatibility.json +25 -0
  6. package/dist/src/{receipt/map.d.ts → candidate/finalize.d.ts} +2 -9
  7. package/dist/src/candidate/finalize.js +71 -0
  8. package/dist/src/cli.js +89 -195
  9. package/dist/src/contracts/mission.d.ts +1 -13
  10. package/dist/src/contracts/mission.js +4 -42
  11. package/dist/src/contracts/validation.d.ts +1 -25
  12. package/dist/src/contracts/validation.js +1 -95
  13. package/dist/src/engines/codex-tools-native.js +2 -2
  14. package/dist/src/index.d.ts +5 -34
  15. package/dist/src/index.js +4 -34
  16. package/dist/src/mission/prepare.d.ts +10 -1
  17. package/dist/src/mission/prepare.js +29 -30
  18. package/dist/src/product/coverage.d.ts +17 -0
  19. package/dist/src/product/coverage.js +137 -0
  20. package/dist/src/product/doctor.d.ts +1 -2
  21. package/dist/src/product/doctor.js +29 -8
  22. package/dist/src/product/profile.js +8 -2
  23. package/dist/src/product/replay.js +11 -12
  24. package/dist/src/product/run.d.ts +5 -9
  25. package/dist/src/product/run.js +17 -81
  26. package/dist/src/product/submission.d.ts +40 -0
  27. package/dist/src/product/submission.js +217 -0
  28. package/dist/src/product/submit.d.ts +22 -0
  29. package/dist/src/product/submit.js +264 -0
  30. package/dist/src/product/version.d.ts +2 -2
  31. package/dist/src/product/version.js +27 -4
  32. package/dist/src/projection/current-run.d.ts +12 -0
  33. package/dist/src/projection/current-run.js +94 -0
  34. package/dist/src/projection/failure.d.ts +17 -7
  35. package/dist/src/projection/failure.js +43 -12
  36. package/dist/src/projection/retained-mission.d.ts +6 -0
  37. package/dist/src/projection/retained-mission.js +41 -0
  38. package/dist/src/projection/retained-run.d.ts +7 -0
  39. package/dist/src/projection/retained-run.js +50 -0
  40. package/dist/src/run.d.ts +43 -34
  41. package/dist/src/run.js +34 -397
  42. package/dist/src/util/canonical.d.ts +1 -5
  43. package/dist/src/util/canonical.js +1 -53
  44. package/dist/src/vela/cli.d.ts +7 -42
  45. package/dist/src/vela/cli.js +102 -475
  46. package/dist/src/vela/types.d.ts +3 -19
  47. package/dist/src/verifier/run.js +1 -1
  48. package/dist/src/workspace/prepare.d.ts +1 -1
  49. package/dist/src/workspace/prepare.js +15 -15
  50. package/docs/EVALUATION.md +142 -0
  51. package/docs/MISSIONS.md +42 -80
  52. package/docs/RUN_RECORD.md +77 -99
  53. package/docs/adr/0010-nonmutating-runs-and-explicit-submission.md +191 -0
  54. package/missions/erdos1056-k15-next/mission.draft.json +10 -8
  55. package/package.json +18 -15
  56. package/profiles/{erdos1056-k15-10428801-10429000.json → erdos1056-k15-10429201-10429400.json} +10 -10
  57. package/schemas/failure.v1.json +17 -0
  58. package/schemas/mission.v0.json +3 -4
  59. package/schemas/mission.v1.json +7 -28
  60. package/schemas/{run.v1.json → run.v2.json} +58 -116
  61. package/schemas/submission-bundle.v1.json +63 -0
  62. package/toolchain.lock.json +31 -0
  63. package/capsules/formal-erdos-505-test-dim-one/verifier +0 -105
  64. package/capsules/quantum-10-1-4/verifier.py +0 -152
  65. package/capsules/sidon-a24/bin/linux-arm64/verifier +0 -0
  66. package/capsules/sidon-a24/bin/linux-x86_64/verifier +0 -0
  67. package/dist/src/engines/codex-exec.d.ts +0 -17
  68. package/dist/src/engines/codex-exec.js +0 -136
  69. package/dist/src/engines/codex-tool-free.d.ts +0 -26
  70. package/dist/src/engines/codex-tool-free.js +0 -213
  71. package/dist/src/projection/diagnostic.d.ts +0 -12
  72. package/dist/src/projection/diagnostic.js +0 -61
  73. package/dist/src/projection/public-run.d.ts +0 -59
  74. package/dist/src/projection/public-run.js +0 -106
  75. package/dist/src/projection/publication.d.ts +0 -36
  76. package/dist/src/projection/publication.js +0 -47
  77. package/dist/src/projection/run.d.ts +0 -95
  78. package/dist/src/projection/run.js +0 -173
  79. package/dist/src/receipt/map.js +0 -168
  80. package/dist/src/repair/plan.d.ts +0 -2
  81. package/dist/src/repair/plan.js +0 -22
  82. package/dist/src/roles.d.ts +0 -2
  83. package/dist/src/roles.js +0 -9
  84. package/missions/formal-erdos-505-test-dim-one-gpt56/mission.draft.json +0 -40
  85. package/missions/quantum-10-1-4-retry/mission.draft.json +0 -40
  86. package/missions/sidon-a24-at-least-7194-gpt56/mission.draft.json +0 -42
  87. package/missions/sidon-a24-at-least-7194-gpt56-v2/mission.draft.json +0 -42
  88. package/missions/sidon-a24-at-least-7194-gpt56-v3/mission.draft.json +0 -42
  89. package/profiles/formal-erdos-505-test-dim-one-gpt56.json +0 -35
  90. package/profiles/quantum-10-1-4-stabilizer-retry.json +0 -35
  91. package/profiles/sidon-a24-at-least-7194-gpt56-v2.json +0 -35
  92. package/profiles/sidon-a24-at-least-7194-gpt56-v3.json +0 -35
  93. package/profiles/sidon-a24-at-least-7194-gpt56.json +0 -35
  94. package/schemas/canopus.public-run.v1.json +0 -116
@@ -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,10 +6,11 @@ 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
15
  function packageFile(relative) {
15
16
  return fileURLToPath(new URL(`../../../${relative}`, import.meta.url));
@@ -38,58 +39,6 @@ async function assertFreshOutput(outputRoot, sourceRoot) {
38
39
  }
39
40
  await mkdir(output, { recursive: true, mode: 0o700 });
40
41
  }
41
- async function gitText(options) {
42
- const result = await options.runner({
43
- argv: ["git", ...options.argv],
44
- cwd: options.cwd,
45
- env: isolatedEnvironment(options.home),
46
- timeoutMs: 120_000,
47
- maxOutputBytes: 8 * 1024 * 1024,
48
- });
49
- if (result.exitCode !== 0 || result.stderr.length !== 0) {
50
- throw new Error(`git ${options.argv.join(" ")} failed: stdout_sha256=${sha256Bytes(result.stdout)}; ` +
51
- `stderr_sha256=${sha256Bytes(result.stderr)}`);
52
- }
53
- return result.stdout.toString("utf8").trim();
54
- }
55
- async function publishVerifiedLanding(options) {
56
- const [head, status] = await Promise.all([
57
- gitText({ argv: ["rev-parse", "--verify", "HEAD^{commit}"], cwd: options.source, home: options.home, runner: options.runner }),
58
- gitText({ argv: ["status", "--porcelain=v1", "--untracked-files=all"], cwd: options.source, home: options.home, runner: options.runner }),
59
- ]);
60
- if (head !== options.expectedStart || status !== "") {
61
- throw new Error("source frontier changed while the bounded run was executing");
62
- }
63
- const landingHead = await gitText({
64
- argv: ["rev-parse", "--verify", "canopus-landing^{commit}"],
65
- cwd: options.landing,
66
- home: options.home,
67
- runner: options.runner,
68
- });
69
- if (landingHead !== options.expectedFinal)
70
- throw new Error("landing clone final commit drifted");
71
- await gitText({
72
- argv: ["fetch", "--quiet", "--no-tags", options.landing, "canopus-landing"],
73
- cwd: options.source,
74
- home: options.home,
75
- runner: options.runner,
76
- });
77
- await gitText({
78
- argv: ["merge", "--ff-only", "--no-edit", "FETCH_HEAD"],
79
- cwd: options.source,
80
- home: options.home,
81
- runner: options.runner,
82
- });
83
- const [commit, tree, finalStatus] = await Promise.all([
84
- gitText({ argv: ["rev-parse", "--verify", "HEAD^{commit}"], cwd: options.source, home: options.home, runner: options.runner }),
85
- gitText({ argv: ["rev-parse", "--verify", "HEAD^{tree}"], cwd: options.source, home: options.home, runner: options.runner }),
86
- gitText({ argv: ["status", "--porcelain=v1", "--untracked-files=all"], cwd: options.source, home: options.home, runner: options.runner }),
87
- ]);
88
- if (commit !== options.expectedFinal || tree !== options.expectedTree || finalStatus !== "") {
89
- throw new Error("source publication did not reproduce the verified landing roots");
90
- }
91
- return { state: "committed_local", commit, tree };
92
- }
93
42
  async function writeEvidenceManifest(run, missionDigest) {
94
43
  const root = run.paths.root;
95
44
  const files = {
@@ -114,10 +63,8 @@ async function writeEvidenceManifest(run, missionDigest) {
114
63
  files: digests,
115
64
  artifact_roots: run.record.candidate.artifacts.map((artifact) => artifact.digest).sort(),
116
65
  verifier_root: contentDigest(run.record.verifier),
117
- receipt_root: run.record.landing?.receipt_root ?? null,
118
- final_roots: "final_roots" in run.record
119
- ? run.record.final_roots
120
- : run.record.mission.starting_roots,
66
+ submission_root: null,
67
+ final_roots: run.record.mission.starting_roots,
121
68
  };
122
69
  const file = path.join(root, "evidence-manifest.json");
123
70
  await writeFile(file, canonicalJson(manifest), { flag: "wx", mode: 0o600 });
@@ -142,9 +89,6 @@ export async function runProduct(options) {
142
89
  const runner = options.runner ?? runCommand;
143
90
  const source = await realpath(options.frontier);
144
91
  const outputRoot = path.resolve(options.outputRoot ?? defaultProductOutput(source));
145
- await assertFreshOutput(outputRoot, source);
146
- const controlHome = path.join(outputRoot, "control-home");
147
- await mkdir(controlHome, { mode: 0o700 });
148
92
  try {
149
93
  const diagnosis = await doctorProduct({
150
94
  frontier: source,
@@ -157,6 +101,9 @@ export async function runProduct(options) {
157
101
  if (!diagnosis.public.worker.mission_ready || codexRuntime === null || dockerRuntime === null) {
158
102
  throw new Error(diagnosis.public.next_action);
159
103
  }
104
+ const draft = await loadProfileDraft(diagnosis.profile);
105
+ await assertMissionNotCovered({ draft, frontier: source });
106
+ await assertFreshOutput(outputRoot, source);
160
107
  const staging = path.join(outputRoot, ".profile-staging");
161
108
  await mkdir(staging, { mode: 0o700 });
162
109
  await stageProfileCapsule({
@@ -164,7 +111,6 @@ export async function runProduct(options) {
164
111
  stagingRoot: staging,
165
112
  });
166
113
  const bundleRoot = path.join(outputRoot, "mission");
167
- const draft = await loadProfileDraft(diagnosis.profile);
168
114
  const resultContract = await loadProfileResultContract(diagnosis.profile);
169
115
  const prepared = await prepareMission({
170
116
  draft: options.requestedTarget === undefined
@@ -201,6 +147,9 @@ export async function runProduct(options) {
201
147
  expectedVersion: prepared.mission.vela_version,
202
148
  expectedSha256: prepared.mission.vela_sha256,
203
149
  home: path.join(runRoot, "vela-home"),
150
+ ...(process.env.SSH_AUTH_SOCK === undefined
151
+ ? {}
152
+ : { repositoryAuthorityAgentSocket: process.env.SSH_AUTH_SOCK }),
204
153
  runner,
205
154
  });
206
155
  const engine = new CodexToolsNativeEngine({
@@ -220,26 +169,8 @@ export async function runProduct(options) {
220
169
  dockerBinary: dockerRuntime.binary,
221
170
  verifierRunner: runner,
222
171
  };
223
- const run = options.noLand === true
224
- ? await runCanopus({ ...commonRun, noLand: true })
225
- : await runCanopus(commonRun);
172
+ const run = await runCanopus(commonRun);
226
173
  const evidence = await writeEvidenceManifest(run, contentDigest(prepared.mission));
227
- const publication = options.noLand === true
228
- ? {
229
- state: "unchanged_no_land",
230
- commit: prepared.mission.roots.git_commit,
231
- tree: prepared.mission.roots.git_tree,
232
- }
233
- : await publishVerifiedLanding({
234
- source,
235
- landing: run.paths.landing,
236
- expectedStart: prepared.mission.roots.git_commit,
237
- expectedFinal: run.record.final_roots.git_commit,
238
- expectedTree: run.record.final_roots.git_tree,
239
- home: controlHome,
240
- runner,
241
- });
242
- await rm(controlHome, { recursive: true, force: true });
243
174
  return {
244
175
  run,
245
176
  doctor: diagnosis.public,
@@ -247,7 +178,12 @@ export async function runProduct(options) {
247
178
  bundle_root: bundleRoot,
248
179
  evidence_manifest: evidence.file,
249
180
  evidence_root: evidence.root,
250
- 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,
251
187
  };
252
188
  }
253
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
+ }>;