@vela-science/canopus 0.6.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +67 -139
  3. package/SECURITY.md +3 -2
  4. package/capsules/erdos1056-k15/bin/linux-arm64/{10428801-10429000 → 10429201-10429400}/verifier +0 -0
  5. package/capsules/erdos1056-k15/bin/linux-x86_64/{10428801-10429000 → 10429201-10429400}/verifier +0 -0
  6. package/compatibility.json +25 -0
  7. package/dist/src/activity/events.d.ts +4 -1
  8. package/dist/src/activity/events.js +8 -2
  9. package/dist/src/activity/store.d.ts +2 -2
  10. package/dist/src/activity/store.js +4 -3
  11. package/dist/src/{receipt/map.d.ts → candidate/finalize.d.ts} +2 -9
  12. package/dist/src/candidate/finalize.js +71 -0
  13. package/dist/src/cli.js +88 -229
  14. package/dist/src/contracts/mission.d.ts +1 -13
  15. package/dist/src/contracts/mission.js +4 -42
  16. package/dist/src/contracts/validation.d.ts +1 -25
  17. package/dist/src/contracts/validation.js +1 -95
  18. package/dist/src/engines/codex-tools-native.js +2 -2
  19. package/dist/src/index.d.ts +5 -34
  20. package/dist/src/index.js +4 -34
  21. package/dist/src/mission/prepare.d.ts +10 -1
  22. package/dist/src/mission/prepare.js +29 -30
  23. package/dist/src/product/coverage.d.ts +17 -0
  24. package/dist/src/product/coverage.js +137 -0
  25. package/dist/src/product/doctor.d.ts +1 -2
  26. package/dist/src/product/doctor.js +29 -8
  27. package/dist/src/product/profile.js +8 -2
  28. package/dist/src/product/replay.js +11 -12
  29. package/dist/src/product/run.d.ts +5 -9
  30. package/dist/src/product/run.js +17 -88
  31. package/dist/src/product/submission.d.ts +40 -0
  32. package/dist/src/product/submission.js +217 -0
  33. package/dist/src/product/submit.d.ts +22 -0
  34. package/dist/src/product/submit.js +264 -0
  35. package/dist/src/product/version.d.ts +2 -2
  36. package/dist/src/product/version.js +27 -4
  37. package/dist/src/projection/current-run.d.ts +12 -0
  38. package/dist/src/projection/current-run.js +94 -0
  39. package/dist/src/projection/failure.d.ts +17 -7
  40. package/dist/src/projection/failure.js +43 -12
  41. package/dist/src/projection/retained-mission.d.ts +6 -0
  42. package/dist/src/projection/retained-mission.js +41 -0
  43. package/dist/src/projection/retained-run.d.ts +7 -0
  44. package/dist/src/projection/retained-run.js +50 -0
  45. package/dist/src/run.d.ts +43 -41
  46. package/dist/src/run.js +34 -412
  47. package/dist/src/util/canonical.d.ts +1 -5
  48. package/dist/src/util/canonical.js +1 -53
  49. package/dist/src/vela/cli.d.ts +7 -51
  50. package/dist/src/vela/cli.js +102 -523
  51. package/dist/src/vela/types.d.ts +3 -19
  52. package/dist/src/verifier/run.js +1 -1
  53. package/dist/src/workspace/prepare.d.ts +1 -1
  54. package/dist/src/workspace/prepare.js +15 -15
  55. package/docs/EVALUATION.md +142 -0
  56. package/docs/MISSIONS.md +42 -80
  57. package/docs/RUN_RECORD.md +77 -99
  58. package/docs/adr/0010-nonmutating-runs-and-explicit-submission.md +191 -0
  59. package/missions/erdos1056-k15-next/mission.draft.json +10 -8
  60. package/package.json +18 -22
  61. package/profiles/{erdos1056-k15-10428801-10429000.json → erdos1056-k15-10429201-10429400.json} +10 -10
  62. package/schemas/failure.v1.json +17 -0
  63. package/schemas/mission.v0.json +3 -4
  64. package/schemas/mission.v1.json +7 -28
  65. package/schemas/{run.v1.json → run.v2.json} +58 -116
  66. package/schemas/submission-bundle.v1.json +63 -0
  67. package/toolchain.lock.json +31 -0
  68. package/BUILD_WEEK.md +0 -198
  69. package/advisories/erdos1056-claim-fidelity/output.schema.json +0 -90
  70. package/advisories/erdos1056-claim-fidelity/registration.json +0 -42
  71. package/advisories/erdos1056-claim-fidelity/results/assessment.json +0 -1
  72. package/advisories/erdos1056-claim-fidelity/results/verification.json +0 -1
  73. package/capsules/formal-erdos-505-test-dim-one/verifier +0 -105
  74. package/capsules/quantum-10-1-4/verifier.py +0 -152
  75. package/capsules/sidon-a24/bin/linux-arm64/verifier +0 -0
  76. package/capsules/sidon-a24/bin/linux-x86_64/verifier +0 -0
  77. package/dist/src/capability/withdrawal.d.ts +0 -47
  78. package/dist/src/capability/withdrawal.js +0 -487
  79. package/dist/src/engines/codex-exec.d.ts +0 -17
  80. package/dist/src/engines/codex-exec.js +0 -136
  81. package/dist/src/engines/codex-tool-free.d.ts +0 -26
  82. package/dist/src/engines/codex-tool-free.js +0 -213
  83. package/dist/src/product/withdraw.d.ts +0 -8
  84. package/dist/src/product/withdraw.js +0 -255
  85. package/dist/src/projection/diagnostic.d.ts +0 -12
  86. package/dist/src/projection/diagnostic.js +0 -61
  87. package/dist/src/projection/public-run.d.ts +0 -59
  88. package/dist/src/projection/public-run.js +0 -106
  89. package/dist/src/projection/publication.d.ts +0 -36
  90. package/dist/src/projection/publication.js +0 -47
  91. package/dist/src/projection/run.d.ts +0 -95
  92. package/dist/src/projection/run.js +0 -173
  93. package/dist/src/receipt/map.js +0 -168
  94. package/dist/src/repair/plan.d.ts +0 -2
  95. package/dist/src/repair/plan.js +0 -22
  96. package/dist/src/roles.d.ts +0 -2
  97. package/dist/src/roles.js +0 -9
  98. package/docs/RELEASES.md +0 -1389
  99. package/evidence/build-week/run_eb6bcd46-cffd-4ae8-b630-2681bd84da71.public.json +0 -1
  100. package/evidence/build-week/run_f68e4cfc-e5c7-4c73-86cb-d79807c47ec4.public.json +0 -1
  101. package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/pending-commands.json +0 -1
  102. package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/public-run.json +0 -1
  103. package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/root-manifest.json +0 -1
  104. package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/web-import.json +0 -1
  105. package/evidence/erdos/run_b3b0cf07-b149-4d53-8258-76ba0e1fc0a5/pending-commands.json +0 -1
  106. package/evidence/erdos/run_b3b0cf07-b149-4d53-8258-76ba0e1fc0a5/public-run.json +0 -1
  107. package/evidence/erdos/run_b3b0cf07-b149-4d53-8258-76ba0e1fc0a5/root-manifest.json +0 -1
  108. package/evidence/erdos/run_b3b0cf07-b149-4d53-8258-76ba0e1fc0a5/web-import.json +0 -1
  109. package/missions/formal-erdos-505-test-dim-one-gpt56/mission.draft.json +0 -40
  110. package/missions/quantum-10-1-4-retry/mission.draft.json +0 -40
  111. package/missions/sidon-a24-at-least-7194-gpt56/mission.draft.json +0 -42
  112. package/missions/sidon-a24-at-least-7194-gpt56-v2/mission.draft.json +0 -42
  113. package/missions/sidon-a24-at-least-7194-gpt56-v3/mission.draft.json +0 -42
  114. package/profiles/formal-erdos-505-test-dim-one-gpt56.json +0 -35
  115. package/profiles/quantum-10-1-4-stabilizer-retry.json +0 -35
  116. package/profiles/sidon-a24-at-least-7194-gpt56-v2.json +0 -35
  117. package/profiles/sidon-a24-at-least-7194-gpt56-v3.json +0 -35
  118. package/profiles/sidon-a24-at-least-7194-gpt56.json +0 -35
  119. package/schemas/canopus.public-run.v1.json +0 -116
  120. package/scripts/run-claim-fidelity-advisory.mjs +0 -235
