@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.
- package/CHANGELOG.md +80 -0
- package/README.md +67 -145
- package/capsules/erdos1056-k15/bin/linux-arm64/{10428801-10429000 → 10429201-10429400}/verifier +0 -0
- package/capsules/erdos1056-k15/bin/linux-x86_64/{10428801-10429000 → 10429201-10429400}/verifier +0 -0
- package/compatibility.json +25 -0
- package/dist/src/{receipt/map.d.ts → candidate/finalize.d.ts} +2 -9
- package/dist/src/candidate/finalize.js +71 -0
- package/dist/src/cli.js +89 -195
- package/dist/src/contracts/mission.d.ts +1 -13
- package/dist/src/contracts/mission.js +4 -42
- package/dist/src/contracts/validation.d.ts +1 -25
- package/dist/src/contracts/validation.js +1 -95
- package/dist/src/engines/codex-tools-native.js +2 -2
- package/dist/src/index.d.ts +5 -34
- package/dist/src/index.js +4 -34
- package/dist/src/mission/prepare.d.ts +10 -1
- package/dist/src/mission/prepare.js +29 -30
- package/dist/src/product/coverage.d.ts +17 -0
- package/dist/src/product/coverage.js +137 -0
- package/dist/src/product/doctor.d.ts +1 -2
- package/dist/src/product/doctor.js +29 -8
- package/dist/src/product/profile.js +8 -2
- package/dist/src/product/replay.js +11 -12
- package/dist/src/product/run.d.ts +5 -9
- package/dist/src/product/run.js +17 -81
- package/dist/src/product/submission.d.ts +40 -0
- package/dist/src/product/submission.js +217 -0
- package/dist/src/product/submit.d.ts +22 -0
- package/dist/src/product/submit.js +264 -0
- package/dist/src/product/version.d.ts +2 -2
- package/dist/src/product/version.js +27 -4
- package/dist/src/projection/current-run.d.ts +12 -0
- package/dist/src/projection/current-run.js +94 -0
- package/dist/src/projection/failure.d.ts +17 -7
- package/dist/src/projection/failure.js +43 -12
- package/dist/src/projection/retained-mission.d.ts +6 -0
- package/dist/src/projection/retained-mission.js +41 -0
- package/dist/src/projection/retained-run.d.ts +7 -0
- package/dist/src/projection/retained-run.js +50 -0
- package/dist/src/run.d.ts +43 -34
- package/dist/src/run.js +34 -397
- package/dist/src/util/canonical.d.ts +1 -5
- package/dist/src/util/canonical.js +1 -53
- package/dist/src/vela/cli.d.ts +7 -42
- package/dist/src/vela/cli.js +102 -475
- package/dist/src/vela/types.d.ts +3 -19
- package/dist/src/verifier/run.js +1 -1
- package/dist/src/workspace/prepare.d.ts +1 -1
- package/dist/src/workspace/prepare.js +15 -15
- package/docs/EVALUATION.md +142 -0
- package/docs/MISSIONS.md +42 -80
- package/docs/RUN_RECORD.md +77 -99
- package/docs/adr/0010-nonmutating-runs-and-explicit-submission.md +191 -0
- package/missions/erdos1056-k15-next/mission.draft.json +10 -8
- package/package.json +18 -15
- package/profiles/{erdos1056-k15-10428801-10429000.json → erdos1056-k15-10429201-10429400.json} +10 -10
- package/schemas/failure.v1.json +17 -0
- package/schemas/mission.v0.json +3 -4
- package/schemas/mission.v1.json +7 -28
- package/schemas/{run.v1.json → run.v2.json} +58 -116
- package/schemas/submission-bundle.v1.json +63 -0
- package/toolchain.lock.json +31 -0
- package/capsules/formal-erdos-505-test-dim-one/verifier +0 -105
- package/capsules/quantum-10-1-4/verifier.py +0 -152
- package/capsules/sidon-a24/bin/linux-arm64/verifier +0 -0
- package/capsules/sidon-a24/bin/linux-x86_64/verifier +0 -0
- package/dist/src/engines/codex-exec.d.ts +0 -17
- package/dist/src/engines/codex-exec.js +0 -136
- package/dist/src/engines/codex-tool-free.d.ts +0 -26
- package/dist/src/engines/codex-tool-free.js +0 -213
- package/dist/src/projection/diagnostic.d.ts +0 -12
- package/dist/src/projection/diagnostic.js +0 -61
- package/dist/src/projection/public-run.d.ts +0 -59
- package/dist/src/projection/public-run.js +0 -106
- package/dist/src/projection/publication.d.ts +0 -36
- package/dist/src/projection/publication.js +0 -47
- package/dist/src/projection/run.d.ts +0 -95
- package/dist/src/projection/run.js +0 -173
- package/dist/src/receipt/map.js +0 -168
- package/dist/src/repair/plan.d.ts +0 -2
- package/dist/src/repair/plan.js +0 -22
- package/dist/src/roles.d.ts +0 -2
- package/dist/src/roles.js +0 -9
- package/missions/formal-erdos-505-test-dim-one-gpt56/mission.draft.json +0 -40
- package/missions/quantum-10-1-4-retry/mission.draft.json +0 -40
- package/missions/sidon-a24-at-least-7194-gpt56/mission.draft.json +0 -42
- package/missions/sidon-a24-at-least-7194-gpt56-v2/mission.draft.json +0 -42
- package/missions/sidon-a24-at-least-7194-gpt56-v3/mission.draft.json +0 -42
- package/profiles/formal-erdos-505-test-dim-one-gpt56.json +0 -35
- package/profiles/quantum-10-1-4-stabilizer-retry.json +0 -35
- package/profiles/sidon-a24-at-least-7194-gpt56-v2.json +0 -35
- package/profiles/sidon-a24-at-least-7194-gpt56-v3.json +0 -35
- package/profiles/sidon-a24-at-least-7194-gpt56.json +0 -35
- package/schemas/canopus.public-run.v1.json +0 -116
|
@@ -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 {
|
|
8
|
-
import {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
12
|
-
state: "
|
|
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>;
|
package/dist/src/product/run.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
118
|
-
final_roots:
|
|
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 =
|
|
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
|
-
|
|
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
|
+
}>;
|