@vela-science/canopus 0.4.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +249 -0
  2. package/LICENSE-APACHE +176 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +124 -0
  5. package/SECURITY.md +35 -0
  6. package/capsules/erdos1056-k15/README.md +35 -0
  7. package/capsules/erdos1056-k15/bin/linux-arm64/10428401-10428600/verifier +0 -0
  8. package/capsules/erdos1056-k15/bin/linux-x86_64/10428401-10428600/verifier +0 -0
  9. package/capsules/formal-erdos-505-test-dim-one/README.md +20 -0
  10. package/capsules/formal-erdos-505-test-dim-one/verifier +105 -0
  11. package/dist/src/activity/events.d.ts +15 -0
  12. package/dist/src/activity/events.js +24 -0
  13. package/dist/src/activity/store.d.ts +10 -0
  14. package/dist/src/activity/store.js +165 -0
  15. package/dist/src/artifact/freeze.d.ts +21 -0
  16. package/dist/src/artifact/freeze.js +177 -0
  17. package/dist/src/artifact/materialize.d.ts +6 -0
  18. package/dist/src/artifact/materialize.js +29 -0
  19. package/dist/src/budget/enforce.d.ts +30 -0
  20. package/dist/src/budget/enforce.js +98 -0
  21. package/dist/src/candidate/validate.d.ts +3 -0
  22. package/dist/src/candidate/validate.js +32 -0
  23. package/dist/src/capability/withdrawal.d.ts +47 -0
  24. package/dist/src/capability/withdrawal.js +487 -0
  25. package/dist/src/cli.d.ts +2 -0
  26. package/dist/src/cli.js +503 -0
  27. package/dist/src/contracts/candidate.d.ts +50 -0
  28. package/dist/src/contracts/candidate.js +93 -0
  29. package/dist/src/contracts/mission.d.ts +139 -0
  30. package/dist/src/contracts/mission.js +401 -0
  31. package/dist/src/contracts/validation.d.ts +25 -0
  32. package/dist/src/contracts/validation.js +95 -0
  33. package/dist/src/engines/codex-events.d.ts +25 -0
  34. package/dist/src/engines/codex-events.js +219 -0
  35. package/dist/src/engines/codex-exec.d.ts +17 -0
  36. package/dist/src/engines/codex-exec.js +136 -0
  37. package/dist/src/engines/codex-home.d.ts +7 -0
  38. package/dist/src/engines/codex-home.js +50 -0
  39. package/dist/src/engines/codex-tool-free.d.ts +26 -0
  40. package/dist/src/engines/codex-tool-free.js +213 -0
  41. package/dist/src/engines/codex-tools-native.d.ts +30 -0
  42. package/dist/src/engines/codex-tools-native.js +405 -0
  43. package/dist/src/engines/engine.d.ts +55 -0
  44. package/dist/src/engines/engine.js +4 -0
  45. package/dist/src/engines/fake.d.ts +7 -0
  46. package/dist/src/engines/fake.js +37 -0
  47. package/dist/src/evidence/manifests.d.ts +9 -0
  48. package/dist/src/evidence/manifests.js +56 -0
  49. package/dist/src/index.d.ts +32 -0
  50. package/dist/src/index.js +32 -0
  51. package/dist/src/mission/prepare.d.ts +33 -0
  52. package/dist/src/mission/prepare.js +375 -0
  53. package/dist/src/product/custody.d.ts +27 -0
  54. package/dist/src/product/custody.js +131 -0
  55. package/dist/src/product/doctor.d.ts +61 -0
  56. package/dist/src/product/doctor.js +278 -0
  57. package/dist/src/product/profile-bundle.d.ts +21 -0
  58. package/dist/src/product/profile-bundle.js +78 -0
  59. package/dist/src/product/profile.d.ts +67 -0
  60. package/dist/src/product/profile.js +251 -0
  61. package/dist/src/product/replay.d.ts +10 -0
  62. package/dist/src/product/replay.js +71 -0
  63. package/dist/src/product/run.d.ts +31 -0
  64. package/dist/src/product/run.js +264 -0
  65. package/dist/src/product/runtime.d.ts +18 -0
  66. package/dist/src/product/runtime.js +64 -0
  67. package/dist/src/product/version.d.ts +1 -0
  68. package/dist/src/product/version.js +6 -0
  69. package/dist/src/product/withdraw.d.ts +8 -0
  70. package/dist/src/product/withdraw.js +255 -0
  71. package/dist/src/projection/diagnostic.d.ts +12 -0
  72. package/dist/src/projection/diagnostic.js +61 -0
  73. package/dist/src/projection/failure.d.ts +26 -0
  74. package/dist/src/projection/failure.js +63 -0
  75. package/dist/src/projection/run.d.ts +89 -0
  76. package/dist/src/projection/run.js +159 -0
  77. package/dist/src/receipt/map.d.ts +25 -0
  78. package/dist/src/receipt/map.js +162 -0
  79. package/dist/src/repair/plan.d.ts +2 -0
  80. package/dist/src/repair/plan.js +22 -0
  81. package/dist/src/roles.d.ts +2 -0
  82. package/dist/src/roles.js +9 -0
  83. package/dist/src/run.d.ts +94 -0
  84. package/dist/src/run.js +654 -0
  85. package/dist/src/util/canonical.d.ts +5 -0
  86. package/dist/src/util/canonical.js +53 -0
  87. package/dist/src/util/command.d.ts +26 -0
  88. package/dist/src/util/command.js +161 -0
  89. package/dist/src/util/files.d.ts +3 -0
  90. package/dist/src/util/files.js +65 -0
  91. package/dist/src/vela/cli.d.ts +71 -0
  92. package/dist/src/vela/cli.js +718 -0
  93. package/dist/src/vela/types.d.ts +27 -0
  94. package/dist/src/vela/types.js +1 -0
  95. package/dist/src/verifier/run.d.ts +24 -0
  96. package/dist/src/verifier/run.js +315 -0
  97. package/dist/src/workspace/prepare.d.ts +30 -0
  98. package/dist/src/workspace/prepare.js +183 -0
  99. package/docs/MISSIONS.md +78 -0
  100. package/docs/RELEASES.md +1036 -0
  101. package/docs/RUN_RECORD.md +70 -0
  102. package/missions/erdos1056-k15-next/mission.draft.json +40 -0
  103. package/missions/formal-erdos-505-test-dim-one/mission.draft.json +40 -0
  104. package/package.json +89 -0
  105. package/profiles/erdos1056-k15-10428401-10428600.json +33 -0
  106. package/profiles/formal-erdos-505-test-dim-one.json +35 -0
  107. package/runtime/native-worker/config-linux.toml +20 -0
  108. package/runtime/native-worker/config.toml +17 -0
  109. package/schemas/candidate.v0.json +125 -0
  110. package/schemas/engine-manifest.v0.json +45 -0
  111. package/schemas/engine-output.v0.json +41 -0
  112. package/schemas/mission.v0.json +175 -0
  113. package/schemas/mission.v1.json +224 -0
  114. package/schemas/verifier-manifest.v0.json +48 -0
  115. package/schemas/verifier-manifest.v1.json +59 -0
  116. package/scripts/run-hostile-native-custody-fixture.mjs +382 -0
  117. package/scripts/run-hostile-verifier-fixture.mjs +63 -0
  118. package/tests/fixtures/hostile-verifier/probe.py +35 -0
