@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,25 @@
1
+ import type { EngineUsage } from "./engine.js";
2
+ export interface CodexEventSummary {
3
+ usage: EngineUsage;
4
+ eventTypes: string[];
5
+ actionTypes: string[];
6
+ }
7
+ export interface CodexStructuralSummary {
8
+ lines: number;
9
+ parsed_lines: number;
10
+ invalid_lines: number;
11
+ event_types: Record<string, number>;
12
+ item_types: Record<string, number>;
13
+ }
14
+ /**
15
+ * Describe only the shape of a Codex JSONL stream. Values that could contain
16
+ * prompts, paths, commands, host data, or credentials are never returned.
17
+ */
18
+ export declare function summarizeCodexStructure(value: string): CodexStructuralSummary;
19
+ /**
20
+ * Extract a bounded diagnostic from Codex's documented JSONL failure events.
21
+ * Raw stderr and unstructured stdout are intentionally excluded because they
22
+ * can contain credential, host, or prompt material.
23
+ */
24
+ export declare function summarizeCodexFailure(value: string): string;
25
+ export declare function parseCodexEvents(value: string): CodexEventSummary;
@@ -0,0 +1,219 @@
1
+ import { objectAt } from "../contracts/validation.js";
2
+ const EVENT_TYPES = new Set([
3
+ "thread.started",
4
+ "turn.started",
5
+ "item.started",
6
+ "item.updated",
7
+ "item.completed",
8
+ "turn.completed",
9
+ ]);
10
+ const PASSIVE_ITEMS = new Set(["agent_message", "reasoning", "todo_list"]);
11
+ const ACTION_ITEMS = new Set(["command_execution", "file_change"]);
12
+ const FORBIDDEN_COMMAND = /(?:^|[\s;&|])(?:vela\s+sign|git\s+push|gh\s+|curl\s+|wget\s+|ssh\s+)/iu;
13
+ const SECRET_ASSIGNMENT = /\b(?:api[_-]?key|access[_-]?token|authorization|password|secret)\b\s*[:=]\s*[^\s,;]+/giu;
14
+ const SECRET_TOKEN = /\b(?:sk|sess|key)-[A-Za-z0-9_-]{8,}\b/gu;
15
+ const BEARER_TOKEN = /\bBearer\s+[A-Za-z0-9._~+\/-]+=*/giu;
16
+ const URL_WITH_QUERY = /https?:\/\/[^\s?#]+[?#][^\s]*/giu;
17
+ const MAX_FAILURE_LINES = 256;
18
+ const MAX_FAILURE_MESSAGE_CHARS = 512;
19
+ function safeEventLabel(value) {
20
+ return typeof value === "string" &&
21
+ (EVENT_TYPES.has(value) || value === "error" || value === "turn.failed")
22
+ ? value
23
+ : "other";
24
+ }
25
+ function safeItemLabel(value) {
26
+ return typeof value === "string" &&
27
+ (PASSIVE_ITEMS.has(value) || ACTION_ITEMS.has(value) || value === "error")
28
+ ? value
29
+ : "other";
30
+ }
31
+ function increment(counts, label) {
32
+ counts[label] = (counts[label] ?? 0) + 1;
33
+ }
34
+ /**
35
+ * Describe only the shape of a Codex JSONL stream. Values that could contain
36
+ * prompts, paths, commands, host data, or credentials are never returned.
37
+ */
38
+ export function summarizeCodexStructure(value) {
39
+ const summary = {
40
+ lines: 0,
41
+ parsed_lines: 0,
42
+ invalid_lines: 0,
43
+ event_types: {},
44
+ item_types: {},
45
+ };
46
+ for (const line of value.split("\n")) {
47
+ if (line.length === 0)
48
+ continue;
49
+ summary.lines += 1;
50
+ if (Buffer.byteLength(line) > 1_048_576) {
51
+ summary.invalid_lines += 1;
52
+ continue;
53
+ }
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(line);
57
+ }
58
+ catch {
59
+ summary.invalid_lines += 1;
60
+ continue;
61
+ }
62
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
63
+ summary.invalid_lines += 1;
64
+ continue;
65
+ }
66
+ const event = parsed;
67
+ summary.parsed_lines += 1;
68
+ increment(summary.event_types, safeEventLabel(event.type));
69
+ if (typeof event.item === "object" && event.item !== null && !Array.isArray(event.item)) {
70
+ increment(summary.item_types, safeItemLabel(event.item.type));
71
+ }
72
+ }
73
+ summary.event_types = Object.fromEntries(Object.entries(summary.event_types).sort());
74
+ summary.item_types = Object.fromEntries(Object.entries(summary.item_types).sort());
75
+ return summary;
76
+ }
77
+ function safeDiagnostic(value) {
78
+ const normalized = value
79
+ .replace(/[\u0000-\u001f\u007f]+/gu, " ")
80
+ .replace(URL_WITH_QUERY, "[url-with-query-redacted]")
81
+ .replace(BEARER_TOKEN, "Bearer [redacted]")
82
+ .replace(SECRET_TOKEN, "[secret-redacted]")
83
+ .replace(SECRET_ASSIGNMENT, "[secret-assignment-redacted]")
84
+ .replace(/\s+/gu, " ")
85
+ .trim();
86
+ return [...normalized].slice(0, MAX_FAILURE_MESSAGE_CHARS).join("");
87
+ }
88
+ /**
89
+ * Extract a bounded diagnostic from Codex's documented JSONL failure events.
90
+ * Raw stderr and unstructured stdout are intentionally excluded because they
91
+ * can contain credential, host, or prompt material.
92
+ */
93
+ export function summarizeCodexFailure(value) {
94
+ const diagnostics = [];
95
+ const lines = value.split("\n").filter((line) => line.length > 0).slice(0, MAX_FAILURE_LINES);
96
+ for (const line of lines) {
97
+ if (Buffer.byteLength(line) > 1_048_576)
98
+ continue;
99
+ let event;
100
+ try {
101
+ const parsed = JSON.parse(line);
102
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
103
+ continue;
104
+ event = parsed;
105
+ }
106
+ catch {
107
+ continue;
108
+ }
109
+ let message;
110
+ if (event.type === "error") {
111
+ message = event.message;
112
+ }
113
+ else if (event.type === "turn.failed") {
114
+ const error = event.error;
115
+ if (typeof error === "object" && error !== null && !Array.isArray(error)) {
116
+ message = error.message;
117
+ }
118
+ }
119
+ else if ((event.type === "item.started" ||
120
+ event.type === "item.updated" ||
121
+ event.type === "item.completed") &&
122
+ typeof event.item === "object" &&
123
+ event.item !== null &&
124
+ !Array.isArray(event.item)) {
125
+ const item = event.item;
126
+ if (item.type === "error")
127
+ message = item.message;
128
+ }
129
+ if (typeof message !== "string" || message.length === 0)
130
+ continue;
131
+ const diagnostic = safeDiagnostic(message);
132
+ if (diagnostic.length > 0 && !diagnostics.includes(diagnostic))
133
+ diagnostics.push(diagnostic);
134
+ if (diagnostics.length === 3)
135
+ break;
136
+ }
137
+ return diagnostics.length === 0
138
+ ? "no structured Codex failure event"
139
+ : diagnostics.join("; ");
140
+ }
141
+ function usageInteger(value, at) {
142
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
143
+ throw new Error(`${at} must be a nonnegative integer`);
144
+ }
145
+ return value;
146
+ }
147
+ export function parseCodexEvents(value) {
148
+ const eventTypes = new Set();
149
+ const actionTypes = new Set();
150
+ const itemKinds = new Map();
151
+ let usage;
152
+ const lines = value.split("\n").filter((line) => line.length > 0);
153
+ if (lines.length === 0)
154
+ throw new Error("Codex returned no JSONL events");
155
+ for (const [index, line] of lines.entries()) {
156
+ if (Buffer.byteLength(line) > 1_048_576)
157
+ throw new Error(`Codex event ${index} is oversized`);
158
+ let parsed;
159
+ try {
160
+ parsed = JSON.parse(line);
161
+ }
162
+ catch {
163
+ throw new Error(`Codex event ${index} is invalid JSON`);
164
+ }
165
+ const event = objectAt(parsed, `codex_event[${index}]`);
166
+ const type = event.type;
167
+ if (typeof type !== "string" || !EVENT_TYPES.has(type)) {
168
+ throw new Error(`Codex event ${index} has unknown type ${String(type)}`);
169
+ }
170
+ eventTypes.add(type);
171
+ if (type === "thread.started" && (typeof event.thread_id !== "string" || event.thread_id === "")) {
172
+ throw new Error("Codex thread.started event has no thread_id");
173
+ }
174
+ if (type === "turn.completed") {
175
+ const rawUsage = objectAt(event.usage, "codex turn.completed.usage");
176
+ usage = {
177
+ input_tokens: usageInteger(rawUsage.input_tokens, "usage.input_tokens"),
178
+ cached_input_tokens: usageInteger(rawUsage.cached_input_tokens, "usage.cached_input_tokens"),
179
+ output_tokens: usageInteger(rawUsage.output_tokens, "usage.output_tokens"),
180
+ reasoning_output_tokens: usageInteger(rawUsage.reasoning_output_tokens, "usage.reasoning_output_tokens"),
181
+ };
182
+ }
183
+ if (type === "item.started" || type === "item.updated" || type === "item.completed") {
184
+ const item = objectAt(event.item, `codex ${type}.item`);
185
+ if (typeof item.id !== "string" || item.id === "" || typeof item.type !== "string") {
186
+ throw new Error(`Codex ${type} has a malformed item`);
187
+ }
188
+ const previous = itemKinds.get(item.id);
189
+ if (previous !== undefined && previous !== item.type) {
190
+ throw new Error(`Codex item ${item.id} changed type`);
191
+ }
192
+ itemKinds.set(item.id, item.type);
193
+ if (!PASSIVE_ITEMS.has(item.type) && !ACTION_ITEMS.has(item.type)) {
194
+ throw new Error(`Codex emitted unsupported action type ${item.type}`);
195
+ }
196
+ if (ACTION_ITEMS.has(item.type))
197
+ actionTypes.add(item.type);
198
+ if (type === "item.completed" && item.type === "command_execution") {
199
+ if (typeof item.command !== "string" || item.command === "") {
200
+ throw new Error("Codex completed command has no command text");
201
+ }
202
+ if (FORBIDDEN_COMMAND.test(item.command)) {
203
+ throw new Error(`Codex attempted a forbidden external or custody action: ${item.command}`);
204
+ }
205
+ }
206
+ }
207
+ }
208
+ for (const required of ["thread.started", "turn.started", "turn.completed"]) {
209
+ if (!eventTypes.has(required))
210
+ throw new Error(`Codex event stream is missing ${required}`);
211
+ }
212
+ if (usage === undefined)
213
+ throw new Error("Codex event stream has no usage record");
214
+ return {
215
+ usage,
216
+ eventTypes: [...eventTypes].sort(),
217
+ actionTypes: [...actionTypes].sort(),
218
+ };
219
+ }
@@ -0,0 +1,17 @@
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
+ }
@@ -0,0 +1,136 @@
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
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Stage only the credential and optional model catalog needed by Codex into a
3
+ * fresh, disposable home. Native Codex writes installation_id during startup;
4
+ * it must never receive write access to the user's real credential directory.
5
+ */
6
+ export declare function prepareIsolatedCodexHome(sourceHome: string, runtimeRoot: string, config?: Uint8Array): Promise<string>;
7
+ export declare function removeIsolatedCodexHome(codexHome: string): Promise<void>;
@@ -0,0 +1,50 @@
1
+ import path from "node:path";
2
+ import { mkdir, rm, writeFile } from "node:fs/promises";
3
+ import { readBoundedRegularFile } from "../util/files.js";
4
+ const AUTH_MAX_BYTES = 2 * 1024 * 1024;
5
+ const MODEL_CATALOG_MAX_BYTES = 32 * 1024 * 1024;
6
+ async function optionalBoundedFile(file, maxBytes) {
7
+ try {
8
+ return await readBoundedRegularFile(file, maxBytes);
9
+ }
10
+ catch (error) {
11
+ if (error.code === "ENOENT")
12
+ return undefined;
13
+ throw error;
14
+ }
15
+ }
16
+ /**
17
+ * Stage only the credential and optional model catalog needed by Codex into a
18
+ * fresh, disposable home. Native Codex writes installation_id during startup;
19
+ * it must never receive write access to the user's real credential directory.
20
+ */
21
+ export async function prepareIsolatedCodexHome(sourceHome, runtimeRoot, config) {
22
+ const source = path.resolve(sourceHome);
23
+ const destination = path.join(path.resolve(runtimeRoot), "codex-runtime");
24
+ await mkdir(destination, { mode: 0o700 });
25
+ try {
26
+ const auth = await readBoundedRegularFile(path.join(source, "auth.json"), AUTH_MAX_BYTES);
27
+ const modelCatalog = await optionalBoundedFile(path.join(source, "models_cache.json"), MODEL_CATALOG_MAX_BYTES);
28
+ await writeFile(path.join(destination, "auth.json"), auth, { flag: "wx", mode: 0o600 });
29
+ if (modelCatalog !== undefined) {
30
+ await writeFile(path.join(destination, "models_cache.json"), modelCatalog, {
31
+ flag: "wx",
32
+ mode: 0o600,
33
+ });
34
+ }
35
+ if (config !== undefined) {
36
+ await writeFile(path.join(destination, "config.toml"), config, {
37
+ flag: "wx",
38
+ mode: 0o600,
39
+ });
40
+ }
41
+ return destination;
42
+ }
43
+ catch (error) {
44
+ await rm(destination, { recursive: true, force: true });
45
+ throw error;
46
+ }
47
+ }
48
+ export async function removeIsolatedCodexHome(codexHome) {
49
+ await rm(codexHome, { recursive: true, force: true });
50
+ }
@@ -0,0 +1,26 @@
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[];