@attest-ai/core 1.0.0 → 1.1.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.
package/dist/esm/cli.js CHANGED
@@ -3,30 +3,34 @@
3
3
  // src/cli.ts
4
4
  import { spawnSync as spawnSync2 } from "child_process";
5
5
  import * as fs4 from "fs";
6
- import * as os2 from "os";
6
+ import * as os3 from "os";
7
7
  import * as path4 from "path";
8
- import { fileURLToPath as fileURLToPath2 } from "url";
8
+ import { fileURLToPath as fileURLToPath3 } from "url";
9
9
 
10
10
  // src/version.ts
11
- var VERSION = "1.0.0";
12
- var ENGINE_VERSION = "1.0.0";
11
+ var VERSION = "1.1.0-rc.1";
12
+ var ENGINE_VERSION = "1.1.0-rc.1";
13
13
 
14
14
  // src/engine-manager.ts
15
15
  import { spawn, spawnSync } from "child_process";
16
16
  import { createInterface } from "readline";
17
- import { existsSync, accessSync, statSync as statSync2, constants as fsConstants } from "fs";
17
+ import { existsSync as existsSync2, accessSync, statSync as statSync2, constants as fsConstants } from "fs";
18
18
  import { delimiter, join as join2 } from "path";
19
19
 
20
20
  // src/engine-downloader.ts
21
21
  import * as https from "https";
22
- import * as http from "http";
23
22
  import * as fs from "fs";
24
23
  import * as path from "path";
25
24
  import * as os from "os";
26
25
  import * as crypto from "crypto";
26
+ import { fileURLToPath } from "url";
27
27
  var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
28
- var CHECKSUMS_URL = `${RELEASE_BASE}/checksums-sha256.txt`;
28
+ var TABLE_NAME = "engine-checksums.json";
29
29
  var MAX_REDIRECTS = 5;
30
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
31
+ var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
32
+ var DEFAULT_TOTAL_TIMEOUT_MS = 12e4;
33
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
30
34
  function versionedBinDir() {
31
35
  return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
32
36
  }
@@ -59,73 +63,131 @@ function cachedEnginePath() {
59
63
  const binPath = path.join(versionedBinDir(), binaryFilename());
60
64
  return fs.statSync(binPath, { throwIfNoEntry: false })?.isFile() ? binPath : null;
61
65
  }