@@ -0,0 +1,55 @@
1
+ import type { CandidateStatus } from "../contracts/candidate.js";
2
+ import type { Mission } from "../contracts/mission.js";
3
+ import type { BudgetTracker } from "../budget/enforce.js";
4
+ import type { WorkspacePaths } from "../workspace/prepare.js";
5
+ export declare const ENGINE_OUTPUT_SCHEMA: "canopus.engine-output.v0";
6
+ export declare const MAX_ENGINE_ARTIFACTS = 8;
7
+ export declare const MAX_ENGINE_OBSERVATIONS = 16;
8
+ export declare const MAX_ENGINE_CAVEATS = 8;
9
+ export interface DraftArtifact {
10
+ path: string;
11
+ kind: string;
12
+ encoding: "utf8";
13
+ content: string;
14
+ }
15
+ export interface CandidateDraft {
16
+ schema: typeof ENGINE_OUTPUT_SCHEMA;
17
+ status: CandidateStatus;
18
+ claim: string;
19
+ artifacts: DraftArtifact[];
20
+ observations: string[];
21
+ caveats: string[];
22
+ }
23
+ export interface EngineUsage {
24
+ input_tokens: number;
25
+ cached_input_tokens: number;
26
+ output_tokens: number;
27
+ reasoning_output_tokens: number;
28
+ }
29
+ export interface EngineIdentity {
30
+ name: string;
31
+ version: string;
32
+ binary_sha256: string | null;
33
+ model: string | null;
34
+ configuration_sha256: string;
35
+ }
36
+ export interface EngineResult {
37
+ draft: CandidateDraft;
38
+ engine: EngineIdentity;
39
+ usage: EngineUsage;
40
+ wallTimeMs: number;
41
+ eventTypes: string[];
42
+ actionTypes: string[];
43
+ eventsDigest: string;
44
+ stderrDigest: string;
45
+ }
46
+ export interface EngineContext {
47
+ mission: Mission;
48
+ briefing: Record<string, unknown>;
49
+ paths: WorkspacePaths;
50
+ budget: BudgetTracker;
51
+ }
52
+ export interface Engine {
53
+ readonly name: string;
54
+ run(context: EngineContext): Promise<EngineResult>;
55
+ }
@@ -0,0 +1,4 @@
1
+ export const ENGINE_OUTPUT_SCHEMA = "canopus.engine-output.v0";
2
+ export const MAX_ENGINE_ARTIFACTS = 8;
3
+ export const MAX_ENGINE_OBSERVATIONS = 16;
4
+ export const MAX_ENGINE_CAVEATS = 8;
@@ -0,0 +1,7 @@
1
+ import type { CandidateDraft, Engine, EngineContext, EngineResult } from "./engine.js";
2
+ export declare class FakeEngine implements Engine {
3
+ #private;
4
+ readonly name = "fake";
5
+ constructor(draft: CandidateDraft | ((context: EngineContext) => CandidateDraft | Promise<CandidateDraft>));
6
+ run(context: EngineContext): Promise<EngineResult>;
7
+ }
@@ -0,0 +1,37 @@
1
+ import { contentDigest, sha256Bytes } from "../util/canonical.js";
2
+ export class FakeEngine {
3
+ name = "fake";
4
+ #draft;
5
+ constructor(draft) {
6
+ this.#draft = draft;
7
+ }
8
+ async run(context) {
9
+ const started = performance.now();
10
+ context.budget.beginAttempt();
11
+ context.budget.beginProcess();
12
+ const draft = typeof this.#draft === "function" ? await this.#draft(context) : structuredClone(this.#draft);
13
+ const serialized = JSON.stringify(draft);
14
+ context.budget.addOutput(Buffer.byteLength(serialized));
15
+ return {
16
+ draft,
17
+ engine: {
18
+ name: this.name,
19
+ version: "1",
20
+ binary_sha256: null,
21
+ model: null,
22
+ configuration_sha256: contentDigest({ engine: this.name, version: "1" }),
23
+ },
24
+ usage: {
25
+ input_tokens: 0,
26
+ cached_input_tokens: 0,
27
+ output_tokens: 0,
28
+ reasoning_output_tokens: 0,
29
+ },
30
+ wallTimeMs: Math.max(0, Math.round(performance.now() - started)),
31
+ eventTypes: ["fake.completed"],
32
+ actionTypes: [],
33
+ eventsDigest: contentDigest({ type: "fake.completed", output: draft }),
34
+ stderrDigest: sha256Bytes(""),
35
+ };
36
+ }
37
+ }
@@ -0,0 +1,9 @@
1
+ import type { Mission } from "../contracts/mission.js";
2
+ import type { EngineResult } from "../engines/engine.js";
3
+ export interface SupportingArtifactSpec {
4
+ path: string;
5
+ kind: "engine-manifest" | "verifier-manifest";
6
+ value: Record<string, unknown>;
7
+ }
8
+ export declare function engineManifest(engine: EngineResult): SupportingArtifactSpec;
9
+ export declare function verifierManifest(mission: Mission): SupportingArtifactSpec;
@@ -0,0 +1,56 @@
1
+ import { contentDigest } from "../util/canonical.js";
2
+ function manifestPath(kind, value) {
3
+ const directory = kind === "engine-manifest" ? "engine-manifests" : "verifier-manifests";
4
+ return `canopus/${directory}/${contentDigest(value).slice("sha256:".length)}.json`;
5
+ }
6
+ export function engineManifest(engine) {
7
+ const value = {
8
+ schema: "canopus.engine-manifest.v0",
9
+ engine: engine.engine,
10
+ usage: engine.usage,
11
+ wall_time_ms: engine.wallTimeMs,
12
+ event_types: engine.eventTypes,
13
+ action_types: engine.actionTypes,
14
+ events_sha256: engine.eventsDigest,
15
+ stderr_sha256: engine.stderrDigest,
16
+ };
17
+ return { path: manifestPath("engine-manifest", value), kind: "engine-manifest", value };
18
+ }
19
+ export function verifierManifest(mission) {
20
+ const common = {
21
+ capsule_path: mission.verifier.argv[0],
22
+ executable_sha256: mission.verifier.executable_sha256,
23
+ argv_template: mission.verifier.argv,
24
+ cwd: mission.verifier.cwd,
25
+ };
26
+ const value = mission.schema === "canopus.mission.v1"
27
+ ? {
28
+ schema: "canopus.verifier-manifest.v1",
29
+ ...common,
30
+ image: mission.verifier.image,
31
+ sandbox: {
32
+ backend: "docker",
33
+ network: "deny",
34
+ root_filesystem: "read_only",
35
+ bind_mounts: "exact_inputs_read_only",
36
+ capabilities: "drop_all",
37
+ privilege_escalation: "deny",
38
+ },
39
+ platform: {
40
+ os: "linux",
41
+ arch: mission.verifier.platform?.slice("linux/".length) ?? "unspecified",
42
+ },
43
+ }
44
+ : {
45
+ schema: "canopus.verifier-manifest.v0",
46
+ ...common,
47
+ sandbox: {
48
+ backend: "macos_seatbelt",
49
+ network: "deny",
50
+ persistent_writes: "deny",
51
+ process_fork: "deny",
52
+ },
53
+ platform: { os: "darwin", arch: process.arch },
54
+ };
55
+ return { path: manifestPath("verifier-manifest", value), kind: "verifier-manifest", value };
56
+ }
@@ -0,0 +1,32 @@
1
+ export * from "./contracts/candidate.js";
2
+ export * from "./contracts/mission.js";
3
+ export * from "./contracts/validation.js";
4
+ export * from "./budget/enforce.js";
5
+ export * from "./evidence/manifests.js";
6
+ export * from "./artifact/freeze.js";
7
+ export * from "./activity/events.js";
8
+ export * from "./activity/store.js";
9
+ export * from "./candidate/validate.js";
10
+ export * from "./util/canonical.js";
11
+ export * from "./util/command.js";
12
+ export * from "./util/files.js";
13
+ export * from "./roles.js";
14
+ export * from "./receipt/map.js";
15
+ export * from "./repair/plan.js";
16
+ export * from "./projection/run.js";
17
+ export * from "./projection/diagnostic.js";
18
+ export * from "./product/doctor.js";
19
+ export * from "./product/profile.js";
20
+ export * from "./product/profile-bundle.js";
21
+ export * from "./product/replay.js";
22
+ export * from "./product/run.js";
23
+ export * from "./run.js";
24
+ export * from "./engines/engine.js";
25
+ export * from "./engines/fake.js";
26
+ export * from "./engines/codex-events.js";
27
+ export * from "./engines/codex-tool-free.js";
28
+ export * from "./engines/codex-exec.js";
29
+ export * from "./vela/cli.js";
30
+ export * from "./vela/types.js";
31
+ export * from "./workspace/prepare.js";
32
+ export * from "./verifier/run.js";
@@ -0,0 +1,32 @@
1
+ export * from "./contracts/candidate.js";
2
+ export * from "./contracts/mission.js";
3
+ export * from "./contracts/validation.js";
4
+ export * from "./budget/enforce.js";
5
+ export * from "./evidence/manifests.js";
6
+ export * from "./artifact/freeze.js";
7
+ export * from "./activity/events.js";
8
+ export * from "./activity/store.js";
9
+ export * from "./candidate/validate.js";
10
+ export * from "./util/canonical.js";
11
+ export * from "./util/command.js";
12
+ export * from "./util/files.js";
13
+ export * from "./roles.js";
14
+ export * from "./receipt/map.js";
15
+ export * from "./repair/plan.js";
16
+ export * from "./projection/run.js";
17
+ export * from "./projection/diagnostic.js";
18
+ export * from "./product/doctor.js";
19
+ export * from "./product/profile.js";
20
+ export * from "./product/profile-bundle.js";
21
+ export * from "./product/replay.js";
22
+ export * from "./product/run.js";
23
+ export * from "./run.js";
24
+ export * from "./engines/engine.js";
25
+ export * from "./engines/fake.js";
26
+ export * from "./engines/codex-events.js";
27
+ export * from "./engines/codex-tool-free.js";
28
+ export * from "./engines/codex-exec.js";
29
+ export * from "./vela/cli.js";
30
+ export * from "./vela/types.js";
31
+ export * from "./workspace/prepare.js";
32
+ export * from "./verifier/run.js";
@@ -0,0 +1,33 @@
1
+ import { type LandingSpec, type MissionV1, type PositiveResultContractV1 } from "../contracts/mission.js";
2
+ import { type CommandRunner } from "../util/command.js";
3
+ export interface PrepareMissionOptions {
4
+ draft: unknown;
5
+ draftRoot: string;
6
+ sourceRepo: string;
7
+ outputRoot: string;
8
+ velaBinary: string;
9
+ codexBinary: string;
10
+ dockerBinary: string;
11
+ verifierImage: string;
12
+ verifierPlatform?: "linux/amd64" | "linux/arm64";
13
+ outputSchema: string;
14
+ permissionProfile: string;
15
+ targetPacket?: {
16
+ target: string;
17
+ schema: string;
18
+ };
19
+ landing?: LandingSpec;
20
+ profileName?: string;
21
+ profileRoot?: string;
22
+ resultContract?: PositiveResultContractV1;
23
+ runner?: CommandRunner;
24
+ }
25
+ export interface PreparedMission {
26
+ mission: MissionV1;
27
+ bundleRoot: string;
28
+ missionPath: string;
29
+ manifestPath: string;
30
+ }
31
+ export declare function assertVerifierWorkingDirectory(sourceRepo: string, declaredCwd: string): Promise<void>;
32
+ export declare function validateMissionBundle(mission: MissionV1, bundleRoot: string): Promise<void>;
33
+ export declare function prepareMission(options: PrepareMissionOptions): Promise<PreparedMission>;
@@ -0,0 +1,375 @@
1
+ import { constants } from "node:fs";
2
+ import { access, chmod, copyFile, lstat, mkdir, mkdtemp, realpath, rm, writeFile, } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { EXECUTION_BINDING_SCHEMA, parseMission, } from "../contracts/mission.js";
7
+ import { objectAt, relativePathAt, sha256At, stringAt, } from "../contracts/validation.js";
8
+ import { canonicalJson, contentDigest, sha256Bytes } from "../util/canonical.js";
9
+ import { isolatedEnvironment, runCommand } from "../util/command.js";
10
+ import { MAX_EXECUTABLE_BYTES, readBoundedRegularFile, sha256RegularFile } from "../util/files.js";
11
+ import { VelaClient } from "../vela/cli.js";
12
+ export async function assertVerifierWorkingDirectory(sourceRepo, declaredCwd) {
13
+ const root = await realpath(sourceRepo);
14
+ const relative = relativePathAt(declaredCwd, "mission.verifier.cwd");
15
+ const candidate = path.resolve(root, relative);
16
+ let resolved;
17
+ let stat;
18
+ try {
19
+ [resolved, stat] = await Promise.all([realpath(candidate), lstat(candidate)]);
20
+ }
21
+ catch {
22
+ throw new Error(`mission verifier cwd does not exist in the sealed source checkout: ${relative}`);
23
+ }
24
+ if (!resolved.startsWith(`${root}${path.sep}`) || !stat.isDirectory() || stat.isSymbolicLink()) {
25
+ throw new Error(`mission verifier cwd is not a real directory below the sealed source checkout: ${relative}`);
26
+ }
27
+ }
28
+ export async function validateMissionBundle(mission, bundleRoot) {
29
+ const root = await realpath(bundleRoot);
30
+ const capsule = await sourceFile(root, mission.verifier.capsule_path, "verifier capsule");
31
+ await access(capsule, constants.X_OK);
32
+ const capsuleDigest = sha256Bytes(await readBoundedRegularFile(capsule, 268_435_456));
33
+ if (capsuleDigest !== mission.verifier.capsule_sha256 ||
34
+ capsuleDigest !== mission.verifier.executable_sha256) {
35
+ throw new Error("mission bundle verifier capsule drifted");
36
+ }
37
+ const packet = await sourceFile(root, "packet/target.json", "bundled target packet");
38
+ if (sha256Bytes(await readBoundedRegularFile(packet, 64 * 1024 * 1024)) !== mission.target_packet.sha256) {
39
+ throw new Error("mission bundle target packet drifted");
40
+ }
41
+ const outputSchema = await sourceFile(root, "contract/engine-output.v0.json", "bundled engine output schema");
42
+ if (sha256Bytes(await readBoundedRegularFile(outputSchema, 8 * 1024 * 1024)) !==
43
+ mission.worker.output_schema_sha256) {
44
+ throw new Error("mission bundle engine output schema drifted");
45
+ }
46
+ const permissionProfile = await sourceFile(root, mission.worker.permission_profile_path, "bundled worker permission profile");
47
+ if (sha256Bytes(await readBoundedRegularFile(permissionProfile, 8 * 1024 * 1024)) !==
48
+ mission.worker.permission_profile_sha256) {
49
+ throw new Error("mission bundle worker permission profile drifted");
50
+ }
51
+ const manifestBytes = await readBoundedRegularFile(path.join(root, "bundle-manifest.json"), 8 * 1024 * 1024);
52
+ let manifestValue;
53
+ try {
54
+ manifestValue = JSON.parse(manifestBytes.toString("utf8"));
55
+ }
56
+ catch (error) {
57
+ throw new Error(`mission bundle manifest is invalid JSON: ${String(error)}`);
58
+ }
59
+ const manifest = objectAt(manifestValue, "mission bundle manifest");
60
+ if (manifest.schema !== "canopus.mission-bundle.v1" ||
61
+ manifest.authority !== "non_authoritative" ||
62
+ manifest.mission_sha256 !== contentDigest(mission)) {
63
+ throw new Error("mission bundle manifest does not bind the mission");
64
+ }
65
+ }
66
+ async function commandText(runner, argv, cwd, home, allowStderr = false) {
67
+ const result = await runner({
68
+ argv,
69
+ cwd,
70
+ env: isolatedEnvironment(home),
71
+ timeoutMs: 120_000,
72
+ maxOutputBytes: 8 * 1024 * 1024,
73
+ });
74
+ if (result.exitCode !== 0 || (!allowStderr && result.stderr.length !== 0)) {
75
+ throw new Error(`${argv[0]} ${argv.slice(1).join(" ")} failed with exit ${result.exitCode}; ` +
76
+ `stdout_sha256=${sha256Bytes(result.stdout)}; stderr_sha256=${sha256Bytes(result.stderr)}`);
77
+ }
78
+ return result.stdout.toString("utf8").trim();
79
+ }
80
+ async function assertFreshOutput(outputRoot) {
81
+ try {
82
+ await lstat(outputRoot);
83
+ throw new Error("mission output root already exists");
84
+ }
85
+ catch (error) {
86
+ if (error.code !== "ENOENT")
87
+ throw error;
88
+ }
89
+ }
90
+ function below(child, parent) {
91
+ const relative = path.relative(parent, child);
92
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
93
+ }
94
+ async function sourceFile(root, relative, label) {
95
+ const parent = await realpath(root);
96
+ const resolved = await realpath(path.resolve(parent, relative));
97
+ if (!below(resolved, parent))
98
+ throw new Error(`${label} escapes its registered root`);
99
+ const stat = await lstat(resolved);
100
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
101
+ throw new Error(`${label} must be one regular, singly linked file`);
102
+ }
103
+ return resolved;
104
+ }
105
+ function boundedObjective(value) {
106
+ const objective = stringAt(value, "mission.objective", { min: 1, max: 8192 });
107
+ const universal = /\b(?:settle|solve|prove|establish)\b[^.]{0,160}\b(?:for all|for every|unbounded|universally)\b/iu;
108
+ const finite = /\b(?:bounded|exact|finite|range|through|up to|k\s*=\s*\d+|witness|counterexample|exhaust)\b/iu;
109
+ if (universal.test(objective) || !finite.test(objective)) {
110
+ throw new Error("mission objective must name a finite or exactly checkable obligation");
111
+ }
112
+ return objective;
113
+ }
114
+ function selectedAttack(offer, requested) {
115
+ if (!Array.isArray(offer.targets))
116
+ throw new Error("vela next omitted targets");
117
+ const attacks = [];
118
+ for (const [index, raw] of offer.targets.entries()) {
119
+ const target = objectAt(raw, `vela next.targets[${index}]`);
120
+ if (target.lane === "attack")
121
+ attacks.push(target);
122
+ }
123
+ if (attacks.length === 0) {
124
+ throw new Error("vela next returned no non-review attack target in the first 128 offers");
125
+ }
126
+ if (requested === undefined)
127
+ return attacks[0];
128
+ const matches = attacks.filter((target) => (target.target_id ?? target.id) === requested);
129
+ if (matches.length !== 1) {
130
+ throw new Error(`explicit mission target ${requested} must appear exactly once among the first 128 attack offers; ` +
131
+ `observed ${matches.length}`);
132
+ }
133
+ return matches[0];
134
+ }
135
+ function packetFromTarget(target, expected) {
136
+ const packet = target.packet === undefined
137
+ ? objectAt(objectAt(target.task, "vela next attack.task").packet_ref, "vela next attack.task.packet_ref")
138
+ : objectAt(target.packet, "vela next attack.packet");
139
+ const targetId = target.target_id ?? target.id;
140
+ if (expected !== undefined && targetId !== expected.target) {
141
+ throw new Error(`selected attack target ${String(targetId)} does not match profile target ${expected.target}`);
142
+ }
143
+ const schema = stringAt(packet.schema, "vela next attack.packet.schema", { min: 1, max: 128 });
144
+ if (expected !== undefined && schema !== expected.schema) {
145
+ throw new Error(`selected packet schema ${schema} does not match profile schema ${expected.schema}`);
146
+ }
147
+ if (expected === undefined && schema !== "erdos-frontier.problem-work.v1") {
148
+ throw new Error("selected attack target has no registered exact problem packet schema");
149
+ }
150
+ return {
151
+ path: relativePathAt(packet.path, "vela next attack.packet.path"),
152
+ sha256: sha256At(packet.sha256, "vela next attack.packet.sha256"),
153
+ };
154
+ }
155
+ async function imageId(runner, dockerBinary, image, cwd, home) {
156
+ const observed = await commandText(runner, [dockerBinary, "image", "inspect", "--format={{.Id}}", image], cwd, home);
157
+ return sha256At(observed, "Docker image ID");
158
+ }
159
+ async function workerIdentity(runner, binaryPath, cwd, home) {
160
+ if (process.platform !== "darwin" && process.platform !== "linux") {
161
+ throw new Error("canopus.mission.v1 tool workers require macOS, Linux, or WSL2");
162
+ }
163
+ const binary = await realpath(binaryPath);
164
+ const output = await commandText(runner, [binary, "--version"], cwd, home, true);
165
+ const version = stringAt(output, "worker Codex version", {
166
+ min: 10,
167
+ max: 64,
168
+ pattern: /^codex-cli [0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$/u,
169
+ });
170
+ return {
171
+ codex_version: version,
172
+ codex_sha256: await sha256RegularFile(binary, MAX_EXECUTABLE_BYTES),
173
+ };
174
+ }
175
+ export async function prepareMission(options) {
176
+ const runner = options.runner ?? runCommand;
177
+ const sourceRepo = await realpath(options.sourceRepo);
178
+ const draftRoot = await realpath(options.draftRoot);
179
+ const outputRoot = path.resolve(options.outputRoot);
180
+ await assertFreshOutput(outputRoot);
181
+ const runtimeHome = await mkdtemp(path.join(os.tmpdir(), "canopus-prepare-"));
182
+ try {
183
+ const status = await commandText(runner, ["git", "status", "--porcelain=v1", "--untracked-files=all"], sourceRepo, runtimeHome);
184
+ if (status !== "")
185
+ throw new Error("mission source checkout must be clean");
186
+ const raw = objectAt(options.draft, "mission draft");
187
+ const frontier = raw.frontier === "."
188
+ ? "."
189
+ : relativePathAt(raw.frontier, "mission.frontier");
190
+ const velaBinary = await realpath(options.velaBinary);
191
+ const velaSha256 = await sha256RegularFile(velaBinary, MAX_EXECUTABLE_BYTES);
192
+ const versionText = await commandText(runner, [velaBinary, "--version"], sourceRepo, runtimeHome);
193
+ const versionMatch = /^vela (0\.[0-9]+\.[0-9]+)$/u.exec(versionText);
194
+ if (versionMatch?.[1] === undefined)
195
+ throw new Error("Vela returned an invalid version");
196
+ const velaVersion = versionMatch[1];
197
+ const vela = new VelaClient({
198
+ binary: velaBinary,
199
+ expectedVersion: velaVersion,
200
+ expectedSha256: velaSha256,
201
+ home: path.join(runtimeHome, "vela"),
202
+ runner,
203
+ });
204
+ const strictBaseline = await vela.observeStrictBaseline(sourceRepo, frontier);
205
+ const inspection = await vela.inspect(sourceRepo, frontier, strictBaseline);
206
+ const offer = await vela.offer(sourceRepo, frontier, inspection.roots, strictBaseline, 128);
207
+ const requestedTarget = raw.target === undefined || raw.target === "auto"
208
+ ? undefined
209
+ : stringAt(raw.target, "mission.target", { min: 1, max: 256 });
210
+ const target = selectedAttack(offer.value, requestedTarget);
211
+ const targetId = stringAt(target.target_id ?? target.id, "vela next attack.target_id", { min: 1, max: 256 });
212
+ const packet = packetFromTarget(target, options.targetPacket);
213
+ const packetSource = await sourceFile(sourceRepo, packet.path, "target packet");
214
+ const packetBytes = await readBoundedRegularFile(packetSource, 64 * 1024 * 1024);
215
+ if (sha256Bytes(packetBytes) !== packet.sha256) {
216
+ throw new Error("selected target packet digest does not match vela next");
217
+ }
218
+ const tracked = await commandText(runner, ["git", "ls-files", "--error-unmatch", "--", packet.path], sourceRepo, runtimeHome);
219
+ if (tracked !== packet.path)
220
+ throw new Error("target packet is not an exact tracked source file");
221
+ const verifierDraft = objectAt(raw.verifier, "mission.verifier");
222
+ const sourceCapsuleRelative = relativePathAt(verifierDraft.capsule_path, "mission.verifier.capsule_path");
223
+ const sourceCapsule = await sourceFile(draftRoot, sourceCapsuleRelative, "verifier capsule");
224
+ await access(sourceCapsule, constants.X_OK);
225
+ const capsuleBytes = await readBoundedRegularFile(sourceCapsule, 268_435_456);
226
+ const capsuleDigest = sha256Bytes(capsuleBytes);
227
+ const workerDraft = objectAt(raw.worker, "mission.worker");
228
+ const outputSchemaSource = await sourceFile(path.dirname(await realpath(options.outputSchema)), path.basename(options.outputSchema), "engine output schema");
229
+ const outputSchemaBytes = await readBoundedRegularFile(outputSchemaSource, 8 * 1024 * 1024);
230
+ const outputSchemaDigest = sha256Bytes(outputSchemaBytes);
231
+ const permissionProfileSource = await sourceFile(path.dirname(await realpath(options.permissionProfile)), path.basename(options.permissionProfile), "worker permission profile");
232
+ const permissionProfileBytes = await readBoundedRegularFile(permissionProfileSource, 8 * 1024 * 1024);
233
+ const permissionProfileDigest = sha256Bytes(permissionProfileBytes);
234
+ const verifierImage = await imageId(runner, options.dockerBinary, options.verifierImage, sourceRepo, runtimeHome);
235
+ const identity = await workerIdentity(runner, options.codexBinary, sourceRepo, runtimeHome);
236
+ const verifierArgv = Array.isArray(verifierDraft.argv)
237
+ ? [...verifierDraft.argv]
238
+ : verifierDraft.argv;
239
+ if (!Array.isArray(verifierArgv) || verifierArgv.length === 0) {
240
+ throw new Error("mission verifier argv is required");
241
+ }
242
+ verifierArgv[0] = "capsule/verifier";
243
+ const prepared = parseMission({
244
+ ...raw,
245
+ schema: "canopus.mission.v1",
246
+ target: targetId,
247
+ vela_version: velaVersion,
248
+ vela_sha256: velaSha256,
249
+ objective: boundedObjective(raw.objective),
250
+ roots: inspection.roots,
251
+ target_packet: packet,
252
+ strict_baseline: strictBaseline,
253
+ worker: {
254
+ ...workerDraft,
255
+ kind: "codex_tools_native",
256
+ platform: process.platform,
257
+ ...identity,
258
+ permission_profile_path: "contract/native-worker.config.toml",
259
+ permission_profile_sha256: permissionProfileDigest,
260
+ workspace: "target_packet_only",
261
+ output_schema_sha256: outputSchemaDigest,
262
+ network: "provider_only",
263
+ tools: ["shell", "apply_patch"],
264
+ },
265
+ verifier: {
266
+ ...verifierDraft,
267
+ argv: verifierArgv,
268
+ executable_sha256: capsuleDigest,
269
+ capsule_path: "capsule/verifier",
270
+ capsule_sha256: capsuleDigest,
271
+ image: verifierImage,
272
+ ...(options.verifierPlatform === undefined
273
+ ? {}
274
+ : { platform: options.verifierPlatform }),
275
+ network: "deny",
276
+ writes: "deny",
277
+ },
278
+ landing: options.landing ?? { expected_routes: ["defer"], max_accepted_delta: 0 },
279
+ ...(options.profileName === undefined || options.profileRoot === undefined
280
+ ? {}
281
+ : {
282
+ profile: {
283
+ name: options.profileName,
284
+ root: sha256At(options.profileRoot, "profile root"),
285
+ },
286
+ }),
287
+ ...(options.profileRoot === undefined || options.resultContract === undefined
288
+ ? {}
289
+ : {
290
+ execution_binding: {
291
+ schema: EXECUTION_BINDING_SCHEMA,
292
+ packet_root: packet.sha256,
293
+ profile_root: sha256At(options.profileRoot, "profile root"),
294
+ verifier_capsule_root: capsuleDigest,
295
+ result_contract_root: contentDigest(options.resultContract),
296
+ },
297
+ result_contract: options.resultContract,
298
+ }),
299
+ });
300
+ if ((options.profileName === undefined) !== (options.profileRoot === undefined)) {
301
+ throw new Error("mission preparation requires profile name and root together");
302
+ }
303
+ if (options.resultContract !== undefined && options.profileRoot === undefined) {
304
+ throw new Error("mission result contract requires a profile root");
305
+ }
306
+ if (prepared.schema !== "canopus.mission.v1") {
307
+ throw new Error("prepared mission did not produce mission v1");
308
+ }
309
+ await assertVerifierWorkingDirectory(sourceRepo, prepared.verifier.cwd);
310
+ const capsuleTarget = path.join(outputRoot, "capsule", "verifier");
311
+ const packetTarget = path.join(outputRoot, "packet", "target.json");
312
+ const outputSchemaTarget = path.join(outputRoot, "contract", "engine-output.v0.json");
313
+ const permissionProfileTarget = path.join(outputRoot, "contract", "native-worker.config.toml");
314
+ await Promise.all([
315
+ mkdir(path.dirname(capsuleTarget), { recursive: true, mode: 0o700 }),
316
+ mkdir(path.dirname(packetTarget), { recursive: true, mode: 0o700 }),
317
+ mkdir(path.dirname(outputSchemaTarget), { recursive: true, mode: 0o700 }),
318
+ ]);
319
+ await Promise.all([
320
+ copyFile(sourceCapsule, capsuleTarget),
321
+ copyFile(packetSource, packetTarget),
322
+ copyFile(outputSchemaSource, outputSchemaTarget),
323
+ copyFile(permissionProfileSource, permissionProfileTarget),
324
+ ]);
325
+ await chmod(capsuleTarget, 0o555);
326
+ const missionPath = path.join(outputRoot, "mission.json");
327
+ const manifestPath = path.join(outputRoot, "bundle-manifest.json");
328
+ const manifest = {
329
+ schema: "canopus.mission-bundle.v1",
330
+ authority: "non_authoritative",
331
+ mission_sha256: contentDigest(prepared),
332
+ source: {
333
+ git_commit: prepared.roots.git_commit,
334
+ git_tree: prepared.roots.git_tree,
335
+ },
336
+ packet: {
337
+ source_path: packet.path,
338
+ bundle_path: "packet/target.json",
339
+ sha256: packet.sha256,
340
+ },
341
+ verifier: {
342
+ bundle_path: "capsule/verifier",
343
+ sha256: capsuleDigest,
344
+ image: verifierImage,
345
+ ...(options.verifierPlatform === undefined
346
+ ? {}
347
+ : { platform: options.verifierPlatform }),
348
+ },
349
+ worker: {
350
+ platform: process.platform,
351
+ codex_version: identity.codex_version,
352
+ codex_sha256: identity.codex_sha256,
353
+ permission_profile_path: "contract/native-worker.config.toml",
354
+ permission_profile_sha256: permissionProfileDigest,
355
+ workspace: "target_packet_only",
356
+ output_schema_sha256: outputSchemaDigest,
357
+ },
358
+ ...(prepared.execution_binding === undefined
359
+ ? {}
360
+ : { execution_binding: prepared.execution_binding }),
361
+ };
362
+ await Promise.all([
363
+ writeFile(missionPath, canonicalJson(prepared), { flag: "wx", mode: 0o600 }),
364
+ writeFile(manifestPath, canonicalJson(manifest), { flag: "wx", mode: 0o600 }),
365
+ ]);
366
+ return { mission: prepared, bundleRoot: outputRoot, missionPath, manifestPath };
367
+ }
368
+ catch (error) {
369
+ await rm(outputRoot, { recursive: true, force: true });
370
+ throw error;
371
+ }
372
+ finally {
373
+ await rm(runtimeHome, { recursive: true, force: true });
374
+ }
375
+ }