@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,503 @@
1
+ #!/usr/bin/env node
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { readdir, stat } from "node:fs/promises";
7
+ import { parseMission } from "./contracts/mission.js";
8
+ import { CodexExecEngine } from "./engines/codex-exec.js";
9
+ import { CodexToolsNativeEngine } from "./engines/codex-tools-native.js";
10
+ import { prepareMission, validateMissionBundle } from "./mission/prepare.js";
11
+ import { parseDiagnosticRunRecord, projectDiagnosticRun } from "./projection/diagnostic.js";
12
+ import { parseFailureRecord, projectFailure } from "./projection/failure.js";
13
+ import { parseRunRecord, projectRun } from "./projection/run.js";
14
+ import { doctorProduct } from "./product/doctor.js";
15
+ import { replayProduct } from "./product/replay.js";
16
+ import { runProduct } from "./product/run.js";
17
+ import { withdrawProduct } from "./product/withdraw.js";
18
+ import { listProductProfiles, packProductProfile, validateProductProfile, } from "./product/profile-bundle.js";
19
+ import { loadProductProfile } from "./product/profile.js";
20
+ import { runCanopus } from "./run.js";
21
+ import { readBoundedRegularFile } from "./util/files.js";
22
+ import { VelaClient } from "./vela/cli.js";
23
+ import { withdrawalCapabilityStatus } from "./capability/withdrawal.js";
24
+ function usage() {
25
+ return `Canopus — bounded Vela research harness
26
+
27
+ Primary workflow:
28
+ canopus doctor [frontier]
29
+ canopus run [frontier] [--first | --target <id>] [--profile <name>] \\
30
+ [--output <dir>] [--no-land]
31
+ canopus inspect [run.json | latest]
32
+ canopus replay <run.json>
33
+ canopus withdraw [frontier] [--run <run.json|latest>] --reason <text>
34
+
35
+ Mission v1 prepare/validate remains available under advanced help.
36
+
37
+ Canopus may land a Receipt v1 as an agent after verifier success. It cannot
38
+ sign, accept, or make a human scientific decision.`;
39
+ }
40
+ function missionUsage() {
41
+ return `Usage:
42
+ canopus mission prepare <draft.json> --source <clean-repo> --output <new-bundle> \\
43
+ --vela <binary> --codex <binary> --verifier-image <image> [--docker <binary>]
44
+ canopus mission validate <bundle/mission.json>
45
+
46
+ prepare derives exact Git, Vela, packet, native permission-profile,
47
+ verifier-image, Codex, capsule, and strict-baseline roots. validate is read-only
48
+ and checks the closed contract and portable bundle bytes.`;
49
+ }
50
+ function profileUsage() {
51
+ return `Usage:
52
+ canopus profile list
53
+ canopus profile show <name>
54
+ canopus profile validate <name>
55
+ canopus profile pack <name> --output <new-directory>
56
+
57
+ Profiles are closed, content-addressed producer contracts. Version 1 profiles
58
+ remain replay-only; new tool-using missions require a validated version 2 profile.`;
59
+ }
60
+ function runUsage() {
61
+ return `Usage:
62
+ canopus run [frontier] [--first | --target <id>] [--profile <name>] \\
63
+ [--output <dir>] [--no-land]
64
+
65
+ Discovers and binds Vela, Codex, Git, Docker, the exact frontier roots, and the
66
+ registered verifier profile. --no-land runs the worker and verifier in disposable
67
+ clones and leaves the source frontier unchanged.`;
68
+ }
69
+ function inspectUsage() {
70
+ return `Usage:
71
+ canopus inspect [run.json | failure.json | latest]
72
+
73
+ Projects the newest completed or safely stopped non-authoritative run record
74
+ without mutating Vela. A failed run never implies that landing was unchanged;
75
+ inspect reports whether retained landing-recovery evidence is required.`;
76
+ }
77
+ function doctorUsage() {
78
+ return `Usage:
79
+ canopus doctor [frontier]
80
+
81
+ Checks the compact Vela contract, exact runtimes, first offer, registered
82
+ profile, packaged capsule root, real native custody boundary, and verifier
83
+ isolation prerequisites.`;
84
+ }
85
+ function replayUsage() {
86
+ return `Usage:
87
+ canopus replay <run.json>
88
+
89
+ Re-runs the frozen verifier over the content-addressed candidate without a
90
+ model call, Vela mutation, network, or authority action.`;
91
+ }
92
+ function withdrawUsage() {
93
+ return `Usage:
94
+ canopus withdraw [frontier] [--run <run.json|latest>] --reason <text>
95
+
96
+ Uses only the retained proposal-scoped producer capability. The operation runs
97
+ in a disposable exact-head clone, verifies strict and clean-clone replay, then
98
+ fast-forwards the clean source. It never mounts a worker or human key.`;
99
+ }
100
+ function isHelp(value) {
101
+ return value === "--help" || value === "-h" || value === "help";
102
+ }
103
+ async function jsonFile(file) {
104
+ const bytes = await readBoundedRegularFile(path.resolve(file), 8 * 1024 * 1024);
105
+ return JSON.parse(bytes.toString("utf8"));
106
+ }
107
+ function options(args, allowed) {
108
+ const result = new Map();
109
+ const allow = new Set(allowed);
110
+ for (let index = 0; index < args.length; index += 2) {
111
+ const key = args[index];
112
+ const value = args[index + 1];
113
+ if (key === undefined || value === undefined || !allow.has(key) || result.has(key)) {
114
+ throw new Error(`invalid or duplicate option near ${key ?? "end of arguments"}`);
115
+ }
116
+ result.set(key, value);
117
+ }
118
+ return result;
119
+ }
120
+ function productOptions(args, valueOptions, booleanOptions) {
121
+ const valueAllow = new Set(valueOptions);
122
+ const flagAllow = new Set(booleanOptions);
123
+ const positional = [];
124
+ const values = new Map();
125
+ const flags = new Set();
126
+ for (let index = 0; index < args.length; index += 1) {
127
+ const item = args[index];
128
+ if (!item.startsWith("--")) {
129
+ positional.push(item);
130
+ continue;
131
+ }
132
+ if (flagAllow.has(item)) {
133
+ if (flags.has(item))
134
+ throw new Error(`duplicate option ${item}`);
135
+ flags.add(item);
136
+ continue;
137
+ }
138
+ if (!valueAllow.has(item) || values.has(item)) {
139
+ throw new Error(`unknown or duplicate option ${item}`);
140
+ }
141
+ const value = args[index + 1];
142
+ if (value === undefined || value.startsWith("--"))
143
+ throw new Error(`${item} requires a value`);
144
+ values.set(item, value);
145
+ index += 1;
146
+ }
147
+ return { positional, values, flags };
148
+ }
149
+ function required(values, key) {
150
+ const value = values.get(key);
151
+ if (value === undefined || value === "")
152
+ throw new Error(`${key} is required`);
153
+ return value;
154
+ }
155
+ function authHome(values) {
156
+ return path.resolve(values.get("--codex-home") ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"));
157
+ }
158
+ function packagedOutputSchema() {
159
+ return fileURLToPath(new URL("../../schemas/engine-output.v0.json", import.meta.url));
160
+ }
161
+ function packagedNativeWorkerProfile() {
162
+ const file = process.platform === "linux" ? "config-linux.toml" : "config.toml";
163
+ return fileURLToPath(new URL(`../../runtime/native-worker/${file}`, import.meta.url));
164
+ }
165
+ async function missionCommand(args) {
166
+ const [subcommand, file, ...rest] = args;
167
+ if (subcommand === undefined || isHelp(subcommand)) {
168
+ process.stdout.write(`${missionUsage()}\n`);
169
+ return;
170
+ }
171
+ if (isHelp(file)) {
172
+ process.stdout.write(`${missionUsage()}\n`);
173
+ return;
174
+ }
175
+ if (file === undefined)
176
+ throw new Error(`mission ${subcommand} requires a JSON file`);
177
+ if (subcommand === "validate") {
178
+ if (rest.length !== 0)
179
+ throw new Error("mission validate accepts only one mission file");
180
+ const mission = parseMission(await jsonFile(file));
181
+ if (mission.schema === "canopus.mission.v1") {
182
+ await validateMissionBundle(mission, path.dirname(path.resolve(file)));
183
+ }
184
+ process.stdout.write(`${JSON.stringify({ ok: true, command: "mission validate", mission_id: mission.id, schema: mission.schema })}\n`);
185
+ return;
186
+ }
187
+ if (subcommand !== "prepare")
188
+ throw new Error(`unknown mission command ${subcommand}`);
189
+ const values = options(rest, [
190
+ "--source",
191
+ "--output",
192
+ "--vela",
193
+ "--codex",
194
+ "--verifier-image",
195
+ "--docker",
196
+ ]);
197
+ const prepared = await prepareMission({
198
+ draft: await jsonFile(file),
199
+ draftRoot: path.dirname(path.resolve(file)),
200
+ sourceRepo: path.resolve(required(values, "--source")),
201
+ outputRoot: path.resolve(required(values, "--output")),
202
+ velaBinary: path.resolve(required(values, "--vela")),
203
+ codexBinary: path.resolve(required(values, "--codex")),
204
+ dockerBinary: values.get("--docker") ?? "docker",
205
+ verifierImage: required(values, "--verifier-image"),
206
+ outputSchema: packagedOutputSchema(),
207
+ permissionProfile: packagedNativeWorkerProfile(),
208
+ });
209
+ process.stdout.write(`${JSON.stringify({
210
+ ok: true,
211
+ command: "mission prepare",
212
+ mission_id: prepared.mission.id,
213
+ target: prepared.mission.target,
214
+ mission: prepared.missionPath,
215
+ manifest: prepared.manifestPath,
216
+ })}\n`);
217
+ }
218
+ async function profileCommand(args) {
219
+ const [subcommand, name, ...rest] = args;
220
+ if (subcommand === undefined || isHelp(subcommand) || isHelp(name)) {
221
+ process.stdout.write(`${profileUsage()}\n`);
222
+ return;
223
+ }
224
+ if (subcommand === "list") {
225
+ if (name !== undefined || rest.length !== 0)
226
+ throw new Error("profile list accepts no arguments");
227
+ process.stdout.write(`${JSON.stringify({
228
+ ok: true,
229
+ command: "profile list",
230
+ profiles: await listProductProfiles(),
231
+ })}\n`);
232
+ return;
233
+ }
234
+ if (name === undefined)
235
+ throw new Error(`profile ${subcommand} requires a profile name`);
236
+ if (subcommand === "show") {
237
+ if (rest.length !== 0)
238
+ throw new Error("profile show accepts exactly one profile name");
239
+ const profile = await loadProductProfile(name);
240
+ process.stdout.write(`${JSON.stringify({ ok: true, command: "profile show", profile })}\n`);
241
+ return;
242
+ }
243
+ if (subcommand === "validate") {
244
+ if (rest.length !== 0)
245
+ throw new Error("profile validate accepts exactly one profile name");
246
+ process.stdout.write(`${JSON.stringify({
247
+ ok: true,
248
+ command: "profile validate",
249
+ validation: await validateProductProfile(name),
250
+ })}\n`);
251
+ return;
252
+ }
253
+ if (subcommand === "pack") {
254
+ const values = options(rest, ["--output"]);
255
+ const result = await packProductProfile(name, path.resolve(required(values, "--output")));
256
+ process.stdout.write(`${JSON.stringify({ ok: true, command: "profile pack", ...result })}\n`);
257
+ return;
258
+ }
259
+ throw new Error(`unknown profile command ${subcommand}`);
260
+ }
261
+ async function runMission(file, rest) {
262
+ const values = options(rest, [
263
+ "--source",
264
+ "--run-root",
265
+ "--vela",
266
+ "--docker",
267
+ "--codex",
268
+ "--codex-version",
269
+ "--codex-sha256",
270
+ "--model",
271
+ "--codex-home",
272
+ ]);
273
+ const mission = parseMission(await jsonFile(file));
274
+ const sourceRepo = path.resolve(required(values, "--source"));
275
+ const runRoot = path.resolve(required(values, "--run-root"));
276
+ const velaBinary = path.resolve(required(values, "--vela"));
277
+ const outputSchema = mission.schema === "canopus.mission.v1"
278
+ ? path.join(path.dirname(path.resolve(file)), "contract", "engine-output.v0.json")
279
+ : packagedOutputSchema();
280
+ const vela = new VelaClient({
281
+ binary: velaBinary,
282
+ expectedVersion: mission.vela_version,
283
+ expectedSha256: mission.vela_sha256,
284
+ home: path.join(runRoot, "vela-home"),
285
+ });
286
+ const engine = mission.schema === "canopus.mission.v1"
287
+ ? new CodexToolsNativeEngine({
288
+ binary: path.resolve(required(values, "--codex")),
289
+ authHome: authHome(values),
290
+ outputSchema,
291
+ permissionProfile: path.join(path.dirname(path.resolve(file)), mission.worker.permission_profile_path),
292
+ })
293
+ : new CodexExecEngine({
294
+ binary: path.resolve(required(values, "--codex")),
295
+ expectedSha256: required(values, "--codex-sha256"),
296
+ expectedVersion: required(values, "--codex-version"),
297
+ model: required(values, "--model"),
298
+ authHome: authHome(values),
299
+ outputSchema,
300
+ });
301
+ const result = await runCanopus({
302
+ mission,
303
+ sourceRepo,
304
+ runRoot,
305
+ vela,
306
+ engine,
307
+ bundleRoot: path.dirname(path.resolve(file)),
308
+ dockerBinary: values.get("--docker") ?? "docker",
309
+ });
310
+ process.stdout.write(`${JSON.stringify({
311
+ ok: true,
312
+ command: "run",
313
+ run_id: result.record.run_id,
314
+ candidate_digest: result.record.candidate.digest,
315
+ receipt_root: result.record.landing.receipt_root,
316
+ route: result.record.landing.route,
317
+ accepted_event_delta: result.record.landing.accepted_event_delta,
318
+ clean_clone_reproduced: result.record.reproduction.matched,
319
+ run_file: path.join(result.paths.root, "run.json"),
320
+ })}\n`);
321
+ }
322
+ async function doctorCommand(args) {
323
+ const parsed = productOptions(args, [], []);
324
+ if (parsed.positional.length > 1)
325
+ throw new Error("doctor accepts at most one frontier");
326
+ const result = await doctorProduct({ frontier: path.resolve(parsed.positional[0] ?? ".") });
327
+ process.stdout.write(`${JSON.stringify(result.public)}\n`);
328
+ }
329
+ async function productRunCommand(args) {
330
+ const parsed = productOptions(args, ["--target", "--profile", "--output", "--codex-home"], ["--first", "--no-land"]);
331
+ if (parsed.positional.length > 1)
332
+ throw new Error("run accepts at most one frontier");
333
+ if (parsed.flags.has("--first") && parsed.values.has("--target")) {
334
+ throw new Error("--first and --target are mutually exclusive");
335
+ }
336
+ const frontier = path.resolve(parsed.positional[0] ?? ".");
337
+ const requested = parsed.values.get("--target");
338
+ const profileName = parsed.values.get("--profile");
339
+ const outputRoot = parsed.values.get("--output");
340
+ const codexHome = parsed.values.get("--codex-home");
341
+ const result = await runProduct({
342
+ frontier,
343
+ ...(profileName === undefined ? {} : { profileName }),
344
+ ...(requested === undefined ? {} : { requestedTarget: requested }),
345
+ ...(outputRoot === undefined ? {} : { outputRoot: path.resolve(outputRoot) }),
346
+ ...(codexHome === undefined ? {} : { codexHome: path.resolve(codexHome) }),
347
+ noLand: parsed.flags.has("--no-land"),
348
+ });
349
+ const landing = result.run.record.landing;
350
+ process.stdout.write(`${JSON.stringify({
351
+ ok: true,
352
+ command: "run",
353
+ mode: landing === null ? "no_land" : "land",
354
+ run_id: result.run.record.run_id,
355
+ target: result.run.record.mission.target,
356
+ candidate_digest: result.run.record.candidate.digest,
357
+ verifier_status: result.run.record.verifier.status,
358
+ observed_tokens: result.run.record.budget.observed_tokens,
359
+ receipt_root: landing?.receipt_root ?? null,
360
+ proposal_id: landing?.proposal_id ?? null,
361
+ route: landing?.route ?? null,
362
+ accepted_event_delta: landing?.accepted_event_delta ?? null,
363
+ clean_clone_reproduced: result.run.record.reproduction.matched,
364
+ evidence_root: result.evidence_root,
365
+ source_publication: result.source_publication,
366
+ run_file: path.join(result.run.paths.root, "run.json"),
367
+ })}\n`);
368
+ }
369
+ async function latestRunFile() {
370
+ const root = path.join(os.homedir(), ".canopus", "runs");
371
+ const entries = await readdir(root, { recursive: true });
372
+ const candidates = entries
373
+ .filter((entry) => entry.endsWith(`${path.sep}run${path.sep}run.json`) || entry === path.join("run", "run.json"))
374
+ .map((entry) => path.join(root, entry));
375
+ if (candidates.length === 0)
376
+ throw new Error("no completed Canopus product run was found");
377
+ const ranked = await Promise.all(candidates.map(async (file) => ({
378
+ file,
379
+ modified: (await stat(file)).mtimeMs,
380
+ })));
381
+ ranked.sort((left, right) => right.modified - left.modified || left.file.localeCompare(right.file));
382
+ return ranked[0]?.file ?? (() => { throw new Error("no completed Canopus product run was found"); })();
383
+ }
384
+ async function latestInspectionFile() {
385
+ const root = path.join(os.homedir(), ".canopus", "runs");
386
+ const entries = await readdir(root, { recursive: true });
387
+ const candidates = entries
388
+ .filter((entry) => entry.endsWith(`${path.sep}run${path.sep}run.json`) ||
389
+ entry === path.join("run", "run.json") ||
390
+ entry.endsWith(`${path.sep}run${path.sep}failure.json`) ||
391
+ entry === path.join("run", "failure.json"))
392
+ .map((entry) => path.join(root, entry));
393
+ if (candidates.length === 0)
394
+ throw new Error("no inspectable Canopus product run was found");
395
+ const ranked = await Promise.all(candidates.map(async (file) => ({
396
+ file,
397
+ modified: (await stat(file)).mtimeMs,
398
+ })));
399
+ ranked.sort((left, right) => right.modified - left.modified || left.file.localeCompare(right.file));
400
+ return ranked[0]?.file ?? (() => { throw new Error("no inspectable Canopus product run was found"); })();
401
+ }
402
+ async function inspectCommand(value, rest) {
403
+ if (rest.length !== 0)
404
+ throw new Error("inspect accepts at most one run file");
405
+ const file = value === undefined || value === "latest"
406
+ ? await latestInspectionFile()
407
+ : path.resolve(value);
408
+ const raw = await jsonFile(file);
409
+ const schema = typeof raw === "object" && raw !== null && !Array.isArray(raw)
410
+ ? raw.schema
411
+ : undefined;
412
+ const projection = schema === "canopus.diagnostic-run.v1"
413
+ ? projectDiagnosticRun(parseDiagnosticRunRecord(raw))
414
+ : schema === "canopus.failure.v0"
415
+ ? projectFailure(parseFailureRecord(raw))
416
+ : projectRun(parseRunRecord(raw));
417
+ const withdrawal = "proposal_id" in projection
418
+ ? await withdrawalCapabilityStatus(projection.proposal_id)
419
+ : { state: "not_applicable", available: false };
420
+ process.stdout.write(`${JSON.stringify({ ok: true, command: "inspect", run_file: file, projection, withdrawal })}\n`);
421
+ }
422
+ async function replayCommand(file, rest) {
423
+ if (file === undefined || rest.length !== 0)
424
+ throw new Error("replay requires exactly one run file");
425
+ process.stdout.write(`${JSON.stringify(await replayProduct(path.resolve(file)))}\n`);
426
+ }
427
+ async function withdrawCommand(args) {
428
+ const parsed = productOptions(args, ["--run", "--reason"], []);
429
+ if (parsed.positional.length > 1)
430
+ throw new Error("withdraw accepts at most one frontier");
431
+ const reason = parsed.values.get("--reason");
432
+ if (reason === undefined || reason.trim().length === 0)
433
+ throw new Error("withdraw requires --reason");
434
+ const runValue = parsed.values.get("--run") ?? "latest";
435
+ const runFile = runValue === "latest" ? await latestRunFile() : path.resolve(runValue);
436
+ const result = await withdrawProduct({
437
+ frontier: path.resolve(parsed.positional[0] ?? "."),
438
+ runFile,
439
+ reason,
440
+ });
441
+ process.stdout.write(`${JSON.stringify(result)}\n`);
442
+ }
443
+ async function main(argv) {
444
+ const [command, file, ...rest] = argv;
445
+ if (command === undefined || isHelp(command)) {
446
+ process.stdout.write(`${usage()}\n`);
447
+ return;
448
+ }
449
+ if (command === "mission") {
450
+ await missionCommand(argv.slice(1));
451
+ return;
452
+ }
453
+ if (command === "profile") {
454
+ await profileCommand(argv.slice(1));
455
+ return;
456
+ }
457
+ if (isHelp(file)) {
458
+ process.stdout.write(`${command === "run" ? runUsage()
459
+ : command === "inspect" ? inspectUsage()
460
+ : command === "doctor" ? doctorUsage()
461
+ : command === "replay" ? replayUsage()
462
+ : command === "withdraw" ? withdrawUsage()
463
+ : usage()}\n`);
464
+ return;
465
+ }
466
+ if (command === "doctor") {
467
+ await doctorCommand(argv.slice(1));
468
+ return;
469
+ }
470
+ if (command === "replay") {
471
+ await replayCommand(file, rest);
472
+ return;
473
+ }
474
+ if (command === "withdraw") {
475
+ await withdrawCommand(argv.slice(1));
476
+ return;
477
+ }
478
+ if (command === "validate") {
479
+ if (file === undefined)
480
+ throw new Error("validate requires a mission file");
481
+ await missionCommand(["validate", file, ...rest]);
482
+ return;
483
+ }
484
+ if (command === "inspect") {
485
+ await inspectCommand(file, rest);
486
+ return;
487
+ }
488
+ if (command === "run") {
489
+ if (file !== undefined && (file.endsWith(".json") || rest.includes("--source"))) {
490
+ await runMission(file, rest);
491
+ }
492
+ else {
493
+ await productRunCommand(argv.slice(1));
494
+ }
495
+ return;
496
+ }
497
+ throw new Error(`unknown command ${command}`);
498
+ }
499
+ main(process.argv.slice(2)).catch((error) => {
500
+ const message = error instanceof Error ? error.message : String(error);
501
+ process.stderr.write(`${JSON.stringify({ ok: false, error: message })}\n`);
502
+ process.exitCode = 1;
503
+ });
@@ -0,0 +1,50 @@
1
+ export declare const CANDIDATE_SCHEMA: "canopus.candidate.v0";
2
+ export declare const CANDIDATE_STATUSES: readonly ["success", "null", "failed"];
3
+ export declare const MAX_CANDIDATE_ARTIFACTS = 10;
4
+ export declare const MAX_CANDIDATE_OBSERVATIONS = 16;
5
+ export declare const MAX_CANDIDATE_TESTS = 2;
6
+ export declare const MAX_CANDIDATE_CAVEATS = 10;
7
+ export type CandidateStatus = (typeof CANDIDATE_STATUSES)[number];
8
+ export interface FrozenArtifact {
9
+ path: string;
10
+ kind: string;
11
+ digest: string;
12
+ bytes: number;
13
+ }
14
+ export interface TestRecord {
15
+ argv: string[];
16
+ executable_digest: string;
17
+ exit_code: number;
18
+ stdout_digest: string;
19
+ stderr_digest: string;
20
+ duration_ms: number;
21
+ }
22
+ export interface CandidateCosts {
23
+ wall_time_ms: number;
24
+ attempt: number;
25
+ input_tokens: number;
26
+ output_tokens: number;
27
+ }
28
+ export interface Candidate {
29
+ schema: typeof CANDIDATE_SCHEMA;
30
+ mission_id: string;
31
+ status: CandidateStatus;
32
+ claim: string;
33
+ artifacts: FrozenArtifact[];
34
+ observations: string[];
35
+ tests: TestRecord[];
36
+ costs: CandidateCosts;
37
+ caveats: string[];
38
+ engine: {
39
+ name: string;
40
+ version: string;
41
+ binary_sha256: string | null;
42
+ model: string | null;
43
+ configuration_sha256: string;
44
+ };
45
+ repair?: {
46
+ parent_candidate: string;
47
+ reason: string;
48
+ };
49
+ }
50
+ export declare function parseCandidate(value: unknown): Candidate;
@@ -0,0 +1,93 @@
1
+ import { MISSION_ID_RE, arrayAt, enumAt, exactKeys, integerAt, objectAt, relativePathAt, sha256At, stringAt, } from "./validation.js";
2
+ export const CANDIDATE_SCHEMA = "canopus.candidate.v0";
3
+ export const CANDIDATE_STATUSES = ["success", "null", "failed"];
4
+ export const MAX_CANDIDATE_ARTIFACTS = 10;
5
+ export const MAX_CANDIDATE_OBSERVATIONS = 16;
6
+ export const MAX_CANDIDATE_TESTS = 2;
7
+ export const MAX_CANDIDATE_CAVEATS = 10;
8
+ function parseArtifact(value, at) {
9
+ const object = objectAt(value, at);
10
+ exactKeys(object, ["path", "kind", "digest", "bytes"], [], at);
11
+ return {
12
+ path: relativePathAt(object.path, `${at}.path`),
13
+ kind: stringAt(object.kind, `${at}.kind`, { min: 1, max: 128 }),
14
+ digest: sha256At(object.digest, `${at}.digest`),
15
+ bytes: integerAt(object.bytes, `${at}.bytes`, 0, 1_073_741_824),
16
+ };
17
+ }
18
+ function parseTest(value, at) {
19
+ const object = objectAt(value, at);
20
+ exactKeys(object, ["argv", "executable_digest", "exit_code", "stdout_digest", "stderr_digest", "duration_ms"], [], at);
21
+ return {
22
+ argv: arrayAt(object.argv, `${at}.argv`, { min: 1, max: 64 }, (item, itemAt) => stringAt(item, itemAt, { max: 4096 })),
23
+ executable_digest: sha256At(object.executable_digest, `${at}.executable_digest`),
24
+ exit_code: integerAt(object.exit_code, `${at}.exit_code`, -1, 255),
25
+ stdout_digest: sha256At(object.stdout_digest, `${at}.stdout_digest`),
26
+ stderr_digest: sha256At(object.stderr_digest, `${at}.stderr_digest`),
27
+ duration_ms: integerAt(object.duration_ms, `${at}.duration_ms`, 0, 3_600_000),
28
+ };
29
+ }
30
+ function parseCosts(value) {
31
+ const object = objectAt(value, "candidate.costs");
32
+ exactKeys(object, ["wall_time_ms", "attempt", "input_tokens", "output_tokens"], [], "candidate.costs");
33
+ return {
34
+ wall_time_ms: integerAt(object.wall_time_ms, "candidate.costs.wall_time_ms", 0, 3_600_000),
35
+ attempt: integerAt(object.attempt, "candidate.costs.attempt", 1, 8),
36
+ input_tokens: integerAt(object.input_tokens, "candidate.costs.input_tokens", 0, 1_000_000),
37
+ output_tokens: integerAt(object.output_tokens, "candidate.costs.output_tokens", 0, 1_000_000),
38
+ };
39
+ }
40
+ export function parseCandidate(value) {
41
+ const object = objectAt(value, "candidate");
42
+ exactKeys(object, [
43
+ "schema",
44
+ "mission_id",
45
+ "status",
46
+ "claim",
47
+ "artifacts",
48
+ "observations",
49
+ "tests",
50
+ "costs",
51
+ "caveats",
52
+ "engine",
53
+ ], ["repair"], "candidate");
54
+ const engine = objectAt(object.engine, "candidate.engine");
55
+ exactKeys(engine, ["name", "version", "binary_sha256", "model", "configuration_sha256"], [], "candidate.engine");
56
+ const base = {
57
+ schema: enumAt(object.schema, "candidate.schema", [CANDIDATE_SCHEMA]),
58
+ mission_id: stringAt(object.mission_id, "candidate.mission_id", {
59
+ max: 134,
60
+ pattern: MISSION_ID_RE,
61
+ }),
62
+ status: enumAt(object.status, "candidate.status", CANDIDATE_STATUSES),
63
+ claim: stringAt(object.claim, "candidate.claim", { min: 1, max: 8192 }),
64
+ artifacts: arrayAt(object.artifacts, "candidate.artifacts", { max: MAX_CANDIDATE_ARTIFACTS }, parseArtifact),
65
+ observations: arrayAt(object.observations, "candidate.observations", { max: MAX_CANDIDATE_OBSERVATIONS }, (item, at) => stringAt(item, at, { min: 1, max: 4096 })),
66
+ tests: arrayAt(object.tests, "candidate.tests", { max: MAX_CANDIDATE_TESTS }, parseTest),
67
+ costs: parseCosts(object.costs),
68
+ caveats: arrayAt(object.caveats, "candidate.caveats", { max: MAX_CANDIDATE_CAVEATS }, (item, at) => stringAt(item, at, { min: 1, max: 4096 })),
69
+ engine: {
70
+ name: stringAt(engine.name, "candidate.engine.name", { min: 1, max: 128 }),
71
+ version: stringAt(engine.version, "candidate.engine.version", { min: 1, max: 256 }),
72
+ binary_sha256: engine.binary_sha256 === null
73
+ ? null
74
+ : sha256At(engine.binary_sha256, "candidate.engine.binary_sha256"),
75
+ model: engine.model === null
76
+ ? null
77
+ : stringAt(engine.model, "candidate.engine.model", { min: 1, max: 256 }),
78
+ configuration_sha256: sha256At(engine.configuration_sha256, "candidate.engine.configuration_sha256"),
79
+ },
80
+ };
81
+ if (object.repair === undefined) {
82
+ return base;
83
+ }
84
+ const repair = objectAt(object.repair, "candidate.repair");
85
+ exactKeys(repair, ["parent_candidate", "reason"], [], "candidate.repair");
86
+ return {
87
+ ...base,
88
+ repair: {
89
+ parent_candidate: sha256At(repair.parent_candidate, "candidate.repair.parent_candidate"),
90
+ reason: stringAt(repair.reason, "candidate.repair.reason", { min: 1, max: 4096 }),
91
+ },
92
+ };
93
+ }