62
- function parseChecksums(text) {
63
- const checksums = /* @__PURE__ */ new Map();
64
- for (const line of text.split("\n")) {
65
- const trimmed = line.trim();
66
- if (!trimmed) continue;
67
- const match = trimmed.match(/^([a-f0-9]{64})\s+(.+)$/);
68
- if (match) {
69
- checksums.set(match[2], match[1]);
70
- }
66
+ function packageRoot() {
67
+ let dir = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
68
+ while (!fs.existsSync(path.join(dir, "package.json"))) {
69
+ const parent = path.dirname(dir);
70
+ if (parent === dir) return dir;
71
+ dir = parent;
71
72
  }
72
- return checksums;
73
+ return dir;
73
74
  }
74
- function httpsGet(url, redirectCount = 0) {
75
- if (redirectCount > MAX_REDIRECTS) {
76
- return Promise.reject(
77
- new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
75
+ function embeddedHash(key, tablePath) {
76
+ if (!fs.existsSync(tablePath)) {
77
+ throw new Error(
78
+ `This @attest-ai/core install has no embedded engine hash table (${TABLE_NAME}), which is how a source or workspace install looks, so it never downloads an engine. Point ATTEST_ENGINE_PATH at an engine binary, or build one with \`make engine\` and set ATTEST_ENGINE_PATH=./bin/attest-engine.`
78
79
  );
79
80
  }
80
- return new Promise((resolve2, reject) => {
81
- const client = url.startsWith("https:") ? https : http;
82
- client.get(url, (res) => {
83
- const status = res.statusCode ?? 0;
84
- if (status === 301 || status === 302) {
85
- const location = res.headers.location;
86
- if (!location) {
87
- reject(new Error(`Redirect ${status} without Location header from ${url}`));
88
- return;
89
- }
90
- res.resume();
91
- resolve2(httpsGet(location, redirectCount + 1));
81
+ let hash;
82
+ try {
83
+ const table = JSON.parse(fs.readFileSync(tablePath, "utf-8"));
84
+ hash = table[ENGINE_VERSION]?.[key];
85
+ } catch (err) {
86
+ throw new Error(
87
+ `@attest-ai/core ${VERSION} ships an unreadable engine hash table at ${tablePath}: ${String(err)}`
88
+ );
89
+ }
90
+ if (typeof hash !== "string" || !SHA256_HEX.test(hash)) {
91
+ throw new Error(
92
+ `@attest-ai/core ${VERSION} ships no valid engine hash for v${ENGINE_VERSION} on ${key} in ${tablePath}.`
93
+ );
94
+ }
95
+ return hash;
96
+ }
97
+ function httpsGet(url, o) {
98
+ return new Promise((resolve3, reject) => {
99
+ let current;
100
+ let done = false;
101
+ const finish = (err, data) => {
102
+ if (done) return;
103
+ done = true;
104
+ clearTimeout(deadline);
105
+ if (err) {
106
+ current?.destroy();
107
+ reject(err);
108
+ } else {
109
+ resolve3(data);
110
+ }
111
+ };
112
+ const deadline = setTimeout(
113
+ () => finish(new Error(`Timed out after ${o.totalTimeoutMs}ms fetching ${url}`)),
114
+ o.totalTimeoutMs
115
+ );
116
+ const hop = (target, redirects) => {
117
+ if (redirects > MAX_REDIRECTS) {
118
+ finish(new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`));
119
+ return;
120
+ }
121
+ let parsed;
122
+ try {
123
+ parsed = new URL(target);
124
+ } catch {
125
+ finish(new Error(`Invalid URL: ${target}`));
92
126
  return;
93
127
  }
94
- if (status < 200 || status >= 300) {
95
- res.resume();
96
- reject(new Error(`HTTP ${status} fetching ${url}`));
128
+ if (parsed.protocol !== "https:") {
129
+ finish(new Error(`Refusing to download over a non-https URL: ${target}`));
97
130
  return;
98
131
  }
99
- const chunks = [];
100
- res.on("data", (chunk) => chunks.push(chunk));
101
- res.on("end", () => resolve2(Buffer.concat(chunks)));
102
- res.on("error", reject);
103
- }).on("error", reject);
132
+ current = https.get(parsed, { ca: o.ca, timeout: o.connectTimeoutMs }, (res) => {
133
+ const status = res.statusCode ?? 0;
134
+ if (REDIRECT_STATUSES.has(status)) {
135
+ res.resume();
136
+ const location = res.headers.location;
137
+ if (!location) {
138
+ finish(new Error(`Redirect ${status} without Location header from ${target}`));
139
+ return;
140
+ }
141
+ let next;
142
+ try {
143
+ next = new URL(location, parsed).toString();
144
+ } catch {
145
+ finish(new Error(`Invalid redirect Location ${JSON.stringify(location)} from ${target}`));
146
+ return;
147
+ }
148
+ hop(next, redirects + 1);
149
+ return;
150
+ }
151
+ if (status !== 200) {
152
+ res.resume();
153
+ finish(new Error(`HTTP ${status} fetching ${target}`));
154
+ return;
155
+ }
156
+ const chunks = [];
157
+ res.on("data", (chunk) => chunks.push(chunk));
158
+ res.on("end", () => finish(null, Buffer.concat(chunks)));
159
+ res.on("error", (err) => finish(err));
160
+ });
161
+ current.on(
162
+ "timeout",
163
+ () => finish(new Error(`No response within ${o.connectTimeoutMs}ms from ${target}`))
164
+ );
165
+ current.on("error", (err) => finish(err));
166
+ };
167
+ hop(url, 0);
104
168
  });
105
169
  }
106
- async function downloadEngine() {
170
+ async function downloadEngine(options2 = {}) {
107
171
  const key = platformKey();
108
172
  const filename = `attest-engine-${key}${os.platform() === "win32" ? ".exe" : ""}`;
109
- const binaryUrl = `${RELEASE_BASE}/${filename}`;
173
+ const expectedHash = embeddedHash(key, options2.tablePath ?? path.join(packageRoot(), TABLE_NAME));
174
+ const binaryUrl = `${options2.releaseBase ?? RELEASE_BASE}/${filename}`;
110
175
  process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
111
176
  `);
112
- process.stderr.write(`[attest] Fetching checksums...
113
- `);
114
- const checksumsText = (await httpsGet(CHECKSUMS_URL)).toString("utf-8");
115
- const checksums = parseChecksums(checksumsText);
116
- const expectedHash = checksums.get(filename);
117
- if (!expectedHash) {
118
- throw new Error(
119
- `No checksum found for '${filename}' in checksums-sha256.txt. Available files: ${[...checksums.keys()].join(", ")}`
120
- );
177
+ let data;
178
+ try {
179
+ data = await httpsGet(binaryUrl, {
180
+ ca: options2.ca,
181
+ connectTimeoutMs: options2.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
182
+ totalTimeoutMs: options2.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS
183
+ });
184
+ } catch (err) {
185
+ throw new Error(`Failed to download engine from ${binaryUrl}: ${err.message}`);
121
186
  }
122
- process.stderr.write(`[attest] Downloading ${binaryUrl}...
123
- `);
124
- const data = await httpsGet(binaryUrl);
125
187
  const actualHash = crypto.createHash("sha256").update(data).digest("hex");
126
188
  if (actualHash !== expectedHash) {
127
189
  throw new Error(
128
- `SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted. Retry or download manually.`
190
+ `${filename} does not match the hash shipped in @attest-ai/core ${VERSION}: expected ${expectedHash}, got ${actualHash}. Refusing to install it. Retry, or set ATTEST_ENGINE_PATH to a binary you trust.`
129
191
  );
130
192
  }
131
193
  process.stderr.write(`[attest] Checksum verified.
@@ -194,7 +256,7 @@ function onPath() {
194
256
  return null;
195
257
  }
196
258
  function requireFile(enginePath, label) {
197
- if (!existsSync(enginePath) || !statSync2(enginePath).isFile()) {
259
+ if (!existsSync2(enginePath) || !statSync2(enginePath).isFile()) {
198
260
  throw new Error(`${label}=${enginePath} does not exist or is not a file.`);
199
261
  }
200
262
  }
@@ -402,8 +464,9 @@ Next: run \`npx vitest run\`. Then change the line marked ${DEMO_MARKER} in test
402
464
  // src/capture.ts
403
465
  import { spawn as spawn2 } from "child_process";
404
466
  import * as fs3 from "fs";
467
+ import * as os2 from "os";
405
468
  import * as path3 from "path";
406
- import { fileURLToPath } from "url";
469
+ import { fileURLToPath as fileURLToPath2 } from "url";
407
470
  var FIXTURE_VERSION = 1;
408
471
  var DEFAULT_FIXTURE_DIR = path3.join("tests", "attest", "fixtures");
409
472
  var CaptureError = class extends Error {
@@ -416,7 +479,7 @@ var CaptureError = class extends Error {
416
479
  }
417
480
  };
418
481
  function toPath(p) {
419
- return p instanceof URL ? fileURLToPath(p) : p;
482
+ return p instanceof URL ? fileURLToPath2(p) : p;
420
483
  }
421
484
  function loadFixture(file) {
422
485
  const p = toPath(file);
@@ -540,7 +603,75 @@ function testPathFor(fixturePath) {
540
603
  const name = path3.basename(fixturePath).replace(/\.attest\.json$/, "");
541
604
  return path3.join(path3.dirname(path3.dirname(fixturePath)), `${name}.test.ts`);
542
605
  }
543
- function renderVitest(fixture, fixturePath, testPath) {
606
+ function relUrl(target, testPath) {
607
+ const rel = path3.relative(path3.dirname(testPath), target).split(path3.sep).join("/");
608
+ const url = rel === "" ? "." : rel;
609
+ return `new URL(${ts(url.startsWith(".") ? url : `./${url}`)}, import.meta.url)`;
610
+ }
611
+ function chainLines(calls, subject, param) {
612
+ const lines = [`function contracts(${param}): ExpectChain {`, ` return attestExpect(${subject})`];
613
+ for (const [c, dsl] of calls) {
614
+ lines.push(` // ${comment(`${c.id}: ${c.description}`)}`, ` ${dsl}`);
615
+ }
616
+ lines[lines.length - 1] += ";";
617
+ lines.push("}");
618
+ return lines;
619
+ }
620
+ function skippedLines(skipped) {
621
+ if (skipped.length === 0) return [];
622
+ return ["", "// Drafted contracts without a TypeScript DSL call (left out):", ...skipped.map((s) => `// - ${comment(s)}`)];
623
+ }
624
+ function renderReplay(fixture, calls, skipped, fixturePath, testPath, rerun) {
625
+ const meaning = fixture.verification.expect_fail ? [
626
+ "// The recording is an incident: these contracts failed on it. The test passes once",
627
+ "// the new run satisfies them, so it fails again if the agent regresses."
628
+ ] : ["// The test fails when the agent's behavior stops satisfying them."];
629
+ const world = rerun.world === void 0 ? "undefined" : relUrl(rerun.world, testPath);
630
+ const lines = [
631
+ `// Replay test drafted by \`attest capture\` from fixture ${comment(ts(fixture.name))}.`,
632
+ "//",
633
+ "// Re-runs COMMAND, the source command of the fixture, under strict replay (the engine's",
634
+ "// `replay --mode strict --no-live`): OpenAI- and Anthropic-compatible calls are served",
635
+ "// from the recording, and one that matches no recording fails the test. Other providers",
636
+ "// are not blocked: run this test without their credentials. The drafted contracts are",
637
+ "// evaluated on the NEW run's trace, not on the recording.",
638
+ "//",
639
+ "// COMMAND is what this test runs and is reviewed with the test: if the fixture's",
640
+ "// source.command stops matching it, the test fails instead of running the fixture's.",
641
+ ...meaning,
642
+ "//",
643
+ "// A pass means the new run is consistent with the recording and satisfies the drafted",
644
+ "// contracts; it does not prove the agent correct. Identities are compared by role",
645
+ "// (relative identity): a run that swaps who plays which role misses, while the absolute",
646
+ "// identity is not checked. Review and edit before committing. Re-capturing does not",
647
+ "// overwrite this file.",
648
+ `import { attestExpect, replayRun, type ExpectChain } from "@attest-ai/core";`,
649
+ `import type { Trace } from "@attest-ai/core/proto";`,
650
+ `import { evaluate } from "@attest-ai/vitest";`,
651
+ `import { describe, it } from "vitest";`,
652
+ "",
653
+ `const FIXTURE = ${relUrl(fixturePath, testPath)};`,
654
+ "// The directory `attest capture` ran in: the source command's relative paths start here.",
655
+ `const RUN_DIR = ${relUrl(rerun.cwd, testPath)};`,
656
+ `const WORLD = ${world};`,
657
+ "// Reviewed argv: the only command this test runs.",
658
+ `const COMMAND = ${ts(fixture.source.command ?? [])};`,
659
+ ...skippedLines(skipped),
660
+ "",
661
+ ...chainLines(calls, "trace", "trace: Trace"),
662
+ "",
663
+ `describe(${ts(fixture.name)}, () => {`,
664
+ ` it("new run is consistent with the recording", async () => {`,
665
+ " const trace = await replayRun(FIXTURE, { command: COMMAND, cwd: RUN_DIR, world: WORLD });",
666
+ "",
667
+ " await evaluate(contracts(trace));",
668
+ " });",
669
+ "});"
670
+ ];
671
+ return `${lines.join("\n")}
672
+ `;
673
+ }
674
+ function renderVitest(fixture, fixturePath, testPath, rerun) {
544
675
  const calls = [];
545
676
  const skipped = [];
546
677
  for (const c of fixture.contracts) {
@@ -550,7 +681,9 @@ function renderVitest(fixture, fixturePath, testPath) {
550
681
  skipped.push(err.message);
551
682
  }
552
683
  }
553
- const rel = path3.relative(path3.dirname(testPath), fixturePath).split(path3.sep).join("/");
684
+ if (rerun !== void 0 && fixture.source.kind === "command" && fixture.source.replay_capable) {
685
+ return renderReplay(fixture, calls, skipped, fixturePath, testPath, rerun);
686
+ }
554
687
  const status = new Map(fixture.verification.results.map((r) => [r.contract, r.status]));
555
688
  const expectFail = fixture.verification.expect_fail;
556
689
  const lines = [
@@ -562,18 +695,11 @@ function renderVitest(fixture, fixturePath, testPath) {
562
695
  `import { ${expectFail ? "inspect" : "evaluate"} } from "@attest-ai/vitest";`,
563
696
  `import { describe, ${expectFail ? "expect, " : ""}it } from "vitest";`,
564
697
  "",
565
- `const FIXTURE = loadFixture(new URL(${ts(rel.startsWith(".") ? rel : `./${rel}`)}, import.meta.url));`
698
+ `const FIXTURE = loadFixture(${relUrl(fixturePath, testPath)});`,
699
+ ...skippedLines(skipped),
700
+ "",
701
+ ...chainLines(calls, "FIXTURE.trace", "")
566
702
  ];
567
- if (skipped.length > 0) {
568
- lines.push("", "// Drafted contracts without a TypeScript DSL call (left out):");
569
- for (const s of skipped) lines.push(`// - ${comment(s)}`);
570
- }
571
- lines.push("", "function contracts(): ExpectChain {", " return attestExpect(FIXTURE.trace)");
572
- for (const [c, dsl] of calls) {
573
- lines.push(` // ${comment(`${c.id}: ${c.description}`)}`, ` ${dsl}`);
574
- }
575
- lines[lines.length - 1] += ";";
576
- lines.push("}");
577
703
  if (expectFail) {
578
704
  lines.push("", "// Status of each contract on the captured incident, in contracts() order.");
579
705
  lines.push("const EXPECTED: Array<[string, string]> = [");
@@ -602,14 +728,14 @@ function renderVitest(fixture, fixturePath, testPath) {
602
728
  return `${lines.join("\n")}
603
729
  `;
604
730
  }
605
- function emitVitest(fixturePath) {
731
+ function emitVitest(fixturePath, rerun) {
606
732
  const testPath = testPathFor(fixturePath);
607
733
  const existing = fs3.lstatSync(testPath, { throwIfNoEntry: false });
608
734
  if (existing?.isSymbolicLink()) {
609
735
  throw new CaptureError(`${testPath} is a symbolic link; refusing to draft a test through it`);
610
736
  }
611
737
  if (existing !== void 0) return null;
612
- const source = renderVitest(loadFixture(fixturePath), fixturePath, testPath);
738
+ const source = renderVitest(loadFixture(fixturePath), fixturePath, testPath, rerun);
613
739
  fs3.mkdirSync(path3.dirname(testPath), { recursive: true });
614
740
  try {
615
741
  fs3.writeFileSync(testPath, source, { encoding: "utf8", flag: "wx" });
@@ -624,7 +750,7 @@ function emitVitest(fixturePath) {
624
750
 
625
751
  // src/cli.ts
626
752
  var HELP_FLAGS = /* @__PURE__ */ new Set(["-h", "--help"]);
627
- var SDK_USAGE = "usage: attest <command> [args]\n\nSDK commands:\n init [--ci github] [--skills] [--mcp] [--json]\n scaffold a sample test and merge policy here\n (--ci github also writes .github/workflows/attest.yml;\n --skills installs the agent skills with attest-engine;\n --mcp writes the MCP client configs (.mcp.json, .cursor/mcp.json,\n .codex/config.toml) with attest-engine;\n --json prints {created, updated, skipped} paths as JSON)\n\n`attest capture` (engine) also drafts tests/attest/<name>.test.ts from a verified\nfixture, unless that file exists.\n\noptions:\n --version print the SDK version and the engine version it requires\n --help print this help\n";
753
+ var SDK_USAGE = "usage: attest <command> [args]\n\nSDK commands:\n init [--ci github] [--skills] [--mcp] [--json]\n scaffold a sample test and merge policy here\n (--ci github also writes .github/workflows/attest.yml;\n --skills installs the agent skills with attest-engine;\n --mcp writes the MCP client configs (.mcp.json, .cursor/mcp.json,\n .codex/config.toml) with attest-engine;\n --json prints {created, updated, skipped} paths as JSON)\n\n`attest capture` (engine) also drafts tests/attest/<name>.test.ts from a fixture that\nis consistent with the recording, unless that file exists. For a command capture the\ntest re-runs the command written in it under strict replay (OpenAI- and Anthropic-\ncompatible calls are served from the recording; other providers are not blocked) and\nevaluates the contracts on the new run.\n\noptions:\n --version print the SDK version and the engine version it requires\n --help print this help\n";
628
754
  var HELP_PROBE_TIMEOUT_MS = 1e4;
629
755
  function errorMessage(err) {
630
756
  return err instanceof Error ? err.message : String(err);
@@ -679,7 +805,7 @@ async function printHelp() {
679
805
  }
680
806
  function exitStatus(status, signal) {
681
807
  if (status !== null) return status;
682
- if (signal !== null) return 128 + (os2.constants.signals[signal] ?? 0);
808
+ if (signal !== null) return 128 + (os3.constants.signals[signal] ?? 0);
683
809
  return 1;
684
810
  }
685
811
  async function engineProviding(command) {
@@ -765,6 +891,18 @@ function captureWritesFixture(args) {
765
891
  }
766
892
  return true;
767
893
  }
894
+ function captureWorldPath(args) {
895
+ for (let i = 0; i < args.length; i++) {
896
+ const a = args[i];
897
+ if (a === "--") return void 0;
898
+ const [name, ...rest] = a.split("=");
899
+ if (name === "--world" || name === "-world") {
900
+ const value = rest.length > 0 ? rest.join("=") : args[i + 1];
901
+ return value === void 0 ? void 0 : path4.resolve(value);
902
+ }
903
+ }
904
+ return void 0;
905
+ }
768
906
  function userResultFile(args) {
769
907
  for (const a of args) {
770
908
  if (a === "--") break;
@@ -779,7 +917,7 @@ function captureAndEmit(engine, args) {
779
917
  );
780
918
  return 2;
781
919
  }
782
- const tmp = fs4.mkdtempSync(path4.join(os2.tmpdir(), "attest-capture-"));
920
+ const tmp = fs4.mkdtempSync(path4.join(os3.tmpdir(), "attest-capture-"));
783
921
  const resultFile = path4.join(tmp, "result.json");
784
922
  let result;
785
923
  let code;
@@ -799,10 +937,10 @@ function captureAndEmit(engine, args) {
799
937
  `);
800
938
  return code;
801
939
  }
802
- const written = emitVitest(fixture);
940
+ const written = emitVitest(fixture, { cwd: process.cwd(), world: captureWorldPath(args) });
803
941
  process.stderr.write(
804
942
  written === null ? ` test: ${testPathFor(fixture)} exists; left unchanged
805
- ` : ` test: drafted ${written}
943
+ ` : ` test: drafted ${written}; for a command capture it re-runs the command under strict replay (other providers are not blocked: run it without their credentials) and evaluates the contracts on the new run
806
944
  `
807
945
  );
808
946
  return code;
@@ -992,7 +1130,7 @@ ${SDK_USAGE}`);
992
1130
  function isDirectExecution() {
993
1131
  const script = process.argv[1];
994
1132
  if (script === void 0 || !fs4.existsSync(script)) return false;
995
- return fs4.realpathSync(script) === fs4.realpathSync(fileURLToPath2(import.meta.url));
1133
+ return fs4.realpathSync(script) === fs4.realpathSync(fileURLToPath3(import.meta.url));
996
1134
  }
997
1135
  if (isDirectExecution()) {
998
1136
  main().then(
@@ -1005,6 +1143,7 @@ if (isDirectExecution()) {
1005
1143
  );
1006
1144
  }
1007
1145
  export {
1146
+ captureWorldPath,
1008
1147
  captureWritesFixture,
1009
1148
  engineCommands,
1010
1149
  exitStatus,