@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,20 @@
1
+ # Erdős 505 dimension-one verifier
2
+
3
+ This target-specific capsule verifies one raw Lean proof term against the exact
4
+ statement of `Erdos505.erdos_505.test_dim_one` from
5
+ `google-deepmind/formal-conjectures` commit
6
+ `c252a41054125b5fd9c8356e2137cd9b55337657`.
7
+
8
+ The image build verifies that exact upstream commit, tree, and source-file
9
+ hash, but the runtime deliberately does not import the admitted upstream
10
+ theorem or the `FormalConjecturesUtil` whole-Mathlib umbrella. It retains only
11
+ the transitive cache closure of `Mathlib.Analysis.InnerProductSpace.PiL2` and
12
+ `Mathlib.Topology.MetricSpace.Bounded`; the reviewed capsule owns an exact copy
13
+ of the target signature.
14
+
15
+ The producer supplies only the term beginning with `by`. The capsule invokes
16
+ Lean 4.27.0 inside the pinned verifier image and rejects `sorryAx` or any axiom
17
+ outside `propext`, `Classical.choice`, and `Quot.sound`. Kernel success does not
18
+ by itself establish statement fidelity or scientific acceptance; the exact
19
+ source-bound target packet, capsule review, and Vela review boundary retain
20
+ those distinctions.
@@ -0,0 +1,105 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ if [ "$#" -ne 1 ]; then
5
+ echo "usage: verifier <proof-term>" >&2
6
+ exit 2
7
+ fi
8
+
9
+ proof=$1
10
+ if [ ! -f "$proof" ] || [ -L "$proof" ]; then
11
+ echo "proof artifact must be one regular file" >&2
12
+ exit 2
13
+ fi
14
+
15
+ size=$(wc -c < "$proof" | tr -d ' ')
16
+ if [ "$size" -eq 0 ] || [ "$size" -gt 131072 ]; then
17
+ echo "proof artifact must contain 1..131072 bytes" >&2
18
+ exit 2
19
+ fi
20
+
21
+ if [ "$(head -c 2 "$proof")" != "by" ]; then
22
+ echo "proof artifact must begin with a Lean 'by' term" >&2
23
+ exit 2
24
+ fi
25
+
26
+ if LC_ALL=C grep -Eq '(^|[^A-Za-z0-9_])(sorry|admit|axiom|unsafe)([^A-Za-z0-9_]|$)' "$proof"; then
27
+ echo "proof artifact contains a forbidden trust-bypassing token" >&2
28
+ exit 2
29
+ fi
30
+
31
+ toolchain=/home/lean/.elan/toolchains/leanprover--lean4---v4.27.0/bin
32
+ project=/home/lean/mathlib
33
+ if [ ! -x "$toolchain/lean" ]; then
34
+ echo "frozen Lean 4.27.0 toolchain is unavailable" >&2
35
+ exit 3
36
+ fi
37
+ if [ ! -f "$project/.lake/build/lib/lean/Mathlib/Analysis/InnerProductSpace/PiL2.olean" ] \
38
+ || [ ! -f "$project/.lake/build/lib/lean/Mathlib/Topology/MetricSpace/Bounded.olean" ]; then
39
+ echo "frozen minimal Mathlib modules are unavailable" >&2
40
+ exit 3
41
+ fi
42
+
43
+ export PATH="$toolchain:/usr/local/bin:/usr/bin:/bin"
44
+ export LC_ALL=C
45
+ LEAN_PATH=$(
46
+ find "$project" -type d -path '*/.lake/build/lib/lean' -print \
47
+ | LC_ALL=C sort \
48
+ | paste -sd ':' -
49
+ )
50
+ if [ -z "$LEAN_PATH" ]; then
51
+ echo "frozen Mathlib module path is unavailable" >&2
52
+ exit 3
53
+ fi
54
+ export LEAN_PATH
55
+ cd "$project"
56
+
57
+ set +e
58
+ output=$(
59
+ {
60
+ printf '%s\n' \
61
+ 'import Mathlib.Analysis.InnerProductSpace.PiL2' \
62
+ 'import Mathlib.Topology.MetricSpace.Bounded' \
63
+ '/-! # Frozen Canopus verification for Erdos 505, dimension one -/' \
64
+ 'open Metric Set Classical' \
65
+ 'namespace CanopusFormalErdos505' \
66
+ 'theorem test_dim_one' \
67
+ ' (S : Set (EuclideanSpace ℝ (Fin 1)))' \
68
+ ' (hS : Bornology.IsBounded S) (hd : 0 < diam S) :' \
69
+ ' ∃ (F : Fin 2 → Set (EuclideanSpace ℝ (Fin 1))),' \
70
+ ' S ⊆ ⋃ i, F i ∧ ∀ i, diam (F i) < diam S :='
71
+ cat "$proof"
72
+ printf '%s\n' \
73
+ '#print axioms CanopusFormalErdos505.test_dim_one' \
74
+ 'end CanopusFormalErdos505'
75
+ } | "$toolchain/lean" --stdin 2>&1
76
+ )
77
+ status=$?
78
+ set -e
79
+
80
+ printf '%s\n' "$output"
81
+ if [ "$status" -ne 0 ]; then
82
+ exit "$status"
83
+ fi
84
+ if printf '%s\n' "$output" | grep -Fq 'sorryAx'; then
85
+ echo "kernel declaration depends on sorryAx" >&2
86
+ exit 4
87
+ fi
88
+
89
+ axiom_line=$(printf '%s\n' "$output" | sed -n "s/^'CanopusFormalErdos505.test_dim_one' depends on axioms: \[\(.*\)\]$/\1/p")
90
+ if [ -n "$axiom_line" ]; then
91
+ for axiom in $(printf '%s' "$axiom_line" | tr ',' ' '); do
92
+ case "$axiom" in
93
+ propext|Classical.choice|Quot.sound) ;;
94
+ *)
95
+ echo "kernel declaration depends on forbidden axiom: $axiom" >&2
96
+ exit 4
97
+ ;;
98
+ esac
99
+ done
100
+ elif ! printf '%s\n' "$output" | grep -Fq "'CanopusFormalErdos505.test_dim_one' does not depend on any axioms"; then
101
+ echo "axiom audit output was missing or malformed" >&2
102
+ exit 4
103
+ fi
104
+
105
+ echo "verified formal:erdos-505-test-dim-one with Lean 4.27.0"
@@ -0,0 +1,15 @@
1
+ export declare const ACTIVITY_SCHEMA: "canopus.activity.v0";
2
+ export declare const ACTIVITY_TYPES: readonly ["run.started", "workspace.prepared", "roots.verified", "target.offered", "work.skipped", "work.claimed", "engine.started", "engine.completed", "artifact.frozen", "artifacts.published", "verifier.completed", "candidate.finalized", "receipt.mapped", "landing.observed", "landing.bound", "landing.completed", "withdrawal_capability.retained", "reproduction.completed", "projection.written", "run.completed", "run.failed"];
3
+ export type ActivityType = (typeof ACTIVITY_TYPES)[number];
4
+ export interface ActivityEventBody {
5
+ schema: typeof ACTIVITY_SCHEMA;
6
+ run_id: string;
7
+ sequence: number;
8
+ at: string;
9
+ type: ActivityType;
10
+ previous: string | null;
11
+ payload: Record<string, unknown>;
12
+ }
13
+ export interface ActivityEvent extends ActivityEventBody {
14
+ event_digest: string;
15
+ }
@@ -0,0 +1,24 @@
1
+ export const ACTIVITY_SCHEMA = "canopus.activity.v0";
2
+ export const ACTIVITY_TYPES = [
3
+ "run.started",
4
+ "workspace.prepared",
5
+ "roots.verified",
6
+ "target.offered",
7
+ "work.skipped",
8
+ "work.claimed",
9
+ "engine.started",
10
+ "engine.completed",
11
+ "artifact.frozen",
12
+ "artifacts.published",
13
+ "verifier.completed",
14
+ "candidate.finalized",
15
+ "receipt.mapped",
16
+ "landing.observed",
17
+ "landing.bound",
18
+ "landing.completed",
19
+ "withdrawal_capability.retained",
20
+ "reproduction.completed",
21
+ "projection.written",
22
+ "run.completed",
23
+ "run.failed",
24
+ ];
@@ -0,0 +1,10 @@
1
+ import { type ActivityEvent, type ActivityType } from "./events.js";
2
+ export declare function readActivity(file: string): Promise<ActivityEvent[]>;
3
+ export declare class ActivityStore {
4
+ #private;
5
+ private constructor();
6
+ static open(file: string, runId: string): Promise<ActivityStore>;
7
+ get tip(): string | null;
8
+ get events(): readonly ActivityEvent[];
9
+ append(type: ActivityType, payload: Record<string, unknown>): Promise<ActivityEvent>;
10
+ }
@@ -0,0 +1,165 @@
1
+ import { constants } from "node:fs";
2
+ import { mkdir, open } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { canonicalJson, contentDigest } from "../util/canonical.js";
5
+ import { readBoundedRegularFile } from "../util/files.js";
6
+ import { ACTIVITY_SCHEMA, ACTIVITY_TYPES, } from "./events.js";
7
+ const MAX_ACTIVITY_BYTES = 64 * 1024 * 1024;
8
+ const MAX_EVENT_BYTES = 1024 * 1024;
9
+ function parseEvent(value, line) {
10
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
11
+ throw new Error(`activity line ${line} is not an object`);
12
+ }
13
+ const event = value;
14
+ const expected = new Set([
15
+ "schema",
16
+ "run_id",
17
+ "sequence",
18
+ "at",
19
+ "type",
20
+ "previous",
21
+ "payload",
22
+ "event_digest",
23
+ ]);
24
+ if (Object.keys(event).some((key) => !expected.has(key)) || Object.keys(event).length !== expected.size) {
25
+ throw new Error(`activity line ${line} has an invalid field set`);
26
+ }
27
+ if (event.schema !== ACTIVITY_SCHEMA ||
28
+ typeof event.run_id !== "string" ||
29
+ event.run_id.length === 0 ||
30
+ typeof event.sequence !== "number" ||
31
+ !Number.isSafeInteger(event.sequence) ||
32
+ event.sequence < 0 ||
33
+ typeof event.at !== "string" ||
34
+ Number.isNaN(Date.parse(event.at)) ||
35
+ typeof event.type !== "string" ||
36
+ !ACTIVITY_TYPES.includes(event.type) ||
37
+ (event.previous !== null && typeof event.previous !== "string") ||
38
+ typeof event.payload !== "object" ||
39
+ event.payload === null ||
40
+ Array.isArray(event.payload) ||
41
+ typeof event.event_digest !== "string") {
42
+ throw new Error(`activity line ${line} is malformed`);
43
+ }
44
+ const body = {
45
+ schema: ACTIVITY_SCHEMA,
46
+ run_id: event.run_id,
47
+ sequence: event.sequence,
48
+ at: event.at,
49
+ type: event.type,
50
+ previous: event.previous,
51
+ payload: event.payload,
52
+ };
53
+ const expectedDigest = contentDigest(body);
54
+ if (event.event_digest !== expectedDigest) {
55
+ throw new Error(`activity line ${line} has an invalid event digest`);
56
+ }
57
+ return { ...body, event_digest: expectedDigest };
58
+ }
59
+ export async function readActivity(file) {
60
+ let bytes;
61
+ try {
62
+ bytes = await readBoundedRegularFile(file, MAX_ACTIVITY_BYTES);
63
+ }
64
+ catch (error) {
65
+ if (error.code === "ENOENT")
66
+ return [];
67
+ throw error;
68
+ }
69
+ const text = bytes.toString("utf8");
70
+ if (text.length !== 0 && !text.endsWith("\n")) {
71
+ throw new Error("activity log ends with a partial record");
72
+ }
73
+ const lines = text.split("\n").filter((line) => line.length > 0);
74
+ const events = [];
75
+ for (const [index, line] of lines.entries()) {
76
+ if (Buffer.byteLength(line) > MAX_EVENT_BYTES)
77
+ throw new Error(`activity line ${index + 1} is oversized`);
78
+ let value;
79
+ try {
80
+ value = JSON.parse(line);
81
+ }
82
+ catch {
83
+ throw new Error(`activity line ${index + 1} is invalid JSON`);
84
+ }
85
+ const event = parseEvent(value, index + 1);
86
+ const previous = events.at(-1);
87
+ if (event.sequence !== index || event.previous !== (previous?.event_digest ?? null)) {
88
+ throw new Error(`activity line ${index + 1} breaks the append-only chain`);
89
+ }
90
+ if (previous !== undefined && event.run_id !== previous.run_id) {
91
+ throw new Error(`activity line ${index + 1} changes run_id`);
92
+ }
93
+ events.push(event);
94
+ }
95
+ return events;
96
+ }
97
+ export class ActivityStore {
98
+ #file;
99
+ #runId;
100
+ #events;
101
+ #queue = Promise.resolve();
102
+ constructor(file, runId, events) {
103
+ this.#file = file;
104
+ this.#runId = runId;
105
+ this.#events = events;
106
+ }
107
+ static async open(file, runId) {
108
+ if (!/^run_[a-z0-9-]{8,128}$/u.test(runId))
109
+ throw new Error("invalid run_id");
110
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
111
+ const events = await readActivity(file);
112
+ if (events.some((event) => event.run_id !== runId)) {
113
+ throw new Error("activity log belongs to a different run");
114
+ }
115
+ return new ActivityStore(file, runId, events);
116
+ }
117
+ get tip() {
118
+ return this.#events.at(-1)?.event_digest ?? null;
119
+ }
120
+ get events() {
121
+ return this.#events;
122
+ }
123
+ async append(type, payload) {
124
+ let resolveEvent;
125
+ let rejectEvent;
126
+ const result = new Promise((resolve, reject) => {
127
+ resolveEvent = resolve;
128
+ rejectEvent = reject;
129
+ });
130
+ this.#queue = this.#queue.then(async () => {
131
+ try {
132
+ if (!ACTIVITY_TYPES.includes(type))
133
+ throw new Error(`unknown activity type ${type}`);
134
+ const body = {
135
+ schema: ACTIVITY_SCHEMA,
136
+ run_id: this.#runId,
137
+ sequence: this.#events.length,
138
+ at: new Date().toISOString(),
139
+ type,
140
+ previous: this.tip,
141
+ payload,
142
+ };
143
+ const event = { ...body, event_digest: contentDigest(body) };
144
+ const line = canonicalJson(event);
145
+ if (Buffer.byteLength(line) > MAX_EVENT_BYTES)
146
+ throw new Error("activity event is oversized");
147
+ const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
148
+ const handle = await open(this.#file, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | noFollow, 0o600);
149
+ try {
150
+ await handle.writeFile(line);
151
+ await handle.sync();
152
+ }
153
+ finally {
154
+ await handle.close();
155
+ }
156
+ this.#events = [...this.#events, event];
157
+ resolveEvent(event);
158
+ }
159
+ catch (error) {
160
+ rejectEvent(error);
161
+ }
162
+ });
163
+ return await result;
164
+ }
165
+ }
@@ -0,0 +1,21 @@
1
+ import type { FrozenArtifact } from "../contracts/candidate.js";
2
+ export declare class ArtifactError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export interface FrozenArtifactLocation {
6
+ artifact: FrozenArtifact;
7
+ frozenPath: string;
8
+ }
9
+ export declare function hashRegularFile(file: string, maxBytes: number): Promise<{
10
+ digest: string;
11
+ bytes: number;
12
+ }>;
13
+ export declare function freezeArtifact(options: {
14
+ sourceRoot: string;
15
+ artifactRoot: string;
16
+ path: string;
17
+ kind: string;
18
+ maxBytes: number;
19
+ }): Promise<FrozenArtifactLocation>;
20
+ export declare function sealArtifactStore(root: string): Promise<void>;
21
+ export declare function verifyFrozenArtifact(frozen: FrozenArtifactLocation, maxBytes: number): Promise<void>;
@@ -0,0 +1,177 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { chmod, link, lstat, mkdir, open, readdir, unlink, } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { relativePathAt } from "../contracts/validation.js";
6
+ export class ArtifactError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "ArtifactError";
10
+ }
11
+ }
12
+ function below(root, child) {
13
+ const relative = path.relative(root, child);
14
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
15
+ }
16
+ export async function hashRegularFile(file, maxBytes) {
17
+ const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
18
+ const handle = await open(file, constants.O_RDONLY | noFollow);
19
+ try {
20
+ const before = await handle.stat();
21
+ if (!before.isFile() || before.nlink !== 1) {
22
+ throw new ArtifactError("artifact must be one singly linked regular file");
23
+ }
24
+ if (before.size > maxBytes) {
25
+ throw new ArtifactError(`artifact exceeds ${maxBytes} bytes`);
26
+ }
27
+ const hash = createHash("sha256");
28
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, Math.max(1, maxBytes + 1)));
29
+ let bytes = 0;
30
+ while (true) {
31
+ const remaining = maxBytes + 1 - bytes;
32
+ if (remaining <= 0)
33
+ throw new ArtifactError(`artifact exceeds ${maxBytes} bytes`);
34
+ const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, remaining), null);
35
+ if (bytesRead === 0)
36
+ break;
37
+ bytes += bytesRead;
38
+ if (bytes > maxBytes)
39
+ throw new ArtifactError(`artifact exceeds ${maxBytes} bytes`);
40
+ hash.update(buffer.subarray(0, bytesRead));
41
+ }
42
+ const after = await handle.stat();
43
+ if (before.dev !== after.dev ||
44
+ before.ino !== after.ino ||
45
+ before.size !== after.size ||
46
+ before.mtimeMs !== after.mtimeMs ||
47
+ bytes !== before.size) {
48
+ throw new ArtifactError("artifact changed while it was being hashed");
49
+ }
50
+ return { digest: `sha256:${hash.digest("hex")}`, bytes };
51
+ }
52
+ finally {
53
+ await handle.close();
54
+ }
55
+ }
56
+ async function copyBounded(source, target, maxBytes) {
57
+ const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
58
+ const sourceHandle = await open(source, constants.O_RDONLY | noFollow);
59
+ const targetHandle = await open(target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
60
+ try {
61
+ const before = await sourceHandle.stat();
62
+ if (!before.isFile() || before.nlink !== 1) {
63
+ throw new ArtifactError("artifact must be one singly linked regular file");
64
+ }
65
+ if (before.size > maxBytes)
66
+ throw new ArtifactError(`artifact exceeds ${maxBytes} bytes`);
67
+ const hash = createHash("sha256");
68
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, Math.max(1, maxBytes + 1)));
69
+ let bytes = 0;
70
+ while (true) {
71
+ const remaining = maxBytes + 1 - bytes;
72
+ if (remaining <= 0)
73
+ throw new ArtifactError(`artifact exceeds ${maxBytes} bytes`);
74
+ const { bytesRead } = await sourceHandle.read(buffer, 0, Math.min(buffer.length, remaining), null);
75
+ if (bytesRead === 0)
76
+ break;
77
+ bytes += bytesRead;
78
+ if (bytes > maxBytes)
79
+ throw new ArtifactError(`artifact exceeds ${maxBytes} bytes`);
80
+ hash.update(buffer.subarray(0, bytesRead));
81
+ let offset = 0;
82
+ while (offset < bytesRead) {
83
+ const write = await targetHandle.write(buffer, offset, bytesRead - offset, null);
84
+ offset += write.bytesWritten;
85
+ }
86
+ }
87
+ await targetHandle.sync();
88
+ const after = await sourceHandle.stat();
89
+ if (before.dev !== after.dev ||
90
+ before.ino !== after.ino ||
91
+ before.size !== after.size ||
92
+ before.mtimeMs !== after.mtimeMs ||
93
+ bytes !== before.size) {
94
+ throw new ArtifactError("artifact changed while it was being frozen");
95
+ }
96
+ return { digest: `sha256:${hash.digest("hex")}`, bytes };
97
+ }
98
+ finally {
99
+ await Promise.all([sourceHandle.close(), targetHandle.close()]);
100
+ }
101
+ }
102
+ export async function freezeArtifact(options) {
103
+ const relative = relativePathAt(options.path, "artifact.path");
104
+ if (options.kind.length === 0 || options.kind.length > 128) {
105
+ throw new ArtifactError("artifact kind length must be 1..128");
106
+ }
107
+ const sourceRoot = path.resolve(options.sourceRoot);
108
+ const source = path.resolve(sourceRoot, relative);
109
+ if (!below(sourceRoot, source)) {
110
+ throw new ArtifactError("artifact path escapes the worker output root");
111
+ }
112
+ const sourceStat = await lstat(source);
113
+ if (sourceStat.isSymbolicLink())
114
+ throw new ArtifactError("artifact path is a symbolic link");
115
+ await mkdir(options.artifactRoot, { recursive: true, mode: 0o700 });
116
+ const temporary = path.join(options.artifactRoot, `.freeze-${randomUUID()}`);
117
+ let copied;
118
+ try {
119
+ copied = await copyBounded(source, temporary, options.maxBytes);
120
+ const frozenPath = path.join(options.artifactRoot, copied.digest.slice(7));
121
+ try {
122
+ await link(temporary, frozenPath);
123
+ await chmod(frozenPath, 0o444);
124
+ }
125
+ catch (error) {
126
+ if (error.code !== "EEXIST")
127
+ throw error;
128
+ const existing = await hashRegularFile(frozenPath, options.maxBytes);
129
+ if (existing.digest !== copied.digest || existing.bytes !== copied.bytes) {
130
+ throw new ArtifactError("existing content-addressed artifact does not match its name");
131
+ }
132
+ }
133
+ await unlink(temporary);
134
+ const frozen = await hashRegularFile(frozenPath, options.maxBytes);
135
+ if (frozen.digest !== copied.digest || frozen.bytes !== copied.bytes) {
136
+ throw new ArtifactError("frozen artifact failed its immediate digest check");
137
+ }
138
+ return {
139
+ artifact: { path: relative, kind: options.kind, digest: copied.digest, bytes: copied.bytes },
140
+ frozenPath,
141
+ };
142
+ }
143
+ catch (error) {
144
+ try {
145
+ await unlink(temporary);
146
+ }
147
+ catch (unlinkError) {
148
+ if (unlinkError.code !== "ENOENT")
149
+ throw unlinkError;
150
+ }
151
+ throw error;
152
+ }
153
+ }
154
+ export async function sealArtifactStore(root) {
155
+ const visit = async (current) => {
156
+ const stat = await lstat(current);
157
+ if (stat.isSymbolicLink())
158
+ throw new ArtifactError("artifact store contains a symbolic link");
159
+ if (stat.isDirectory()) {
160
+ for (const child of await readdir(current))
161
+ await visit(path.join(current, child));
162
+ await chmod(current, 0o555);
163
+ return;
164
+ }
165
+ if (!stat.isFile() || stat.nlink !== 1) {
166
+ throw new ArtifactError("artifact store contains a non-regular or linked file");
167
+ }
168
+ await chmod(current, 0o444);
169
+ };
170
+ await visit(root);
171
+ }
172
+ export async function verifyFrozenArtifact(frozen, maxBytes) {
173
+ const observed = await hashRegularFile(frozen.frozenPath, maxBytes);
174
+ if (observed.digest !== frozen.artifact.digest || observed.bytes !== frozen.artifact.bytes) {
175
+ throw new ArtifactError(`frozen artifact ${frozen.artifact.path} no longer matches its record`);
176
+ }
177
+ }
@@ -0,0 +1,6 @@
1
+ import type { CandidateDraft } from "../engines/engine.js";
2
+ export declare function materializeDraftArtifacts(options: {
3
+ draft: CandidateDraft;
4
+ outputRoot: string;
5
+ maxTotalBytes: number;
6
+ }): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ function below(root, candidate) {
4
+ const relative = path.relative(root, candidate);
5
+ return (relative !== "" &&
6
+ relative !== ".." &&
7
+ !relative.startsWith(`..${path.sep}`) &&
8
+ !path.isAbsolute(relative));
9
+ }
10
+ export async function materializeDraftArtifacts(options) {
11
+ const root = path.resolve(options.outputRoot);
12
+ const seen = new Set();
13
+ let total = 0;
14
+ for (const artifact of options.draft.artifacts) {
15
+ if (seen.has(artifact.path))
16
+ throw new Error(`duplicate engine artifact ${artifact.path}`);
17
+ seen.add(artifact.path);
18
+ const target = path.resolve(root, artifact.path);
19
+ if (!below(root, target))
20
+ throw new Error(`engine artifact escapes output root: ${artifact.path}`);
21
+ const bytes = Buffer.from(artifact.content, "utf8");
22
+ total += bytes.length;
23
+ if (total > options.maxTotalBytes) {
24
+ throw new Error(`inline engine artifacts exceed ${options.maxTotalBytes} bytes`);
25
+ }
26
+ await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
27
+ await writeFile(target, bytes, { flag: "wx", mode: 0o600 });
28
+ }
29
+ }
@@ -0,0 +1,30 @@
1
+ import type { MissionBudgets } from "../contracts/mission.js";
2
+ export type BudgetDimension = "max_research_wall_time_ms" | "max_research_processes" | "max_research_output_bytes" | "max_prompt_bytes" | "max_artifact_bytes" | "max_attempts" | "max_observed_tokens";
3
+ export declare class BudgetExceeded extends Error {
4
+ readonly dimension: BudgetDimension;
5
+ constructor(dimension: BudgetDimension, used: number, limit: number);
6
+ }
7
+ export interface BudgetSnapshot {
8
+ research_elapsed_ms: number;
9
+ research_processes: number;
10
+ research_output_bytes: number;
11
+ prompt_bytes: number;
12
+ artifact_bytes: number;
13
+ attempts: number;
14
+ observed_tokens: number;
15
+ }
16
+ export declare class BudgetTracker {
17
+ #private;
18
+ constructor(limits: MissionBudgets, now?: () => number);
19
+ assertTime(): number;
20
+ beginAttempt(): number;
21
+ beginProcess(): number;
22
+ addOutput(bytes: number): number;
23
+ addPrompt(bytes: number): number;
24
+ addArtifact(bytes: number): number;
25
+ addTokens(tokens: number): number;
26
+ remainingTimeMs(): number;
27
+ remainingOutputBytes(): number;
28
+ remainingArtifactBytes(): number;
29
+ snapshot(): BudgetSnapshot;
30
+ }