@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,251 @@
1
+ import { constants } from "node:fs";
2
+ import { access, chmod, copyFile, mkdir, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { arrayAt, exactKeys, objectAt, relativePathAt, sha256At, stringAt, } from "../contracts/validation.js";
7
+ import { parsePositiveResultContract, } from "../contracts/mission.js";
8
+ import { contentDigest, sha256Bytes } from "../util/canonical.js";
9
+ import { readBoundedRegularFile } from "../util/files.js";
10
+ export const PROFILE_V1_SCHEMA = "canopus.profile.v1";
11
+ export const PROFILE_SCHEMA = "canopus.profile.v2";
12
+ export const SUPPORTED_PRODUCT_PLATFORMS = ["darwin-arm64", "linux-x86_64"];
13
+ export function productPackageRoot() {
14
+ return fileURLToPath(new URL("../../../", import.meta.url));
15
+ }
16
+ export function currentProductPlatform() {
17
+ if (process.platform === "darwin" && process.arch === "arm64")
18
+ return "darwin-arm64";
19
+ if (process.platform === "linux" && process.arch === "x64")
20
+ return "linux-x86_64";
21
+ // Native Windows is a read-only control surface. Its tool-using mission must
22
+ // be launched inside WSL2, which consumes the exact Linux capsule binding.
23
+ if (process.platform === "win32" && process.arch === "x64")
24
+ return "linux-x86_64";
25
+ throw new Error(`tool-using missions are unsupported on ${process.platform}-${process.arch}; ` +
26
+ "supported workers are macOS arm64 and Linux/WSL2 x86-64; native Windows is read-only");
27
+ }
28
+ async function registeredProfileNames() {
29
+ const root = path.join(productPackageRoot(), "profiles");
30
+ const { readdir } = await import("node:fs/promises");
31
+ return (await readdir(root, { withFileTypes: true }))
32
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
33
+ .map((entry) => entry.name.slice(0, -5))
34
+ .sort();
35
+ }
36
+ export function verifierImageAt(value, at = "verifier_image") {
37
+ return stringAt(value, at, {
38
+ min: 100,
39
+ max: 200,
40
+ pattern: /^ghcr\.io\/vela-science\/(?:canopus-verifier|canopus-formal-verifier)@sha256:[0-9a-f]{64}$/u,
41
+ });
42
+ }
43
+ export async function listProductProfiles() {
44
+ return registeredProfileNames();
45
+ }
46
+ function parsePlatformCapsule(value, at, defaultVerifierPlatform) {
47
+ const object = objectAt(value, at);
48
+ exactKeys(object, [
49
+ "worker_profile",
50
+ "worker_profile_sha256",
51
+ "verifier_capsule",
52
+ "verifier_capsule_sha256",
53
+ "verifier_image",
54
+ ], ["verifier_platform"], at);
55
+ return {
56
+ worker_profile: relativePathAt(object.worker_profile, `${at}.worker_profile`),
57
+ worker_profile_sha256: sha256At(object.worker_profile_sha256, `${at}.worker_profile_sha256`),
58
+ verifier_capsule: relativePathAt(object.verifier_capsule, `${at}.verifier_capsule`),
59
+ verifier_capsule_sha256: sha256At(object.verifier_capsule_sha256, `${at}.verifier_capsule_sha256`),
60
+ verifier_image: verifierImageAt(object.verifier_image, `${at}.verifier_image`),
61
+ verifier_platform: object.verifier_platform === undefined
62
+ ? defaultVerifierPlatform
63
+ : stringAt(object.verifier_platform, `${at}.verifier_platform`, {
64
+ min: 11,
65
+ max: 11,
66
+ pattern: /^linux\/(?:amd64|arm64)$/u,
67
+ }),
68
+ };
69
+ }
70
+ function parseV2(value, name, platform, profileSha256) {
71
+ exactKeys(value, [
72
+ "schema",
73
+ "name",
74
+ "target",
75
+ "target_packet_schema",
76
+ "draft",
77
+ "draft_sha256",
78
+ "objective_sha256",
79
+ "completion_condition_sha256",
80
+ "allowed_artifacts_sha256",
81
+ "budgets_sha256",
82
+ "replay_argv_sha256",
83
+ "landing",
84
+ "platforms",
85
+ ], ["result_contract", "result_contract_sha256"], "profile");
86
+ const platformsValue = objectAt(value.platforms, "profile.platforms");
87
+ exactKeys(platformsValue, [...SUPPORTED_PRODUCT_PLATFORMS], [], "profile.platforms");
88
+ const platforms = Object.fromEntries(SUPPORTED_PRODUCT_PLATFORMS.map((platform) => [
89
+ platform,
90
+ parsePlatformCapsule(platformsValue[platform], `profile.platforms.${platform}`, platform === "darwin-arm64" ? "linux/arm64" : "linux/amd64"),
91
+ ]));
92
+ const landingValue = objectAt(value.landing, "profile.landing");
93
+ exactKeys(landingValue, ["expected_routes", "max_accepted_delta"], [], "profile.landing");
94
+ const expectedRoutes = arrayAt(landingValue.expected_routes, "profile.landing.expected_routes", { min: 1, max: 1, unique: true }, (item, at) => stringAt(item, at, { min: 5, max: 6, pattern: /^(?:defer|permit)$/u }));
95
+ const route = expectedRoutes[0];
96
+ if (route !== "defer" && route !== "permit") {
97
+ throw new Error("profile landing route is unsupported");
98
+ }
99
+ const maxAcceptedDelta = route === "permit" ? 1 : 0;
100
+ if (landingValue.max_accepted_delta !== maxAcceptedDelta) {
101
+ throw new Error(`profile ${route} landing must register accepted delta ${maxAcceptedDelta}`);
102
+ }
103
+ if ((value.result_contract === undefined) !== (value.result_contract_sha256 === undefined)) {
104
+ throw new Error("profile result contract path and root must be supplied together");
105
+ }
106
+ if (route === "permit" && value.result_contract === undefined) {
107
+ throw new Error("profile Permit landing requires an exact positive result contract");
108
+ }
109
+ const selected = platforms[platform];
110
+ const parsed = {
111
+ schema: PROFILE_SCHEMA,
112
+ name: stringAt(value.name, "profile.name", { min: 1, max: 128 }),
113
+ target: stringAt(value.target, "profile.target", { min: 1, max: 256 }),
114
+ target_packet_schema: stringAt(value.target_packet_schema, "profile.target_packet_schema", {
115
+ min: 1,
116
+ max: 128,
117
+ }),
118
+ draft: relativePathAt(value.draft, "profile.draft"),
119
+ draft_sha256: sha256At(value.draft_sha256, "profile.draft_sha256"),
120
+ objective_sha256: sha256At(value.objective_sha256, "profile.objective_sha256"),
121
+ completion_condition_sha256: sha256At(value.completion_condition_sha256, "profile.completion_condition_sha256"),
122
+ allowed_artifacts_sha256: sha256At(value.allowed_artifacts_sha256, "profile.allowed_artifacts_sha256"),
123
+ budgets_sha256: sha256At(value.budgets_sha256, "profile.budgets_sha256"),
124
+ replay_argv_sha256: sha256At(value.replay_argv_sha256, "profile.replay_argv_sha256"),
125
+ landing: { expected_routes: [route], max_accepted_delta: maxAcceptedDelta },
126
+ profile_sha256: profileSha256,
127
+ ...(value.result_contract === undefined
128
+ ? {}
129
+ : {
130
+ result_contract: relativePathAt(value.result_contract, "profile.result_contract"),
131
+ result_contract_sha256: sha256At(value.result_contract_sha256, "profile.result_contract_sha256"),
132
+ }),
133
+ platforms,
134
+ platform,
135
+ capsule_binary: selected.verifier_capsule,
136
+ capsule_sha256: selected.verifier_capsule_sha256,
137
+ permission_profile: selected.worker_profile,
138
+ permission_profile_sha256: selected.worker_profile_sha256,
139
+ verifier_image: selected.verifier_image,
140
+ verifier_platform: selected.verifier_platform,
141
+ };
142
+ if (parsed.name !== name)
143
+ throw new Error("profile filename and registered name disagree");
144
+ return parsed;
145
+ }
146
+ export async function loadProductProfile(name, options = {}) {
147
+ if (!(await registeredProfileNames()).includes(name)) {
148
+ throw new Error(`unknown registered profile ${name}`);
149
+ }
150
+ const file = path.join(productPackageRoot(), "profiles", `${name}.json`);
151
+ const bytes = await readBoundedRegularFile(file, 1024 * 1024);
152
+ const value = objectAt(JSON.parse(bytes.toString("utf8")), "profile");
153
+ if (value.schema === PROFILE_V1_SCHEMA) {
154
+ if (options.allowLegacy !== true) {
155
+ throw new Error("canopus.profile.v1 is replay-only; prepare a closed v2 profile to run work");
156
+ }
157
+ return {
158
+ schema: PROFILE_V1_SCHEMA,
159
+ name: stringAt(value.name, "profile.name", { min: 1, max: 128 }),
160
+ target: stringAt(value.target, "profile.target", { min: 1, max: 256 }),
161
+ };
162
+ }
163
+ if (value.schema !== PROFILE_SCHEMA)
164
+ throw new Error("profile schema is not supported");
165
+ return parseV2(value, name, options.platform ?? currentProductPlatform(), sha256Bytes(bytes));
166
+ }
167
+ export async function loadProfileResultContract(profile) {
168
+ if (profile.result_contract === undefined || profile.result_contract_sha256 === undefined) {
169
+ return undefined;
170
+ }
171
+ const file = await packagedProfileResource(profile.result_contract, "profile result contract");
172
+ const bytes = await readBoundedRegularFile(file, 1024 * 1024);
173
+ const observed = sha256Bytes(bytes);
174
+ if (observed !== profile.result_contract_sha256) {
175
+ throw new Error(`profile result contract drifted: expected ${profile.result_contract_sha256}, observed ${observed}`);
176
+ }
177
+ const contract = parsePositiveResultContract(JSON.parse(bytes.toString("utf8")));
178
+ if (contentDigest(contract) !== observed) {
179
+ throw new Error("profile result contract file is not exact canonical Canopus JSON");
180
+ }
181
+ if (contract.target !== profile.target) {
182
+ throw new Error("profile and positive result contract target disagree");
183
+ }
184
+ return contract;
185
+ }
186
+ export async function packagedProfileResource(relative, label) {
187
+ const root = await realpath(productPackageRoot());
188
+ const file = await realpath(path.resolve(root, relative));
189
+ const fromRoot = path.relative(root, file);
190
+ if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${path.sep}`)) {
191
+ throw new Error(`${label} escapes the Canopus package`);
192
+ }
193
+ return file;
194
+ }
195
+ export async function loadProfileDraft(profile) {
196
+ const file = await packagedProfileResource(profile.draft, "profile draft");
197
+ const bytes = await readBoundedRegularFile(file, 1024 * 1024);
198
+ if (sha256Bytes(bytes) !== profile.draft_sha256)
199
+ throw new Error("profile draft root drifted");
200
+ const draft = objectAt(JSON.parse(bytes.toString("utf8")), "profile draft");
201
+ const assertions = [
202
+ [
203
+ sha256Bytes(Buffer.from(stringAt(draft.objective, "profile draft.objective", { min: 1, max: 8192 }))),
204
+ profile.objective_sha256,
205
+ "objective",
206
+ ],
207
+ [
208
+ sha256Bytes(Buffer.from(stringAt(draft.completion_condition, "profile draft.completion_condition", { min: 1, max: 8192 }))),
209
+ profile.completion_condition_sha256,
210
+ "completion condition",
211
+ ],
212
+ [contentDigest(draft.allowed_paths), profile.allowed_artifacts_sha256, "allowed artifacts"],
213
+ [contentDigest(draft.budgets), profile.budgets_sha256, "budgets"],
214
+ [
215
+ contentDigest(objectAt(draft.verifier, "profile draft.verifier").argv),
216
+ profile.replay_argv_sha256,
217
+ "replay argv",
218
+ ],
219
+ ];
220
+ for (const [observed, expected, label] of assertions) {
221
+ if (observed !== expected) {
222
+ throw new Error(`profile ${label} drifted from its frozen mission draft`);
223
+ }
224
+ }
225
+ return draft;
226
+ }
227
+ export async function stageProfileCapsule(options) {
228
+ const packaged = await packagedProfileResource(options.profile.capsule_binary, "profile capsule binary");
229
+ await access(packaged, constants.R_OK);
230
+ const packagedDigest = sha256Bytes(await readBoundedRegularFile(packaged, 268_435_456));
231
+ if (packagedDigest !== options.profile.capsule_sha256) {
232
+ throw new Error(`packaged capsule drifted: expected ${options.profile.capsule_sha256}, observed ${packagedDigest}`);
233
+ }
234
+ const capsule = path.join(options.stagingRoot, "capsule", "verifier");
235
+ await mkdir(path.dirname(capsule), { recursive: true, mode: 0o700 });
236
+ await copyFile(packaged, capsule, constants.COPYFILE_EXCL);
237
+ await chmod(capsule, 0o555);
238
+ const digest = sha256Bytes(await readBoundedRegularFile(capsule, 268_435_456));
239
+ if (digest !== options.profile.capsule_sha256) {
240
+ throw new Error(`staged capsule drifted: expected ${options.profile.capsule_sha256}, observed ${digest}`);
241
+ }
242
+ return { capsule, source: "packaged" };
243
+ }
244
+ export async function packagedWorkerProfile(profile) {
245
+ const file = await packagedProfileResource(profile.permission_profile, "worker permission profile");
246
+ const digest = sha256Bytes(await readBoundedRegularFile(file, 8 * 1024 * 1024));
247
+ if (digest !== profile.permission_profile_sha256) {
248
+ throw new Error("packaged worker permission profile drifted");
249
+ }
250
+ return file;
251
+ }
@@ -0,0 +1,10 @@
1
+ export declare function replayProduct(runFile: string, dockerBinary?: string): Promise<{
2
+ schema: "canopus.replay.v1";
3
+ ok: true;
4
+ run_id: string;
5
+ mission_root: string;
6
+ verifier_status: "passed" | "failed" | "error";
7
+ stdout_digest: string;
8
+ stderr_digest: string;
9
+ matched: true;
10
+ }>;
@@ -0,0 +1,71 @@
1
+ import { mkdtemp, realpath, rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { BudgetTracker } from "../budget/enforce.js";
5
+ import { parseMission } from "../contracts/mission.js";
6
+ 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";
10
+ import { readBoundedRegularFile } from "../util/files.js";
11
+ import { runVerifier } from "../verifier/run.js";
12
+ import { cleanupWorkspace, prepareWorkspace } from "../workspace/prepare.js";
13
+ export async function replayProduct(runFile, dockerBinary = "docker") {
14
+ const absoluteRun = await realpath(runFile);
15
+ const runRoot = path.dirname(absoluteRun);
16
+ const raw = JSON.parse((await readBoundedRegularFile(absoluteRun, 8 * 1024 * 1024)).toString("utf8"));
17
+ const schema = typeof raw === "object" && raw !== null && !Array.isArray(raw)
18
+ ? raw.schema
19
+ : undefined;
20
+ const record = schema === "canopus.diagnostic-run.v1"
21
+ ? parseDiagnosticRunRecord(raw)
22
+ : parseRunRecord(raw);
23
+ 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)
29
+ throw new Error("run and mission roots disagree");
30
+ const artifacts = record.candidate.artifacts.map((artifact) => ({
31
+ artifact,
32
+ frozenPath: path.join(runRoot, "artifacts", artifact.digest.slice("sha256:".length)),
33
+ }));
34
+ const replayRoot = await mkdtemp(path.join(os.tmpdir(), "canopus-replay-root-"));
35
+ await rm(replayRoot, { recursive: true, force: true });
36
+ const source = path.join(runRoot, "input");
37
+ const paths = await prepareWorkspace({
38
+ sourceRepo: source,
39
+ runRoot: replayRoot,
40
+ gitCommit: mission.roots.git_commit,
41
+ gitTree: mission.roots.git_tree,
42
+ });
43
+ try {
44
+ const verifier = await runVerifier({
45
+ mission,
46
+ paths,
47
+ artifacts,
48
+ budget: new BudgetTracker(mission.budgets),
49
+ bundleRoot,
50
+ dockerBinary,
51
+ });
52
+ if (verifier.status !== record.verifier.status ||
53
+ verifier.record.stdout_digest !== record.verifier.record.stdout_digest ||
54
+ verifier.record.stderr_digest !== record.verifier.record.stderr_digest) {
55
+ throw new Error("verifier replay does not match the frozen run record");
56
+ }
57
+ return {
58
+ schema: "canopus.replay.v1",
59
+ ok: true,
60
+ run_id: record.run_id,
61
+ mission_root: record.mission.digest,
62
+ verifier_status: verifier.status,
63
+ stdout_digest: verifier.record.stdout_digest,
64
+ stderr_digest: verifier.record.stderr_digest,
65
+ matched: true,
66
+ };
67
+ }
68
+ finally {
69
+ await cleanupWorkspace(paths);
70
+ }
71
+ }
@@ -0,0 +1,31 @@
1
+ import { type CanopusDiagnosticRunResult, type CanopusRunResult } from "../run.js";
2
+ import { type CommandRunner } from "../util/command.js";
3
+ import { type ProductDoctorResult } from "./doctor.js";
4
+ export interface ProductRunResult {
5
+ run: CanopusRunResult | CanopusDiagnosticRunResult;
6
+ doctor: ProductDoctorResult;
7
+ output_root: string;
8
+ bundle_root: string;
9
+ evidence_manifest: string;
10
+ evidence_root: string;
11
+ source_publication: {
12
+ state: "committed_local";
13
+ commit: string;
14
+ tree: string;
15
+ } | {
16
+ state: "unchanged_no_land";
17
+ commit: string;
18
+ tree: string;
19
+ };
20
+ }
21
+ export declare function defaultProductOutput(frontier: string): string;
22
+ export declare function assertToolUsingMissionPlatform(platform?: NodeJS.Platform): void;
23
+ export declare function runProduct(options: {
24
+ frontier: string;
25
+ profileName?: string;
26
+ requestedTarget?: string;
27
+ outputRoot?: string;
28
+ codexHome?: string;
29
+ noLand?: boolean;
30
+ runner?: CommandRunner;
31
+ }): Promise<ProductRunResult>;
@@ -0,0 +1,264 @@
1
+ import { lstat, mkdir, realpath, rm, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { CodexToolsNativeEngine } from "../engines/codex-tools-native.js";
6
+ import { prepareMission } from "../mission/prepare.js";
7
+ import { runCanopus, } from "../run.js";
8
+ import { canonicalJson, contentDigest, sha256Bytes } from "../util/canonical.js";
9
+ import { isolatedEnvironment, runCommand } from "../util/command.js";
10
+ import { readBoundedRegularFile } from "../util/files.js";
11
+ import { VelaClient } from "../vela/cli.js";
12
+ import { doctorProduct } from "./doctor.js";
13
+ import { loadProfileDraft, loadProfileResultContract, packagedWorkerProfile, stageProfileCapsule, } from "./profile.js";
14
+ import { retainWithdrawalCapability } from "../capability/withdrawal.js";
15
+ function packageFile(relative) {
16
+ return fileURLToPath(new URL(`../../../${relative}`, import.meta.url));
17
+ }
18
+ async function assertFreshOutput(outputRoot, sourceRoot) {
19
+ const output = path.resolve(outputRoot);
20
+ const cloudBackedRoots = [
21
+ path.join(os.homedir(), "Desktop"),
22
+ path.join(os.homedir(), "Library", "Mobile Documents"),
23
+ path.join(os.homedir(), "Library", "CloudStorage"),
24
+ ];
25
+ if (cloudBackedRoots.some((root) => output === root || output.startsWith(`${root}${path.sep}`))) {
26
+ throw new Error("Canopus output must not use a cloud-synced path because Docker verifier bind mounts can stall; use the default ~/.canopus store or another local directory");
27
+ }
28
+ const relative = path.relative(sourceRoot, output);
29
+ if (relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== "..")) {
30
+ throw new Error("Canopus output must be outside the source frontier");
31
+ }
32
+ try {
33
+ await lstat(output);
34
+ throw new Error("Canopus output root already exists");
35
+ }
36
+ catch (error) {
37
+ if (error.code !== "ENOENT")
38
+ throw error;
39
+ }
40
+ await mkdir(output, { recursive: true, mode: 0o700 });
41
+ }
42
+ async function gitText(options) {
43
+ const result = await options.runner({
44
+ argv: ["git", ...options.argv],
45
+ cwd: options.cwd,
46
+ env: isolatedEnvironment(options.home),
47
+ timeoutMs: 120_000,
48
+ maxOutputBytes: 8 * 1024 * 1024,
49
+ });
50
+ if (result.exitCode !== 0 || result.stderr.length !== 0) {
51
+ throw new Error(`git ${options.argv.join(" ")} failed: stdout_sha256=${sha256Bytes(result.stdout)}; ` +
52
+ `stderr_sha256=${sha256Bytes(result.stderr)}`);
53
+ }
54
+ return result.stdout.toString("utf8").trim();
55
+ }
56
+ async function publishVerifiedLanding(options) {
57
+ const [head, status] = await Promise.all([
58
+ gitText({ argv: ["rev-parse", "--verify", "HEAD^{commit}"], cwd: options.source, home: options.home, runner: options.runner }),
59
+ gitText({ argv: ["status", "--porcelain=v1", "--untracked-files=all"], cwd: options.source, home: options.home, runner: options.runner }),
60
+ ]);
61
+ if (head !== options.expectedStart || status !== "") {
62
+ throw new Error("source frontier changed while the bounded run was executing");
63
+ }
64
+ const landingHead = await gitText({
65
+ argv: ["rev-parse", "--verify", "canopus-landing^{commit}"],
66
+ cwd: options.landing,
67
+ home: options.home,
68
+ runner: options.runner,
69
+ });
70
+ if (landingHead !== options.expectedFinal)
71
+ throw new Error("landing clone final commit drifted");
72
+ await gitText({
73
+ argv: ["fetch", "--quiet", "--no-tags", options.landing, "canopus-landing"],
74
+ cwd: options.source,
75
+ home: options.home,
76
+ runner: options.runner,
77
+ });
78
+ await gitText({
79
+ argv: ["merge", "--ff-only", "--no-edit", "FETCH_HEAD"],
80
+ cwd: options.source,
81
+ home: options.home,
82
+ runner: options.runner,
83
+ });
84
+ const [commit, tree, finalStatus] = await Promise.all([
85
+ gitText({ argv: ["rev-parse", "--verify", "HEAD^{commit}"], cwd: options.source, home: options.home, runner: options.runner }),
86
+ gitText({ argv: ["rev-parse", "--verify", "HEAD^{tree}"], cwd: options.source, home: options.home, runner: options.runner }),
87
+ gitText({ argv: ["status", "--porcelain=v1", "--untracked-files=all"], cwd: options.source, home: options.home, runner: options.runner }),
88
+ ]);
89
+ if (commit !== options.expectedFinal || tree !== options.expectedTree || finalStatus !== "") {
90
+ throw new Error("source publication did not reproduce the verified landing roots");
91
+ }
92
+ return { state: "committed_local", commit, tree };
93
+ }
94
+ async function writeEvidenceManifest(run, missionDigest) {
95
+ const root = run.paths.root;
96
+ const files = {
97
+ activity: "activity.jsonl",
98
+ transcript: "worker-final.json",
99
+ tool_trace: "worker-events.jsonl",
100
+ worker_stderr: "worker-stderr.bin",
101
+ engine_result: "engine-result.json",
102
+ candidate: "candidate.json",
103
+ run: "run.json",
104
+ };
105
+ const digests = {};
106
+ for (const [name, relative] of Object.entries(files)) {
107
+ digests[name] = sha256Bytes(await readBoundedRegularFile(path.join(root, relative), 64 * 1024 * 1024));
108
+ }
109
+ const manifest = {
110
+ schema: "canopus.run-evidence.v1",
111
+ authority: "non_authoritative",
112
+ mission_root: missionDigest,
113
+ run_id: run.record.run_id,
114
+ target: run.record.mission.target,
115
+ files: digests,
116
+ artifact_roots: run.record.candidate.artifacts.map((artifact) => artifact.digest).sort(),
117
+ verifier_root: contentDigest(run.record.verifier),
118
+ receipt_root: run.record.landing?.receipt_root ?? null,
119
+ final_roots: "final_roots" in run.record
120
+ ? run.record.final_roots
121
+ : run.record.mission.starting_roots,
122
+ };
123
+ const file = path.join(root, "evidence-manifest.json");
124
+ await writeFile(file, canonicalJson(manifest), { flag: "wx", mode: 0o600 });
125
+ return { file, root: contentDigest(manifest) };
126
+ }
127
+ export function defaultProductOutput(frontier) {
128
+ const stamp = new Date().toISOString().replaceAll(":", "-").replaceAll(".", "-");
129
+ return path.join(os.homedir(), ".canopus", "runs", path.basename(path.resolve(frontier)), stamp);
130
+ }
131
+ export function assertToolUsingMissionPlatform(platform = process.platform) {
132
+ if (platform === "win32") {
133
+ throw new Error("tool-using missions do not run in native Windows; open WSL2, enter the frontier through its Linux path, and rerun the same canopus command there");
134
+ }
135
+ if (platform !== "darwin" && platform !== "linux") {
136
+ throw new Error(`tool-using missions are unsupported on ${platform}; supported worker hosts are macOS and Linux/WSL2`);
137
+ }
138
+ }
139
+ export async function runProduct(options) {
140
+ // Refuse unsupported custody before creating an output directory or probing
141
+ // credentials. Native Windows retains doctor/inspect/replay only.
142
+ assertToolUsingMissionPlatform();
143
+ const runner = options.runner ?? runCommand;
144
+ const source = await realpath(options.frontier);
145
+ const outputRoot = path.resolve(options.outputRoot ?? defaultProductOutput(source));
146
+ await assertFreshOutput(outputRoot, source);
147
+ const controlHome = path.join(outputRoot, "control-home");
148
+ await mkdir(controlHome, { mode: 0o700 });
149
+ try {
150
+ const diagnosis = await doctorProduct({
151
+ frontier: source,
152
+ ...(options.profileName === undefined ? {} : { profileName: options.profileName }),
153
+ ...(options.requestedTarget === undefined ? {} : { requestedTarget: options.requestedTarget }),
154
+ runner,
155
+ });
156
+ const codexRuntime = diagnosis.public.runtimes.codex;
157
+ const dockerRuntime = diagnosis.public.runtimes.docker;
158
+ if (!diagnosis.public.worker.mission_ready || codexRuntime === null || dockerRuntime === null) {
159
+ throw new Error(diagnosis.public.next_action);
160
+ }
161
+ const staging = path.join(outputRoot, ".profile-staging");
162
+ await mkdir(staging, { mode: 0o700 });
163
+ await stageProfileCapsule({
164
+ profile: diagnosis.profile,
165
+ stagingRoot: staging,
166
+ });
167
+ const bundleRoot = path.join(outputRoot, "mission");
168
+ const draft = await loadProfileDraft(diagnosis.profile);
169
+ const resultContract = await loadProfileResultContract(diagnosis.profile);
170
+ const prepared = await prepareMission({
171
+ draft: options.requestedTarget === undefined
172
+ ? draft
173
+ : { ...draft, target: options.requestedTarget },
174
+ draftRoot: staging,
175
+ sourceRepo: source,
176
+ outputRoot: bundleRoot,
177
+ velaBinary: diagnosis.public.runtimes.vela.binary,
178
+ codexBinary: codexRuntime.binary,
179
+ dockerBinary: dockerRuntime.binary,
180
+ verifierImage: diagnosis.profile.verifier_image,
181
+ verifierPlatform: diagnosis.profile.verifier_platform,
182
+ outputSchema: packageFile("schemas/engine-output.v0.json"),
183
+ permissionProfile: await packagedWorkerProfile(diagnosis.profile),
184
+ targetPacket: {
185
+ target: diagnosis.profile.target,
186
+ schema: diagnosis.profile.target_packet_schema,
187
+ },
188
+ landing: diagnosis.profile.landing,
189
+ profileName: diagnosis.profile.name,
190
+ profileRoot: diagnosis.profile.profile_sha256,
191
+ ...(resultContract === undefined
192
+ ? {}
193
+ : {
194
+ resultContract,
195
+ }),
196
+ runner,
197
+ });
198
+ await rm(staging, { recursive: true, force: true });
199
+ const runRoot = path.join(outputRoot, "run");
200
+ const vela = new VelaClient({
201
+ binary: diagnosis.public.runtimes.vela.binary,
202
+ expectedVersion: prepared.mission.vela_version,
203
+ expectedSha256: prepared.mission.vela_sha256,
204
+ home: path.join(runRoot, "vela-home"),
205
+ runner,
206
+ });
207
+ const engine = new CodexToolsNativeEngine({
208
+ binary: codexRuntime.binary,
209
+ authHome: path.resolve(options.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex")),
210
+ outputSchema: path.join(bundleRoot, "contract", "engine-output.v0.json"),
211
+ permissionProfile: path.join(bundleRoot, prepared.mission.worker.permission_profile_path),
212
+ runner,
213
+ });
214
+ const commonRun = {
215
+ mission: prepared.mission,
216
+ sourceRepo: source,
217
+ runRoot,
218
+ vela,
219
+ engine,
220
+ bundleRoot,
221
+ dockerBinary: dockerRuntime.binary,
222
+ verifierRunner: runner,
223
+ retainWithdrawalCapability: async (context) => {
224
+ await retainWithdrawalCapability({
225
+ ...context,
226
+ velaBinary: diagnosis.public.runtimes.vela.binary,
227
+ });
228
+ },
229
+ };
230
+ const run = options.noLand === true
231
+ ? await runCanopus({ ...commonRun, noLand: true })
232
+ : await runCanopus(commonRun);
233
+ const evidence = await writeEvidenceManifest(run, contentDigest(prepared.mission));
234
+ const publication = options.noLand === true
235
+ ? {
236
+ state: "unchanged_no_land",
237
+ commit: prepared.mission.roots.git_commit,
238
+ tree: prepared.mission.roots.git_tree,
239
+ }
240
+ : await publishVerifiedLanding({
241
+ source,
242
+ landing: run.paths.landing,
243
+ expectedStart: prepared.mission.roots.git_commit,
244
+ expectedFinal: run.record.final_roots.git_commit,
245
+ expectedTree: run.record.final_roots.git_tree,
246
+ home: controlHome,
247
+ runner,
248
+ });
249
+ await rm(controlHome, { recursive: true, force: true });
250
+ return {
251
+ run,
252
+ doctor: diagnosis.public,
253
+ output_root: outputRoot,
254
+ bundle_root: bundleRoot,
255
+ evidence_manifest: evidence.file,
256
+ evidence_root: evidence.root,
257
+ source_publication: publication,
258
+ };
259
+ }
260
+ catch (error) {
261
+ // Preserve bounded failure evidence and the exact diagnostic inputs.
262
+ throw error;
263
+ }
264
+ }
@@ -0,0 +1,18 @@
1
+ import { type CommandRunner } from "../util/command.js";
2
+ export interface RuntimeIdentity {
3
+ binary: string;
4
+ version: string;
5
+ sha256: string;
6
+ }
7
+ export declare function executableNames(name: string, platform: NodeJS.Platform, pathExt: string): string[];
8
+ export declare function findExecutable(name: string, searchPath?: string, options?: {
9
+ platform?: NodeJS.Platform;
10
+ pathExt?: string;
11
+ }): Promise<string>;
12
+ export declare function runtimeIdentity(options: {
13
+ name: string;
14
+ versionArgs?: readonly string[];
15
+ cwd: string;
16
+ home: string;
17
+ runner?: CommandRunner;
18
+ }): Promise<RuntimeIdentity>;