@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,27 @@
1
+ import { type CommandRunner } from "../util/command.js";
2
+ export interface NativeCustodyPreflightResult {
3
+ schema: "canopus.native-custody-preflight.v1";
4
+ ok: true;
5
+ mode: "deterministic_no_model";
6
+ platform: string;
7
+ codex_version: string;
8
+ codex_sha256: string;
9
+ permission_profile_sha256: string;
10
+ verdict: {
11
+ curl_available: true;
12
+ source_auth_readable: false;
13
+ runtime_auth_readable: false;
14
+ sealed_input_readable: false;
15
+ unrelated_repo_readable: false;
16
+ canary_readable: false;
17
+ outside_workspace_writable: false;
18
+ command_network_reachable: false;
19
+ environment_contains_auth: false;
20
+ proc_environ_contains_auth: false;
21
+ };
22
+ }
23
+ export declare function runNativeCustodyPreflight(options: {
24
+ binary: string;
25
+ permissionProfile: string;
26
+ runner?: CommandRunner;
27
+ }): Promise<NativeCustodyPreflightResult>;
@@ -0,0 +1,131 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { chmod, copyFile, mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { assertNativeRuntimeProfile } from "../engines/codex-tools-native.js";
8
+ import { 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
+ export async function runNativeCustodyPreflight(options) {
12
+ if (process.platform !== "darwin" && process.platform !== "linux") {
13
+ throw new Error(`native sandbox preflight does not support ${process.platform}`);
14
+ }
15
+ const runner = options.runner ?? runCommand;
16
+ const binary = await realpath(options.binary);
17
+ const profile = await realpath(options.permissionProfile);
18
+ const [binaryDigest, profileBytes] = await Promise.all([
19
+ sha256RegularFile(binary, MAX_EXECUTABLE_BYTES),
20
+ readBoundedRegularFile(profile, 8 * 1024 * 1024),
21
+ ]);
22
+ const runtime = await mkdtemp(path.join(os.homedir(), ".canopus-native-preflight-"));
23
+ try {
24
+ const codexHome = path.join(runtime, "codex-home");
25
+ const home = path.join(runtime, "home");
26
+ const workspace = path.join(runtime, "workspace");
27
+ const sourceHome = path.join(runtime, "source-auth");
28
+ const sealedInput = path.join(runtime, "sealed-input");
29
+ const unrelated = path.join(runtime, "unrelated-repository");
30
+ await Promise.all([
31
+ mkdir(codexHome, { mode: 0o700 }),
32
+ mkdir(home, { mode: 0o700 }),
33
+ mkdir(workspace, { mode: 0o700 }),
34
+ mkdir(sourceHome, { mode: 0o700 }),
35
+ mkdir(sealedInput, { mode: 0o700 }),
36
+ mkdir(unrelated, { mode: 0o700 }),
37
+ ]);
38
+ const authBytes = Buffer.from(JSON.stringify({
39
+ OPENAI_API_KEY: `preflight-${randomBytes(32).toString("hex")}`,
40
+ }) + "\n");
41
+ const sourceAuth = path.join(sourceHome, "auth.json");
42
+ const runtimeAuth = path.join(codexHome, "auth.json");
43
+ const inputFile = path.join(sealedInput, "packet.json");
44
+ const unrelatedFile = path.join(unrelated, "private.txt");
45
+ const canary = path.join(runtime, "host-secret");
46
+ await Promise.all([
47
+ writeFile(sourceAuth, authBytes, { flag: "wx", mode: 0o600 }),
48
+ writeFile(runtimeAuth, authBytes, { flag: "wx", mode: 0o600 }),
49
+ writeFile(inputFile, "sealed input\n", { flag: "wx", mode: 0o400 }),
50
+ writeFile(unrelatedFile, "unrelated repository\n", { flag: "wx", mode: 0o400 }),
51
+ writeFile(canary, `host-secret-${randomBytes(32).toString("hex")}\n`, {
52
+ flag: "wx",
53
+ mode: 0o400,
54
+ }),
55
+ ]);
56
+ await copyFile(profile, path.join(codexHome, "config.toml"));
57
+ await chmod(path.join(codexHome, "config.toml"), 0o600);
58
+ let runtimeBinary = binary;
59
+ if (process.platform === "linux") {
60
+ const runtimeDirectory = path.join(workspace, ".canopus-runtime");
61
+ await mkdir(runtimeDirectory, { mode: 0o700 });
62
+ runtimeBinary = path.join(runtimeDirectory, "codex");
63
+ await copyFile(binary, runtimeBinary, constants.COPYFILE_EXCL);
64
+ await chmod(runtimeBinary, 0o500);
65
+ if (await sha256RegularFile(runtimeBinary, MAX_EXECUTABLE_BYTES) !== binaryDigest) {
66
+ throw new Error("Codex binary changed while it was staged for the Linux sandbox");
67
+ }
68
+ }
69
+ const environment = {
70
+ ...isolatedEnvironment(home),
71
+ CODEX_HOME: codexHome,
72
+ CANOPUS_AUTH: "canopus-preflight-environment-canary",
73
+ NO_COLOR: "1",
74
+ };
75
+ await assertNativeRuntimeProfile({
76
+ binary: runtimeBinary,
77
+ runner,
78
+ environment,
79
+ cwd: workspace,
80
+ sourceAuth,
81
+ runtimeAuth,
82
+ inaccessibleInput: inputFile,
83
+ unrelatedFile,
84
+ canary,
85
+ outsideWrite: path.join(runtime, "outside-write"),
86
+ timeoutMs: 30_000,
87
+ includeSafeDiagnostics: true,
88
+ });
89
+ const version = await runner({
90
+ argv: [runtimeBinary, "--version"],
91
+ cwd: workspace,
92
+ env: environment,
93
+ timeoutMs: 30_000,
94
+ maxOutputBytes: 4096,
95
+ });
96
+ if (version.exitCode !== 0 || version.stderr.length !== 0) {
97
+ throw new Error("Codex version check failed after native sandbox preflight");
98
+ }
99
+ const codexVersion = version.stdout.toString("utf8").trim();
100
+ if (codexVersion === "" || codexVersion.length > 4096) {
101
+ throw new Error("Codex returned an invalid version after native sandbox preflight");
102
+ }
103
+ if (await sha256RegularFile(runtimeBinary, MAX_EXECUTABLE_BYTES) !== binaryDigest) {
104
+ throw new Error("Codex binary changed during native sandbox preflight");
105
+ }
106
+ return {
107
+ schema: "canopus.native-custody-preflight.v1",
108
+ ok: true,
109
+ mode: "deterministic_no_model",
110
+ platform: `${process.platform}-${process.arch}`,
111
+ codex_version: codexVersion,
112
+ codex_sha256: binaryDigest,
113
+ permission_profile_sha256: sha256Bytes(profileBytes),
114
+ verdict: {
115
+ curl_available: true,
116
+ source_auth_readable: false,
117
+ runtime_auth_readable: false,
118
+ sealed_input_readable: false,
119
+ unrelated_repo_readable: false,
120
+ canary_readable: false,
121
+ outside_workspace_writable: false,
122
+ command_network_reachable: false,
123
+ environment_contains_auth: false,
124
+ proc_environ_contains_auth: false,
125
+ },
126
+ };
127
+ }
128
+ finally {
129
+ await rm(runtime, { recursive: true, force: true });
130
+ }
131
+ }
@@ -0,0 +1,61 @@
1
+ import { type CommandRunner } from "../util/command.js";
2
+ import { type ProductProfile } from "./profile.js";
3
+ import { type RuntimeIdentity } from "./runtime.js";
4
+ export interface ProductDoctorResult {
5
+ schema: "canopus.doctor.v1";
6
+ ok: boolean;
7
+ authority: "read_only_diagnostic";
8
+ frontier: {
9
+ path: string;
10
+ git_commit: string;
11
+ git_tree: string;
12
+ event_log_root: string;
13
+ snapshot_root: string;
14
+ clean: boolean;
15
+ strict_blockers: number;
16
+ };
17
+ offer: {
18
+ target: string;
19
+ rank: number;
20
+ profile: string;
21
+ };
22
+ runtimes: {
23
+ vela: RuntimeIdentity;
24
+ git: RuntimeIdentity;
25
+ codex: RuntimeIdentity | null;
26
+ docker: (RuntimeIdentity & {
27
+ daemon: "ready";
28
+ verifier_image: string;
29
+ verifier_image_id: string;
30
+ }) | null;
31
+ };
32
+ capsule: {
33
+ sha256: string;
34
+ source: "packaged";
35
+ };
36
+ worker: {
37
+ platform: string;
38
+ mission_runtime: "native" | "wsl2_required";
39
+ mission_ready: boolean;
40
+ custody_preflight: "passed" | "wsl2_required";
41
+ custody_mode: "deterministic_no_model" | "not_applicable";
42
+ permission_profile_sha256: string;
43
+ };
44
+ next_action: string;
45
+ }
46
+ export declare function selectProductOffer(offer: Record<string, unknown>, profile: ProductProfile, requestedTarget?: string): {
47
+ target: Record<string, unknown>;
48
+ targetId: string;
49
+ rank: number;
50
+ };
51
+ export declare function resolveProductProfile(offer: Record<string, unknown>, profileName?: string, requestedTarget?: string): Promise<ProductProfile>;
52
+ export declare function doctorProduct(options: {
53
+ frontier: string;
54
+ profileName?: string;
55
+ requestedTarget?: string;
56
+ runner?: CommandRunner;
57
+ platform?: NodeJS.Platform;
58
+ }): Promise<{
59
+ public: ProductDoctorResult;
60
+ profile: ProductProfile;
61
+ }>;
@@ -0,0 +1,278 @@
1
+ import { mkdir, mkdtemp, realpath, rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { objectAt, sha256At, stringAt } from "../contracts/validation.js";
5
+ import { isolatedEnvironment, runCommand } from "../util/command.js";
6
+ import { sha256Bytes } from "../util/canonical.js";
7
+ import { listProductProfiles, loadProductProfile, packagedWorkerProfile, stageProfileCapsule, } from "./profile.js";
8
+ import { runNativeCustodyPreflight } from "./custody.js";
9
+ import { runtimeIdentity } from "./runtime.js";
10
+ import { CANOPUS_VERSION } from "./version.js";
11
+ async function jsonCommand(options) {
12
+ const result = await options.runner({
13
+ argv: options.argv,
14
+ cwd: options.cwd,
15
+ env: isolatedEnvironment(options.home),
16
+ timeoutMs: 120_000,
17
+ maxOutputBytes: 16 * 1024 * 1024,
18
+ });
19
+ if (result.exitCode !== 0) {
20
+ throw new Error(`${options.label} failed: stdout_sha256=${sha256Bytes(result.stdout)}; ` +
21
+ `stderr_sha256=${sha256Bytes(result.stderr)}`);
22
+ }
23
+ return objectAt(JSON.parse(result.stdout.toString("utf8")), options.label);
24
+ }
25
+ function nonnegative(value, at) {
26
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
27
+ throw new Error(`${at} must be a nonnegative integer`);
28
+ }
29
+ return value;
30
+ }
31
+ export function selectProductOffer(offer, profile, requestedTarget) {
32
+ const targets = offer.targets;
33
+ if (!Array.isArray(targets) || targets.length === 0) {
34
+ throw new Error("vela next returned no producer target");
35
+ }
36
+ if (requestedTarget !== undefined && requestedTarget !== profile.target) {
37
+ throw new Error(`registered profile ${profile.name} targets ${profile.target}, not requested target ${requestedTarget}`);
38
+ }
39
+ const candidates = requestedTarget === undefined
40
+ ? [objectAt(targets[0], "vela next.targets[0]")]
41
+ : targets
42
+ .map((value, index) => objectAt(value, `vela next.targets[${index}]`))
43
+ .filter((value) => value.target_id === requestedTarget);
44
+ if (candidates.length !== 1) {
45
+ throw new Error(`requested target ${requestedTarget ?? profile.target} must appear exactly once in the ranked producer offers; ` +
46
+ `observed ${candidates.length}`);
47
+ }
48
+ const target = candidates[0];
49
+ const targetId = stringAt(target.target_id, "vela next target_id", { min: 1, max: 256 });
50
+ if (targetId !== profile.target) {
51
+ throw new Error(`first ranked target is ${targetId}, but registered profile ${profile.name} targets ` +
52
+ `${profile.target}; Canopus will not skip rank 1, so freeze a verifier profile for ${targetId} before running`);
53
+ }
54
+ return {
55
+ target,
56
+ targetId,
57
+ rank: nonnegative(target.rank, "vela next target rank"),
58
+ };
59
+ }
60
+ export async function resolveProductProfile(offer, profileName, requestedTarget) {
61
+ if (profileName !== undefined)
62
+ return loadProductProfile(profileName);
63
+ const targets = offer.targets;
64
+ if (!Array.isArray(targets) || targets.length === 0) {
65
+ throw new Error("vela next returned no producer target");
66
+ }
67
+ const selectedOffer = requestedTarget === undefined
68
+ ? objectAt(targets[0], "vela next.targets[0]")
69
+ : targets
70
+ .map((value, index) => objectAt(value, `vela next.targets[${index}]`))
71
+ .find((value) => value.target_id === requestedTarget);
72
+ if (selectedOffer === undefined) {
73
+ throw new Error(`requested target ${requestedTarget} is not a current producer offer`);
74
+ }
75
+ const target = stringAt(selectedOffer.target_id, "vela next target_id", { min: 1, max: 256 });
76
+ const matches = [];
77
+ for (const name of await listProductProfiles()) {
78
+ try {
79
+ const candidate = await loadProductProfile(name);
80
+ if (candidate.target === target)
81
+ matches.push(candidate);
82
+ }
83
+ catch (error) {
84
+ if (!(error instanceof Error) || !/replay-only/u.test(error.message))
85
+ throw error;
86
+ }
87
+ }
88
+ if (matches.length === 0) {
89
+ throw new Error(`no runnable profile is registered for producer target ${target}`);
90
+ }
91
+ if (matches.length !== 1) {
92
+ throw new Error(`multiple runnable profiles are registered for producer target ${target}: ` +
93
+ `${matches.map((profile) => profile.name).sort().join(", ")}; select one with --profile`);
94
+ }
95
+ return matches[0];
96
+ }
97
+ export async function doctorProduct(options) {
98
+ const runner = options.runner ?? runCommand;
99
+ const platform = options.platform ?? process.platform;
100
+ const frontier = await realpath(options.frontier);
101
+ const runtime = await mkdtemp(path.join(os.tmpdir(), "canopus-doctor-"));
102
+ try {
103
+ const [vela, git] = await Promise.all([
104
+ runtimeIdentity({ name: "vela", cwd: frontier, home: runtime, runner }),
105
+ runtimeIdentity({ name: "git", cwd: frontier, home: runtime, runner }),
106
+ ]);
107
+ if (vela.version !== "vela 0.901.0") {
108
+ throw new Error(`Canopus ${CANOPUS_VERSION} requires vela 0.901.0, observed ${vela.version}`);
109
+ }
110
+ const [status, offer, gitStatus] = await Promise.all([
111
+ jsonCommand({ runner, argv: [vela.binary, "status", ".", "--json"], cwd: frontier, home: runtime, label: "vela status" }),
112
+ jsonCommand({ runner, argv: [vela.binary, "next", ".", "--limit", "128", "--json"], cwd: frontier, home: runtime, label: "vela next" }),
113
+ runner({ argv: [git.binary, "status", "--porcelain=v1", "--untracked-files=all"], cwd: frontier, env: isolatedEnvironment(runtime), timeoutMs: 30_000, maxOutputBytes: 8 * 1024 * 1024 }),
114
+ ]);
115
+ if (gitStatus.exitCode !== 0 || gitStatus.stderr.length !== 0)
116
+ throw new Error("git status failed");
117
+ const clean = gitStatus.stdout.length === 0;
118
+ if (!clean)
119
+ throw new Error("frontier checkout must be clean before Canopus runs");
120
+ if (status.schema !== "vela.status.v1" || offer.schema !== "vela.offer.v1") {
121
+ throw new Error("frontier did not return the Vela 0.9 compact contracts");
122
+ }
123
+ const profile = await resolveProductProfile(offer, options.profileName, options.requestedTarget);
124
+ const selected = selectProductOffer(offer, profile, options.requestedTarget);
125
+ const roots = objectAt(status.roots, "vela status.roots");
126
+ const gitState = objectAt(status.git, "vela status.git");
127
+ const integrity = objectAt(status.integrity, "vela status.integrity");
128
+ const staging = path.join(runtime, "capsule-build");
129
+ await mkdir(staging, { mode: 0o700 });
130
+ const capsule = await stageProfileCapsule({ profile, stagingRoot: staging });
131
+ if (platform === "win32") {
132
+ return {
133
+ profile,
134
+ public: {
135
+ schema: "canopus.doctor.v1",
136
+ ok: true,
137
+ authority: "read_only_diagnostic",
138
+ frontier: {
139
+ path: frontier,
140
+ git_commit: stringAt(gitState.commit, "vela status.git.commit", { min: 40, max: 64 }),
141
+ git_tree: stringAt(gitState.tree, "vela status.git.tree", { min: 40, max: 64 }),
142
+ event_log_root: sha256At(roots.event_log, "vela status.roots.event_log"),
143
+ snapshot_root: sha256At(roots.snapshot, "vela status.roots.snapshot"),
144
+ clean,
145
+ strict_blockers: nonnegative(integrity.blocker_count, "vela status.integrity.blocker_count"),
146
+ },
147
+ offer: {
148
+ target: selected.targetId,
149
+ rank: selected.rank,
150
+ profile: profile.name,
151
+ },
152
+ runtimes: { vela, git, codex: null, docker: null },
153
+ capsule: { sha256: profile.capsule_sha256, source: capsule.source },
154
+ worker: {
155
+ platform: `${platform}-${process.arch}`,
156
+ mission_runtime: "wsl2_required",
157
+ mission_ready: false,
158
+ custody_preflight: "wsl2_required",
159
+ custody_mode: "not_applicable",
160
+ permission_profile_sha256: profile.permission_profile_sha256,
161
+ },
162
+ next_action: "Open WSL2, enter this frontier through its Linux path, and rerun canopus doctor there; native Windows supports inspect and replay only.",
163
+ },
164
+ };
165
+ }
166
+ if (platform !== "darwin" && platform !== "linux") {
167
+ throw new Error(`Canopus doctor does not support ${platform}`);
168
+ }
169
+ const [codex, docker] = await Promise.all([
170
+ runtimeIdentity({ name: "codex", cwd: frontier, home: runtime, runner }),
171
+ runtimeIdentity({ name: "docker", cwd: frontier, home: runtime, runner }),
172
+ ]);
173
+ if (codex.version !== "codex-cli 0.144.6") {
174
+ throw new Error(`registered worker requires codex-cli 0.144.6, observed ${codex.version}`);
175
+ }
176
+ const daemon = await runner({
177
+ argv: [docker.binary, "info", "--format={{.ServerVersion}}"],
178
+ cwd: frontier,
179
+ env: isolatedEnvironment(runtime),
180
+ timeoutMs: 30_000,
181
+ maxOutputBytes: 64 * 1024,
182
+ });
183
+ if (daemon.exitCode !== 0 || daemon.stdout.toString("utf8").trim() === "") {
184
+ throw new Error("Docker daemon is not ready");
185
+ }
186
+ const [image, repoDigests] = await Promise.all([
187
+ runner({
188
+ argv: [docker.binary, "image", "inspect", "--format={{.Id}}", profile.verifier_image],
189
+ cwd: frontier,
190
+ env: isolatedEnvironment(runtime),
191
+ timeoutMs: 30_000,
192
+ maxOutputBytes: 64 * 1024,
193
+ }),
194
+ runner({
195
+ argv: [docker.binary, "image", "inspect", "--format={{json .RepoDigests}}", profile.verifier_image],
196
+ cwd: frontier,
197
+ env: isolatedEnvironment(runtime),
198
+ timeoutMs: 30_000,
199
+ maxOutputBytes: 64 * 1024,
200
+ }),
201
+ ]);
202
+ if (image.exitCode !== 0 || repoDigests.exitCode !== 0) {
203
+ throw new Error("registered verifier image is unavailable; run " +
204
+ `docker pull --platform ${profile.verifier_platform} ${profile.verifier_image}`);
205
+ }
206
+ if (image.stderr.length !== 0 || repoDigests.stderr.length !== 0) {
207
+ throw new Error("registered verifier image inspection produced unexpected stderr");
208
+ }
209
+ const imageId = sha256At(image.stdout.toString("utf8").trim(), "Docker image ID");
210
+ const observedDigests = JSON.parse(repoDigests.stdout.toString("utf8"));
211
+ if (!Array.isArray(observedDigests) || !observedDigests.includes(profile.verifier_image)) {
212
+ throw new Error("registered verifier image repository digest drifted");
213
+ }
214
+ const permissionProfile = await packagedWorkerProfile(profile);
215
+ const custody = await runNativeCustodyPreflight({
216
+ binary: codex.binary,
217
+ permissionProfile,
218
+ runner,
219
+ });
220
+ if (custody.codex_version !== codex.version || custody.codex_sha256 !== codex.sha256) {
221
+ throw new Error("native custody preflight and discovered Codex identity disagree");
222
+ }
223
+ if (custody.permission_profile_sha256 !== profile.permission_profile_sha256) {
224
+ throw new Error("native custody preflight and registered worker profile disagree");
225
+ }
226
+ return {
227
+ profile,
228
+ public: {
229
+ schema: "canopus.doctor.v1",
230
+ ok: true,
231
+ authority: "read_only_diagnostic",
232
+ frontier: {
233
+ path: frontier,
234
+ git_commit: stringAt(gitState.commit, "vela status.git.commit", { min: 40, max: 64 }),
235
+ git_tree: stringAt(gitState.tree, "vela status.git.tree", { min: 40, max: 64 }),
236
+ event_log_root: sha256At(roots.event_log, "vela status.roots.event_log"),
237
+ snapshot_root: sha256At(roots.snapshot, "vela status.roots.snapshot"),
238
+ clean,
239
+ strict_blockers: nonnegative(integrity.blocker_count, "vela status.integrity.blocker_count"),
240
+ },
241
+ offer: {
242
+ target: selected.targetId,
243
+ rank: selected.rank,
244
+ profile: profile.name,
245
+ },
246
+ runtimes: {
247
+ vela,
248
+ codex,
249
+ git,
250
+ docker: {
251
+ ...docker,
252
+ daemon: "ready",
253
+ verifier_image: profile.verifier_image,
254
+ verifier_image_id: imageId,
255
+ },
256
+ },
257
+ capsule: {
258
+ sha256: profile.capsule_sha256,
259
+ source: capsule.source,
260
+ },
261
+ worker: {
262
+ platform: custody.platform,
263
+ mission_runtime: "native",
264
+ mission_ready: true,
265
+ custody_preflight: "passed",
266
+ custody_mode: custody.mode,
267
+ permission_profile_sha256: custody.permission_profile_sha256,
268
+ },
269
+ next_action: options.requestedTarget === undefined
270
+ ? `canopus run ${frontier} --first`
271
+ : `canopus run ${frontier} --target ${selected.targetId}`,
272
+ },
273
+ };
274
+ }
275
+ finally {
276
+ await rm(runtime, { recursive: true, force: true });
277
+ }
278
+ }
@@ -0,0 +1,21 @@
1
+ import { listProductProfiles, type ProductPlatform } from "./profile.js";
2
+ export interface ProfileValidation {
3
+ schema: "canopus.profile-validation.v1";
4
+ name: string;
5
+ profile_sha256: string;
6
+ draft_sha256: string;
7
+ result_contract_sha256: string | null;
8
+ platforms: Record<ProductPlatform, {
9
+ worker_profile_sha256: string;
10
+ verifier_capsule_sha256: string;
11
+ verifier_image: string;
12
+ verifier_platform: string;
13
+ }>;
14
+ }
15
+ export declare function validateProductProfile(name: string): Promise<ProfileValidation>;
16
+ export declare function packProductProfile(name: string, outputRoot: string): Promise<{
17
+ manifest: string;
18
+ root: string;
19
+ files: number;
20
+ }>;
21
+ export { listProductProfiles };
@@ -0,0 +1,78 @@
1
+ import { constants } from "node:fs";
2
+ import { copyFile, mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { canonicalJson, sha256Bytes } from "../util/canonical.js";
5
+ import { readBoundedRegularFile } from "../util/files.js";
6
+ import { SUPPORTED_PRODUCT_PLATFORMS, listProductProfiles, loadProductProfile, loadProfileDraft, loadProfileResultContract, packagedProfileResource, } from "./profile.js";
7
+ async function exactResource(relative, expected, label) {
8
+ const file = await packagedProfileResource(relative, label);
9
+ const bytes = await readBoundedRegularFile(file, 268_435_456);
10
+ const observed = sha256Bytes(bytes);
11
+ if (observed !== expected)
12
+ throw new Error(`${label} root drifted: expected ${expected}, observed ${observed}`);
13
+ return { path: relative, sha256: observed, size: bytes.length };
14
+ }
15
+ export async function validateProductProfile(name) {
16
+ const profiles = await Promise.all(SUPPORTED_PRODUCT_PLATFORMS.map((platform) => loadProductProfile(name, { platform })));
17
+ const canonical = profiles[0];
18
+ if (canonical === undefined)
19
+ throw new Error("profile has no supported platform capsules");
20
+ await loadProfileDraft(canonical);
21
+ await loadProfileResultContract(canonical);
22
+ const profileFile = await packagedProfileResource(`profiles/${name}.json`, "profile");
23
+ const profileBytes = await readBoundedRegularFile(profileFile, 1024 * 1024);
24
+ const platforms = Object.fromEntries(await Promise.all(profiles.map(async (profile) => {
25
+ await exactResource(profile.permission_profile, profile.permission_profile_sha256, `${profile.platform} worker profile`);
26
+ await exactResource(profile.capsule_binary, profile.capsule_sha256, `${profile.platform} verifier capsule`);
27
+ return [profile.platform, {
28
+ worker_profile_sha256: profile.permission_profile_sha256,
29
+ verifier_capsule_sha256: profile.capsule_sha256,
30
+ verifier_image: profile.verifier_image,
31
+ verifier_platform: profile.verifier_platform,
32
+ }];
33
+ })));
34
+ return {
35
+ schema: "canopus.profile-validation.v1",
36
+ name,
37
+ profile_sha256: sha256Bytes(profileBytes),
38
+ draft_sha256: canonical.draft_sha256,
39
+ result_contract_sha256: canonical.result_contract_sha256 ?? null,
40
+ platforms,
41
+ };
42
+ }
43
+ export async function packProductProfile(name, outputRoot) {
44
+ const validation = await validateProductProfile(name);
45
+ const profile = await loadProductProfile(name, { platform: SUPPORTED_PRODUCT_PLATFORMS[0] });
46
+ const resources = new Map();
47
+ const add = async (relative, expected, label) => {
48
+ resources.set(relative, await exactResource(relative, expected, label));
49
+ };
50
+ await add(`profiles/${name}.json`, validation.profile_sha256, "profile");
51
+ await add(profile.draft, profile.draft_sha256, "profile draft");
52
+ if (profile.result_contract !== undefined && profile.result_contract_sha256 !== undefined) {
53
+ await add(profile.result_contract, profile.result_contract_sha256, "profile result contract");
54
+ }
55
+ for (const platform of SUPPORTED_PRODUCT_PLATFORMS) {
56
+ const capsule = profile.platforms[platform];
57
+ await add(capsule.worker_profile, capsule.worker_profile_sha256, `${platform} worker profile`);
58
+ await add(capsule.verifier_capsule, capsule.verifier_capsule_sha256, `${platform} verifier capsule`);
59
+ }
60
+ await mkdir(outputRoot, { recursive: false, mode: 0o755 });
61
+ for (const resource of [...resources.values()].sort((left, right) => left.path.localeCompare(right.path))) {
62
+ const source = await packagedProfileResource(resource.path, "profile pack resource");
63
+ const destination = path.join(outputRoot, resource.path);
64
+ await mkdir(path.dirname(destination), { recursive: true, mode: 0o755 });
65
+ await copyFile(source, destination, constants.COPYFILE_EXCL);
66
+ }
67
+ const manifestValue = {
68
+ schema: "canopus.profile-pack.v1",
69
+ profile: name,
70
+ profile_sha256: validation.profile_sha256,
71
+ files: [...resources.values()].sort((left, right) => left.path.localeCompare(right.path)),
72
+ };
73
+ const manifestBytes = canonicalJson(manifestValue);
74
+ const manifest = path.join(outputRoot, "profile-pack.json");
75
+ await writeFile(manifest, manifestBytes, { encoding: "utf8", flag: "wx", mode: 0o644 });
76
+ return { manifest, root: sha256Bytes(manifestBytes), files: resources.size };
77
+ }
78
+ export { listProductProfiles };
@@ -0,0 +1,67 @@
1
+ import { type LandingSpec, type PositiveResultContractV1 } from "../contracts/mission.js";
2
+ export declare const PROFILE_V1_SCHEMA: "canopus.profile.v1";
3
+ export declare const PROFILE_SCHEMA: "canopus.profile.v2";
4
+ export declare const SUPPORTED_PRODUCT_PLATFORMS: readonly ["darwin-arm64", "linux-x86_64"];
5
+ export type ProductPlatform = (typeof SUPPORTED_PRODUCT_PLATFORMS)[number];
6
+ export type VerifierPlatform = "linux/amd64" | "linux/arm64";
7
+ export interface PlatformCapsule {
8
+ worker_profile: string;
9
+ worker_profile_sha256: string;
10
+ verifier_capsule: string;
11
+ verifier_capsule_sha256: string;
12
+ verifier_image: string;
13
+ verifier_platform: VerifierPlatform;
14
+ }
15
+ export interface ProductProfile {
16
+ schema: typeof PROFILE_SCHEMA;
17
+ name: string;
18
+ target: string;
19
+ target_packet_schema: string;
20
+ draft: string;
21
+ draft_sha256: string;
22
+ objective_sha256: string;
23
+ completion_condition_sha256: string;
24
+ allowed_artifacts_sha256: string;
25
+ budgets_sha256: string;
26
+ replay_argv_sha256: string;
27
+ landing: LandingSpec;
28
+ profile_sha256: string;
29
+ result_contract?: string;
30
+ result_contract_sha256?: string;
31
+ platforms: Record<ProductPlatform, PlatformCapsule>;
32
+ platform: ProductPlatform;
33
+ capsule_binary: string;
34
+ capsule_sha256: string;
35
+ permission_profile: string;
36
+ permission_profile_sha256: string;
37
+ verifier_image: string;
38
+ verifier_platform: VerifierPlatform;
39
+ }
40
+ export interface LegacyProductProfile {
41
+ schema: typeof PROFILE_V1_SCHEMA;
42
+ name: string;
43
+ target: string;
44
+ }
45
+ export declare function productPackageRoot(): string;
46
+ export declare function currentProductPlatform(): ProductPlatform;
47
+ export declare function verifierImageAt(value: unknown, at?: string): string;
48
+ export declare function listProductProfiles(): Promise<string[]>;
49
+ export declare function loadProductProfile(name: string, options?: {
50
+ allowLegacy?: false;
51
+ platform?: ProductPlatform;
52
+ }): Promise<ProductProfile>;
53
+ export declare function loadProductProfile(name: string, options: {
54
+ allowLegacy: true;
55
+ platform?: ProductPlatform;
56
+ }): Promise<ProductProfile | LegacyProductProfile>;
57
+ export declare function loadProfileResultContract(profile: ProductProfile): Promise<PositiveResultContractV1 | undefined>;
58
+ export declare function packagedProfileResource(relative: string, label: string): Promise<string>;
59
+ export declare function loadProfileDraft(profile: ProductProfile): Promise<unknown>;
60
+ export declare function stageProfileCapsule(options: {
61
+ profile: ProductProfile;
62
+ stagingRoot: string;
63
+ }): Promise<{
64
+ capsule: string;
65
+ source: "packaged";
66
+ }>;
67
+ export declare function packagedWorkerProfile(profile: ProductProfile): Promise<string>;