@@ -1,487 +0,0 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
- import { createPrivateKey, createPublicKey, verify as verifyEd25519, } from "node:crypto";
4
- import { chmod, lstat, mkdir, rename, rm, writeFile } from "node:fs/promises";
5
- import { AGENT_RE, exactKeys, gitObjectAt, objectAt, relativePathAt, sha256At, stringAt, } from "../contracts/validation.js";
6
- import { canonicalJcs, canonicalJson, contentDigest, protocolDigest, sha256Bytes, } from "../util/canonical.js";
7
- import { readBoundedRegularFile } from "../util/files.js";
8
- export const WITHDRAWAL_CAPABILITY_SCHEMA = "canopus.withdrawal-capability.v1";
9
- function capabilityBase(storeRoot) {
10
- return path.resolve(storeRoot ?? path.join(os.homedir(), ".canopus", "capabilities"));
11
- }
12
- export function capabilityDirectory(proposalId, storeRoot) {
13
- if (!/^vpr_[0-9a-f]{16}$/u.test(proposalId)) {
14
- throw new Error("withdrawal capability requires a full vpr_<16 lowercase hex> proposal id");
15
- }
16
- return path.join(capabilityBase(storeRoot), proposalId);
17
- }
18
- function agentKeyPath(velaHome, actor) {
19
- const safe = [...actor]
20
- .map((character) => /[A-Za-z0-9_-]/u.test(character) ? character : "-")
21
- .join("");
22
- return path.join(velaHome, ".vela", "agents", safe, "private.key");
23
- }
24
- function object(value, label) {
25
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
26
- throw new Error(`${label} must be an object`);
27
- }
28
- return value;
29
- }
30
- function text(value, label) {
31
- if (typeof value !== "string" || value.length === 0)
32
- throw new Error(`${label} must be a string`);
33
- return value;
34
- }
35
- const RFC3339_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u;
36
- const HEX_SEED_BYTES = 64;
37
- const PRIVATE_KEY_MAX_BYTES = HEX_SEED_BYTES + 1;
38
- function timestampAt(value, at) {
39
- const parsed = stringAt(value, at, { min: 20, max: 64, pattern: RFC3339_RE });
40
- if (!Number.isFinite(Date.parse(parsed)))
41
- throw new Error(`${at} is not a valid RFC3339 timestamp`);
42
- return parsed;
43
- }
44
- function frontierAt(value, at) {
45
- if (value === ".")
46
- return ".";
47
- return relativePathAt(value, at);
48
- }
49
- function parseRoots(value, at) {
50
- const roots = objectAt(value, at);
51
- exactKeys(roots, ["git_commit", "git_tree", "vela_event_log", "vela_snapshot"], [], at);
52
- return {
53
- git_commit: gitObjectAt(roots.git_commit, `${at}.git_commit`),
54
- git_tree: gitObjectAt(roots.git_tree, `${at}.git_tree`),
55
- vela_event_log: sha256At(roots.vela_event_log, `${at}.vela_event_log`),
56
- vela_snapshot: sha256At(roots.vela_snapshot, `${at}.vela_snapshot`),
57
- };
58
- }
59
- function parseStrictBaseline(value, at) {
60
- const baseline = objectAt(value, at);
61
- exactKeys(baseline, ["status", "blocker_count", "blockers_root", "rule_counts"], [], at);
62
- if (baseline.status !== "pass" && baseline.status !== "fail") {
63
- throw new Error(`${at}.status must be pass or fail`);
64
- }
65
- if (typeof baseline.blocker_count !== "number" ||
66
- !Number.isSafeInteger(baseline.blocker_count) ||
67
- baseline.blocker_count < 0) {
68
- throw new Error(`${at}.blocker_count must be a nonnegative integer`);
69
- }
70
- if (!Array.isArray(baseline.rule_counts) || baseline.rule_counts.length > 512) {
71
- throw new Error(`${at}.rule_counts must be a bounded array`);
72
- }
73
- const seen = new Set();
74
- let total = 0;
75
- const ruleCounts = baseline.rule_counts.map((value, index) => {
76
- const ruleCount = objectAt(value, `${at}.rule_counts[${index}]`);
77
- exactKeys(ruleCount, ["rule", "count"], [], `${at}.rule_counts[${index}]`);
78
- const rule = stringAt(ruleCount.rule, `${at}.rule_counts[${index}].rule`, {
79
- min: 1,
80
- max: 128,
81
- pattern: /^[a-z][a-z0-9_]*$/u,
82
- });
83
- if (seen.has(rule))
84
- throw new Error(`${at}.rule_counts contains duplicate rules`);
85
- seen.add(rule);
86
- if (typeof ruleCount.count !== "number" ||
87
- !Number.isSafeInteger(ruleCount.count) ||
88
- ruleCount.count < 1) {
89
- throw new Error(`${at}.rule_counts[${index}].count must be a positive integer`);
90
- }
91
- total += ruleCount.count;
92
- return { rule, count: ruleCount.count };
93
- });
94
- if (total !== baseline.blocker_count ||
95
- (baseline.status === "pass") !== (baseline.blocker_count === 0)) {
96
- throw new Error(`${at} status, count, and per-rule totals disagree`);
97
- }
98
- const sorted = [...ruleCounts].sort((left, right) => left.rule.localeCompare(right.rule));
99
- if (canonicalJcs(sorted) !== canonicalJcs(ruleCounts)) {
100
- throw new Error(`${at}.rule_counts must be sorted by rule`);
101
- }
102
- return {
103
- status: baseline.status,
104
- blocker_count: baseline.blocker_count,
105
- blockers_root: sha256At(baseline.blockers_root, `${at}.blockers_root`),
106
- rule_counts: ruleCounts,
107
- };
108
- }
109
- function parseManifest(value, proposalId) {
110
- const manifest = objectAt(value, "withdrawal capability manifest");
111
- exactKeys(manifest, [
112
- "schema",
113
- "state",
114
- "proposal_id",
115
- "proposal_root",
116
- "receipt_root",
117
- "identity_binding_id",
118
- "actor",
119
- "public_key",
120
- "frontier",
121
- "final_roots",
122
- "strict_baseline",
123
- "vela",
124
- "created_at",
125
- ], ["consumed_at", "consumed_reason"], "withdrawal capability manifest");
126
- if (manifest.schema !== WITHDRAWAL_CAPABILITY_SCHEMA) {
127
- throw new Error(`withdrawal capability schema must be ${WITHDRAWAL_CAPABILITY_SCHEMA}`);
128
- }
129
- if (manifest.proposal_id !== proposalId) {
130
- throw new Error("withdrawal capability manifest does not match its directory");
131
- }
132
- const state = manifest.state;
133
- if (state !== "available" && state !== "consumed") {
134
- throw new Error("withdrawal capability state must be available or consumed");
135
- }
136
- const vela = objectAt(manifest.vela, "withdrawal capability manifest.vela");
137
- exactKeys(vela, ["binary", "version", "sha256"], [], "withdrawal capability manifest.vela");
138
- const binary = stringAt(vela.binary, "withdrawal capability manifest.vela.binary", {
139
- min: 1,
140
- max: 4096,
141
- });
142
- if (!path.isAbsolute(binary) || binary.includes("\0")) {
143
- throw new Error("withdrawal capability Vela binary must be an absolute path");
144
- }
145
- const consumedAt = manifest.consumed_at === undefined
146
- ? undefined
147
- : timestampAt(manifest.consumed_at, "withdrawal capability manifest.consumed_at");
148
- const consumedReason = manifest.consumed_reason;
149
- if (consumedReason !== undefined &&
150
- consumedReason !== "withdrawn" &&
151
- consumedReason !== "human_decision_observed") {
152
- throw new Error("withdrawal capability consumed_reason is invalid");
153
- }
154
- if ((state === "available" && (consumedAt !== undefined || consumedReason !== undefined)) ||
155
- (state === "consumed" && (consumedAt === undefined || consumedReason === undefined))) {
156
- throw new Error("withdrawal capability consumption fields disagree with state");
157
- }
158
- return {
159
- schema: WITHDRAWAL_CAPABILITY_SCHEMA,
160
- state,
161
- proposal_id: stringAt(manifest.proposal_id, "withdrawal capability manifest.proposal_id", {
162
- max: 20,
163
- pattern: /^vpr_[0-9a-f]{16}$/u,
164
- }),
165
- proposal_root: sha256At(manifest.proposal_root, "withdrawal capability manifest.proposal_root"),
166
- receipt_root: sha256At(manifest.receipt_root, "withdrawal capability manifest.receipt_root"),
167
- identity_binding_id: stringAt(manifest.identity_binding_id, "withdrawal capability manifest.identity_binding_id", { max: 20, pattern: /^vib_[0-9a-f]{16}$/u }),
168
- actor: stringAt(manifest.actor, "withdrawal capability manifest.actor", {
169
- max: 69,
170
- pattern: AGENT_RE,
171
- }),
172
- public_key: stringAt(manifest.public_key, "withdrawal capability manifest.public_key", {
173
- max: 64,
174
- pattern: /^[0-9a-f]{64}$/u,
175
- }),
176
- frontier: frontierAt(manifest.frontier, "withdrawal capability manifest.frontier"),
177
- final_roots: parseRoots(manifest.final_roots, "withdrawal capability manifest.final_roots"),
178
- strict_baseline: parseStrictBaseline(manifest.strict_baseline, "withdrawal capability manifest.strict_baseline"),
179
- vela: {
180
- binary,
181
- version: stringAt(vela.version, "withdrawal capability manifest.vela.version", {
182
- min: 1,
183
- max: 64,
184
- pattern: /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$/u,
185
- }),
186
- sha256: sha256At(vela.sha256, "withdrawal capability manifest.vela.sha256"),
187
- },
188
- created_at: timestampAt(manifest.created_at, "withdrawal capability manifest.created_at"),
189
- ...(consumedAt === undefined ? {} : { consumed_at: consumedAt }),
190
- ...(consumedReason === undefined ? {} : { consumed_reason: consumedReason }),
191
- };
192
- }
193
- function hexNibble(byte) {
194
- if (byte >= 48 && byte <= 57)
195
- return byte - 48;
196
- if (byte >= 97 && byte <= 102)
197
- return byte - 87;
198
- return -1;
199
- }
200
- function decodeSeed(keyBytes) {
201
- const length = keyBytes.length;
202
- if ((length !== HEX_SEED_BYTES && length !== PRIVATE_KEY_MAX_BYTES) ||
203
- (length === PRIVATE_KEY_MAX_BYTES && keyBytes[HEX_SEED_BYTES] !== 10)) {
204
- throw new Error("producer session key must contain one 32-byte lowercase hex seed");
205
- }
206
- const seed = Buffer.alloc(32);
207
- for (let index = 0; index < HEX_SEED_BYTES; index += 2) {
208
- const high = hexNibble(keyBytes[index] ?? -1);
209
- const low = hexNibble(keyBytes[index + 1] ?? -1);
210
- if (high < 0 || low < 0) {
211
- seed.fill(0);
212
- throw new Error("producer session key must contain one 32-byte lowercase hex seed");
213
- }
214
- seed[index / 2] = (high << 4) | low;
215
- }
216
- return seed;
217
- }
218
- function derivePublicKey(seed) {
219
- const privateDer = Buffer.concat([
220
- Buffer.from("302e020100300506032b657004220420", "hex"),
221
- seed,
222
- ]);
223
- try {
224
- const derivedPublicDer = createPublicKey(createPrivateKey({ key: privateDer, format: "der", type: "pkcs8" })).export({ format: "der", type: "spki" });
225
- return Buffer.from(derivedPublicDer).subarray(-32).toString("hex");
226
- }
227
- finally {
228
- privateDer.fill(0);
229
- }
230
- }
231
- function verifyIdentityBinding(value) {
232
- const binding = objectAt(value, "Receipt identity binding");
233
- exactKeys(binding, [
234
- "schema",
235
- "binding_id",
236
- "actor_id",
237
- "actor_class",
238
- "public_key_hex",
239
- "created_at",
240
- "signature",
241
- ], [], "Receipt identity binding");
242
- if (binding.schema !== "vela.identity_binding.v0.1" || binding.actor_class !== "agent") {
243
- throw new Error("Receipt identity binding must be a Vela agent binding");
244
- }
245
- const actor = stringAt(binding.actor_id, "Receipt identity binding.actor_id", {
246
- max: 69,
247
- pattern: AGENT_RE,
248
- });
249
- const bindingId = stringAt(binding.binding_id, "Receipt identity binding.binding_id", {
250
- max: 20,
251
- pattern: /^vib_[0-9a-f]{16}$/u,
252
- });
253
- const publicKey = stringAt(binding.public_key_hex, "Receipt identity binding.public_key_hex", {
254
- max: 64,
255
- pattern: /^[0-9a-f]{64}$/u,
256
- });
257
- const createdAt = timestampAt(binding.created_at, "Receipt identity binding.created_at");
258
- const signatureHex = stringAt(binding.signature, "Receipt identity binding.signature", {
259
- max: 128,
260
- pattern: /^[0-9a-f]{128}$/u,
261
- });
262
- const preimage = {
263
- schema: "vela.identity_binding.v0.1",
264
- binding_id: "",
265
- actor_id: actor,
266
- actor_class: "agent",
267
- public_key_hex: publicKey,
268
- created_at: createdAt,
269
- signature: "",
270
- };
271
- const preimageBytes = Buffer.from(canonicalJcs(preimage));
272
- const expectedId = `vib_${sha256Bytes(preimageBytes).slice("sha256:".length, "sha256:".length + 16)}`;
273
- if (bindingId !== expectedId)
274
- throw new Error("Receipt identity binding id does not rederive");
275
- const publicDer = Buffer.concat([
276
- Buffer.from("302a300506032b6570032100", "hex"),
277
- Buffer.from(publicKey, "hex"),
278
- ]);
279
- const signature = Buffer.from(signatureHex, "hex");
280
- try {
281
- const key = createPublicKey({ key: publicDer, format: "der", type: "spki" });
282
- if (!verifyEd25519(null, preimageBytes, key, signature)) {
283
- throw new Error("Receipt identity binding has no valid proof of possession");
284
- }
285
- }
286
- finally {
287
- publicDer.fill(0);
288
- signature.fill(0);
289
- }
290
- return { actor, bindingId, publicKey };
291
- }
292
- async function checkedCapabilityDirectory(proposalId, storeRoot) {
293
- const base = capabilityBase(storeRoot);
294
- const directory = capabilityDirectory(proposalId, storeRoot);
295
- const [baseMetadata, directoryMetadata] = await Promise.all([lstat(base), lstat(directory)]);
296
- if (!baseMetadata.isDirectory() || baseMetadata.isSymbolicLink() || (baseMetadata.mode & 0o777) !== 0o700) {
297
- throw new Error("withdrawal capability store must be one mode-0700 directory");
298
- }
299
- if (!directoryMetadata.isDirectory() ||
300
- directoryMetadata.isSymbolicLink() ||
301
- (directoryMetadata.mode & 0o777) !== 0o700) {
302
- throw new Error("withdrawal capability directory must be one mode-0700 directory");
303
- }
304
- return { base, directory };
305
- }
306
- export async function retainWithdrawalCapability(options) {
307
- if (options.landing.route !== "defer" && options.landing.originalRoute !== "defer") {
308
- throw new Error("only a pending deferred proposal may retain a withdrawal capability");
309
- }
310
- const frontier = frontierAt(options.mission.frontier, "mission.frontier");
311
- const frontierRoot = path.resolve(options.landingRepo, frontier);
312
- const proposalPath = path.join(frontierRoot, ".vela", "proposals", `${options.landing.proposalId}.json`);
313
- const proposal = object(JSON.parse((await readBoundedRegularFile(proposalPath, 8 * 1024 * 1024)).toString("utf8")), "proposal");
314
- if (text(proposal.id, "proposal.id") !== options.landing.proposalId) {
315
- throw new Error("landed proposal id does not match its canonical file");
316
- }
317
- if (text(proposal.status, "proposal.status") !== "pending_review") {
318
- throw new Error("landed proposal is not pending_review");
319
- }
320
- const proposalRoot = protocolDigest(proposal);
321
- const payload = object(proposal.payload, "proposal.payload");
322
- const submission = object(payload.vela_submission, "proposal.payload.vela_submission");
323
- const receiptRoot = text(submission.receipt_root, "vela_submission.receipt_root");
324
- if (receiptRoot !== options.landing.receiptRoot)
325
- throw new Error("landed Receipt root drifted");
326
- const receiptPath = relativePathAt(submission.receipt_path, "vela_submission.receipt_path");
327
- const receipt = object(JSON.parse((await readBoundedRegularFile(path.join(frontierRoot, receiptPath), 16 * 1024 * 1024)).toString("utf8")), "Receipt");
328
- if (protocolDigest(receipt) !== receiptRoot)
329
- throw new Error("bound Receipt bytes do not match their root");
330
- const environment = object(receipt.environment, "Receipt.environment");
331
- const producer = object(environment["vela:producer_context"], "Receipt producer context");
332
- const binding = verifyIdentityBinding(producer.identity_binding);
333
- const actor = binding.actor;
334
- if (actor !== options.mission.actor || actor !== object(proposal.actor, "proposal.actor").id) {
335
- throw new Error("Receipt identity actor does not match the mission and proposal");
336
- }
337
- const identityBindingId = binding.bindingId;
338
- const publicKey = binding.publicKey;
339
- const sourceKey = agentKeyPath(options.velaHome, actor);
340
- const sourceMetadata = await lstat(sourceKey);
341
- if (!sourceMetadata.isFile() || sourceMetadata.isSymbolicLink() || sourceMetadata.nlink !== 1 || (sourceMetadata.mode & 0o777) !== 0o600) {
342
- throw new Error("producer session key must be one singly-linked mode-0600 regular file");
343
- }
344
- const keyBytes = await readBoundedRegularFile(sourceKey, PRIVATE_KEY_MAX_BYTES);
345
- let seed;
346
- let directory;
347
- try {
348
- seed = decodeSeed(keyBytes);
349
- if (derivePublicKey(seed) !== publicKey) {
350
- throw new Error("producer session key does not derive the Receipt-bound public key");
351
- }
352
- directory = capabilityDirectory(options.landing.proposalId, options.storeRoot);
353
- await mkdir(capabilityBase(options.storeRoot), { recursive: true, mode: 0o700 });
354
- await chmod(capabilityBase(options.storeRoot), 0o700);
355
- await mkdir(directory, { mode: 0o700 });
356
- const manifest = {
357
- schema: WITHDRAWAL_CAPABILITY_SCHEMA,
358
- state: "available",
359
- proposal_id: options.landing.proposalId,
360
- proposal_root: proposalRoot,
361
- receipt_root: receiptRoot,
362
- identity_binding_id: identityBindingId,
363
- actor,
364
- public_key: publicKey,
365
- frontier,
366
- final_roots: options.finalRoots,
367
- strict_baseline: options.mission.schema === "canopus.mission.v1"
368
- ? options.mission.strict_baseline
369
- : {
370
- status: "pass",
371
- blocker_count: 0,
372
- blockers_root: sha256Bytes(canonicalJcs([])),
373
- rule_counts: [],
374
- },
375
- vela: {
376
- binary: path.resolve(options.velaBinary),
377
- version: options.mission.vela_version,
378
- sha256: options.mission.vela_sha256,
379
- },
380
- created_at: new Date().toISOString(),
381
- };
382
- await writeFile(path.join(directory, "manifest.json"), canonicalJson(manifest), { flag: "wx", mode: 0o600 });
383
- // Publish the secret last: an interruption before this point can leave at
384
- // most a public, unavailable manifest rather than an orphaned key.
385
- await writeFile(path.join(directory, "private.key"), keyBytes, { flag: "wx", mode: 0o600 });
386
- await chmod(path.join(directory, "private.key"), 0o600);
387
- return { directory, manifest, manifest_root: contentDigest(manifest) };
388
- }
389
- catch (error) {
390
- if (directory !== undefined)
391
- await rm(directory, { recursive: true, force: true });
392
- throw error;
393
- }
394
- finally {
395
- seed?.fill(0);
396
- keyBytes.fill(0);
397
- }
398
- }
399
- export async function loadWithdrawalCapability(proposalId, storeRoot) {
400
- const { directory } = await checkedCapabilityDirectory(proposalId, storeRoot);
401
- const manifestBytes = await readBoundedRegularFile(path.join(directory, "manifest.json"), 1024 * 1024);
402
- const manifestMetadata = await lstat(path.join(directory, "manifest.json"));
403
- if ((manifestMetadata.mode & 0o777) !== 0o600) {
404
- throw new Error("withdrawal capability manifest must be mode 0600");
405
- }
406
- const raw = parseManifest(JSON.parse(manifestBytes.toString("utf8")), proposalId);
407
- let secretAvailable = false;
408
- try {
409
- const metadata = await lstat(path.join(directory, "private.key"));
410
- if (!metadata.isFile() ||
411
- metadata.isSymbolicLink() ||
412
- metadata.nlink !== 1 ||
413
- (metadata.mode & 0o777) !== 0o600 ||
414
- (metadata.size !== HEX_SEED_BYTES && metadata.size !== PRIVATE_KEY_MAX_BYTES)) {
415
- throw new Error("withdrawal secret must be one singly-linked mode-0600 seed file");
416
- }
417
- secretAvailable = true;
418
- }
419
- catch (error) {
420
- if (error.code !== "ENOENT")
421
- throw error;
422
- }
423
- if (raw.state === "available" && !secretAvailable) {
424
- throw new Error("withdrawal capability state disagrees with secret presence");
425
- }
426
- return { directory, manifest: raw, manifest_root: contentDigest(raw), secret_available: secretAvailable };
427
- }
428
- export async function installWithdrawalCapabilitySecret(proposalId, target, storeRoot) {
429
- const loaded = await loadWithdrawalCapability(proposalId, storeRoot);
430
- if (loaded.manifest.state !== "available" || !loaded.secret_available) {
431
- throw new Error("withdrawal capability is not available");
432
- }
433
- const bytes = await readBoundedRegularFile(path.join(loaded.directory, "private.key"), PRIVATE_KEY_MAX_BYTES);
434
- let seed;
435
- try {
436
- seed = decodeSeed(bytes);
437
- if (derivePublicKey(seed) !== loaded.manifest.public_key) {
438
- throw new Error("withdrawal secret does not match the capability public key");
439
- }
440
- await writeFile(target, bytes, { flag: "wx", mode: 0o600 });
441
- await chmod(target, 0o600);
442
- }
443
- finally {
444
- seed?.fill(0);
445
- bytes.fill(0);
446
- }
447
- }
448
- export async function withdrawalCapabilityStatus(proposalId, storeRoot) {
449
- try {
450
- const loaded = await loadWithdrawalCapability(proposalId, storeRoot);
451
- return {
452
- proposal_id: proposalId,
453
- state: loaded.manifest.state,
454
- available: loaded.manifest.state === "available" && loaded.secret_available,
455
- cleanup_required: loaded.manifest.state === "consumed" && loaded.secret_available,
456
- manifest_root: loaded.manifest_root,
457
- consumed_at: loaded.manifest.consumed_at ?? null,
458
- consumed_reason: loaded.manifest.consumed_reason ?? null,
459
- };
460
- }
461
- catch (error) {
462
- if (error.code === "ENOENT") {
463
- return { proposal_id: proposalId, state: "absent", available: false };
464
- }
465
- throw error;
466
- }
467
- }
468
- export async function consumeWithdrawalCapability(proposalId, reason, storeRoot) {
469
- const loaded = await loadWithdrawalCapability(proposalId, storeRoot);
470
- if (loaded.manifest.state === "consumed") {
471
- if (loaded.secret_available) {
472
- await rm(path.join(loaded.directory, "private.key"), { force: true });
473
- }
474
- return loaded.manifest;
475
- }
476
- const consumed = {
477
- ...loaded.manifest,
478
- state: "consumed",
479
- consumed_at: new Date().toISOString(),
480
- consumed_reason: reason,
481
- };
482
- const temporary = path.join(loaded.directory, `.manifest.${process.pid}.tmp`);
483
- await writeFile(temporary, canonicalJson(consumed), { flag: "wx", mode: 0o600 });
484
- await rename(temporary, path.join(loaded.directory, "manifest.json"));
485
- await rm(path.join(loaded.directory, "private.key"), { force: true });
486
- return consumed;
487
- }
@@ -1,17 +0,0 @@
1
- import { type CommandRunner } from "../util/command.js";
2
- import type { Engine, EngineContext, EngineResult } from "./engine.js";
3
- export interface CodexEngineOptions {
4
- binary: string;
5
- expectedSha256: string;
6
- expectedVersion: string;
7
- model: string;
8
- authHome: string;
9
- outputSchema: string;
10
- runner?: CommandRunner;
11
- }
12
- export declare class CodexExecEngine implements Engine {
13
- #private;
14
- readonly name = "codex-exec";
15
- constructor(options: CodexEngineOptions);
16
- run(context: EngineContext): Promise<EngineResult>;
17
- }
@@ -1,136 +0,0 @@
1
- import path from "node:path";
2
- import { parseCandidateDraft, assertDraftArtifactsAllowed } from "../candidate/validate.js";
3
- import { roleInstruction } from "../roles.js";
4
- import { canonicalJson, contentDigest, sha256Bytes } from "../util/canonical.js";
5
- import { isolatedEnvironment, runCommand, } from "../util/command.js";
6
- import { MAX_EXECUTABLE_BYTES, readBoundedRegularFile, sha256RegularFile } from "../util/files.js";
7
- import { parseCodexEvents, summarizeCodexFailure } from "./codex-events.js";
8
- import { prepareIsolatedCodexHome, removeIsolatedCodexHome } from "./codex-home.js";
9
- import { CODEX_TOOL_FEATURES, sandboxedToolFreeCodexExecArgv, } from "./codex-tool-free.js";
10
- function prompt(mission, briefing) {
11
- return [
12
- "Execute one bounded Canopus research mission.",
13
- roleInstruction(mission.role),
14
- "This is a tool-free synthesis stage. You cannot inspect the host or run commands.",
15
- "Use only the mission and the exact Vela work briefing supplied below.",
16
- "Return artifact bytes inline as UTF-8 content. The trusted harness materializes and verifies them later.",
17
- "Do not request tools, network access, external messages, Vela signing, human keys, or accepted-state writes.",
18
- "Worker status reports producer completion, not verifier or scientific standing. Return status success when you produced all artifact bytes required by the output contract, while stating that separate verification remains pending.",
19
- "Return null only when the bounded work produced no candidate, and failed only when you could not produce a contract-complete candidate or observed disqualifying evidence. Do not turn verifier failure into a success claim.",
20
- "Return only the supplied engine-output JSON shape.",
21
- "Mission:",
22
- canonicalJson(mission),
23
- "Vela work briefing:",
24
- canonicalJson(briefing),
25
- ].join("\n");
26
- }
27
- export class CodexExecEngine {
28
- name = "codex-exec";
29
- #options;
30
- #runner;
31
- constructor(options) {
32
- this.#options = options;
33
- this.#runner = options.runner ?? runCommand;
34
- }
35
- async #version(cwd, home, timeoutMs) {
36
- const result = await this.#runner({
37
- argv: [this.#options.binary, "--version"],
38
- cwd,
39
- env: isolatedEnvironment(home),
40
- timeoutMs,
41
- maxOutputBytes: 4096,
42
- });
43
- const version = result.stdout.toString("utf8").trim();
44
- if (result.exitCode !== 0 || result.stderr.length !== 0 || version !== this.#options.expectedVersion) {
45
- throw new Error(`expected Codex ${this.#options.expectedVersion}, observed ${JSON.stringify(version)}`);
46
- }
47
- return version;
48
- }
49
- async run(context) {
50
- context.budget.beginAttempt();
51
- const binaryDigest = await sha256RegularFile(this.#options.binary, MAX_EXECUTABLE_BYTES);
52
- if (binaryDigest !== this.#options.expectedSha256) {
53
- throw new Error(`Codex binary digest mismatch: expected ${this.#options.expectedSha256}, observed ${binaryDigest}`);
54
- }
55
- context.budget.beginProcess();
56
- const finalPath = path.join(context.paths.work, ".canopus-final.json");
57
- const version = await this.#version(context.paths.work, context.paths.home, context.budget.remainingTimeMs());
58
- context.budget.beginProcess();
59
- const runtimeCodexHome = await prepareIsolatedCodexHome(this.#options.authHome, context.paths.home);
60
- try {
61
- const environment = {
62
- ...isolatedEnvironment(context.paths.home),
63
- CODEX_HOME: runtimeCodexHome,
64
- NO_COLOR: "1",
65
- };
66
- const argv = await sandboxedToolFreeCodexExecArgv({
67
- binary: this.#options.binary,
68
- model: this.#options.model,
69
- outputSchema: this.#options.outputSchema,
70
- finalPath,
71
- cwd: context.paths.work,
72
- authHome: runtimeCodexHome,
73
- });
74
- const started = performance.now();
75
- const workerPrompt = prompt(context.mission, context.briefing);
76
- context.budget.addPrompt(Buffer.byteLength(workerPrompt));
77
- const result = await this.#runner({
78
- argv,
79
- cwd: context.paths.work,
80
- env: environment,
81
- timeoutMs: context.budget.remainingTimeMs(),
82
- maxOutputBytes: context.budget.remainingOutputBytes(),
83
- stdin: workerPrompt,
84
- });
85
- context.budget.addOutput(result.stdout.length + result.stderr.length);
86
- if (result.exitCode !== 0) {
87
- throw new Error(`codex exec exited ${result.exitCode}: ` +
88
- `${summarizeCodexFailure(result.stdout.toString("utf8"))}; ` +
89
- `stdout_sha256=${sha256Bytes(result.stdout)}; stderr_sha256=${sha256Bytes(result.stderr)}`);
90
- }
91
- const events = parseCodexEvents(result.stdout.toString("utf8"));
92
- if (events.actionTypes.length !== 0) {
93
- throw new Error(`tool-free Codex worker emitted actions: ${events.actionTypes.join(",")}`);
94
- }
95
- context.budget.addTokens(events.usage.input_tokens + events.usage.output_tokens);
96
- const finalBytes = await readBoundedRegularFile(finalPath, 1_048_576);
97
- context.budget.addOutput(finalBytes.length);
98
- let raw;
99
- try {
100
- raw = JSON.parse(finalBytes.toString("utf8"));
101
- }
102
- catch (error) {
103
- throw new Error(`Codex final response is not JSON: ${String(error)}`);
104
- }
105
- const draft = parseCandidateDraft(raw);
106
- assertDraftArtifactsAllowed(draft, context.mission.allowed_paths);
107
- return {
108
- draft,
109
- engine: {
110
- name: this.name,
111
- version,
112
- binary_sha256: binaryDigest,
113
- model: this.#options.model,
114
- configuration_sha256: contentDigest({
115
- binary_sha256: binaryDigest,
116
- codex_version: version,
117
- disabled_features: CODEX_TOOL_FEATURES,
118
- model: this.#options.model,
119
- output_schema_sha256: sha256Bytes(await readBoundedRegularFile(this.#options.outputSchema, 1_048_576)),
120
- outer_sandbox: "macos_seatbelt_bounded_reads",
121
- product_sandbox: "read-only",
122
- }),
123
- },
124
- usage: events.usage,
125
- wallTimeMs: Math.max(0, Math.round(performance.now() - started)),
126
- eventTypes: events.eventTypes,
127
- actionTypes: events.actionTypes,
128
- eventsDigest: sha256Bytes(result.stdout),
129
- stderrDigest: sha256Bytes(result.stderr),
130
- };
131
- }
132
- finally {
133
- await removeIsolatedCodexHome(runtimeCodexHome);
134
- }
135
- }
136
- }
@@ -1,26 +0,0 @@
1
- export declare const CODEX_TOOL_FEATURES: readonly ["apply_patch_freeform", "apply_patch_streaming_events", "apps", "artifact", "browser_use", "browser_use_external", "computer_use", "enable_mcp_apps", "goals", "hooks", "image_generation", "in_app_browser", "memories", "multi_agent", "plugin_sharing", "remote_plugin", "shell_snapshot", "shell_tool", "tool_suggest", "unified_exec", "workspace_dependencies"];
2
- export interface ToolFreeCodexArgs {
3
- binary: string;
4
- model: string;
5
- outputSchema: string;
6
- finalPath: string;
7
- cwd: string;
8
- reasoningEffort?: string;
9
- }
10
- export interface SandboxedToolFreeCodexArgs extends ToolFreeCodexArgs {
11
- authHome: string;
12
- }
13
- /**
14
- * Wrap Codex itself in a host read boundary. Codex 0.139 always registers
15
- * view_image even when every feature-backed tool is disabled, so this outer
16
- * sandbox is the control that makes an arbitrary host image unreadable.
17
- */
18
- export declare function sandboxedToolFreeCodexExecArgv(options: SandboxedToolFreeCodexArgs): Promise<string[]>;
19
- /**
20
- * Build the complete, pinned Codex lane used for untrusted synthesis.
21
- *
22
- * Read-only sandboxing alone still permits host reads. This lane registers no
23
- * shell, patch, browser, app, MCP, memory, or computer-use surface at all.
24
- * The only accepted product is the final response constrained by outputSchema.
25
- */
26
- export declare function toolFreeCodexExecArgv(options: ToolFreeCodexArgs): string[];