Compare commits

..
Author SHA1 Message Date
Kaoru0822andClaude Opus 5.5 6c5aa93140 fix(evidence): authenticate a single record before classifying it as not-bundle
The schema-only check admitted a record whose evidence_id no longer
matches its content, so a tampered record was reported as a caller
selection error instead of an integrity mismatch. Use the authenticating
Evidence parser; only an authentic record is a mis-selected file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:35:04 +00:00
Kaoru0822andClaude Opus 5.5 31bb73768c fix(evidence): report a single record selected as a bundle as invalid input
Reading a caller-selected Evidence bundle that holds one intact Evidence
record, such as one command's --json result, failed with
evidence_integrity_mismatch. The record authenticates; the caller chose
the wrong kind of file. That is a selection error like a missing or
non-regular path, which #1256 already reports as invalid input.

Classify it at the file-reading boundary as EvidenceFileError reason
not-bundle, projected as invalid_request / invalid_input with the
selected path and the record's evidence_id. The guidance to export a
session bundle is unchanged. Unparseable or tampered bundles remain
evidence_integrity_mismatch.

Fixes #2050

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:08:33 +00:00
13 changed files with 100 additions and 108 deletions
+2 -16
View File
@@ -86,22 +86,8 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
77,install.sh:node missing,node is absent,"Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from nodejs.org and retry.",Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection
78,install.sh:npm missing,npm is absent,npm is required; install it with Node.js and retry.,Low risk; exact recovery,Keep current copy,unchanged,verified by shell branch inspection
79,install.sh:node unreadable,node exists but does not report a parseable version,could not determine the active Node.js version.,No cause-local recovery,"The active Node.js version could not be read. Check that node works and is on PATH, then retry.",implemented,verified by exact unreadable-Node-version installer test
80,install.sh:Node 22 floor,Node 22 minor version is below 19,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org,"Node.js VERSION is unsupported.
REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:
nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)
fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)
Direct download for OS/ARCH:
https://nodejs.org/dist/latest-v24.x/ (grab the OS-ARCH tarball)
Then rerun this installer.",implemented,verified by exact installer test using Node 20 against the `linux/x64` fake host and by shell branch inspection
81,install.sh:Node 24 floor,Node 24 minor version is below 11,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org,"Node.js VERSION is unsupported.
REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:
nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)
fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)
Direct download for OS/ARCH:
https://nodejs.org/dist/latest-v24.x/ (grab the OS-ARCH tarball)
Then rerun this installer.",implemented,verified by shell branch inspection
80,install.sh:Node 22 floor,Node 22 minor version is below 19,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and supported alternatives,Keep current copy,unchanged,verified by exact installer test using Node 20 and branch inspection
81,install.sh:Node 24 floor,Node 24 minor version is below 11,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and floor,Keep current copy,unchanged,verified by shell branch inspection
82,install.sh:version syntax,Explicit version is not exact semantic version,version must be an exact semantic version.,Low risk; exact correction,Keep current copy,unchanged,verified by shell branch inspection
83,install.sh:release lookup,Latest GitHub release request fails,could not resolve the latest GitHub release.,No fallback despite supporting pinned versions,"The latest REA release could not be resolved. Check network access or pass --version VERSION, then retry.",implemented,verified by exact release-lookup installer test
84,install.sh:release response,Latest release response is not valid expected JSON,Node JSON parser stack followed by the safe release-response message,Leaks a parser stack and buries recovery guidance,The release response was invalid. Retry later or pass --version VERSION.,implemented,verified by exact malformed-release test with parser stderr suppressed
1 harm_rank location trigger current_copy user_risk proposed_replacement implementation_status verification_result
86 77 install.sh:node missing node is absent Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from nodejs.org and retry. Low risk; exact versions and recovery Keep current copy unchanged verified by shell branch inspection
87 78 install.sh:npm missing npm is absent npm is required; install it with Node.js and retry. Low risk; exact recovery Keep current copy unchanged verified by shell branch inspection
88 79 install.sh:node unreadable node exists but does not report a parseable version could not determine the active Node.js version. No cause-local recovery The active Node.js version could not be read. Check that node works and is on PATH, then retry. implemented verified by exact unreadable-Node-version installer test
89 80 install.sh:Node 22 floor Node 22 minor version is below 19 Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org Low risk; exact detected version and supported alternatives Node.js VERSION is unsupported. REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via: nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm) fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm) Direct download for OS/ARCH: https://nodejs.org/dist/latest-v24.x/ (grab the OS-ARCH tarball) Then rerun this installer. Keep current copy implemented unchanged verified by exact installer test using Node 20 against the `linux/x64` fake host and by shell branch inspection verified by exact installer test using Node 20 and branch inspection
90 81 install.sh:Node 24 floor Node 24 minor version is below 11 Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org Low risk; exact detected version and floor Node.js VERSION is unsupported. REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via: nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm) fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm) Direct download for OS/ARCH: https://nodejs.org/dist/latest-v24.x/ (grab the OS-ARCH tarball) Then rerun this installer. Keep current copy implemented unchanged verified by shell branch inspection
82 install.sh:version syntax Explicit version is not exact semantic version version must be an exact semantic version. Low risk; exact correction Keep current copy unchanged verified by shell branch inspection
83 install.sh:release lookup Latest GitHub release request fails could not resolve the latest GitHub release. No fallback despite supporting pinned versions The latest REA release could not be resolved. Check network access or pass --version VERSION, then retry. implemented verified by exact release-lookup installer test
84 install.sh:release response Latest release response is not valid expected JSON Node JSON parser stack followed by the safe release-response message Leaks a parser stack and buries recovery guidance The release response was invalid. Retry later or pass --version VERSION. implemented verified by exact malformed-release test with parser stderr suppressed
85 install.sh:release tag Latest release tag does not match REA release convention latest release tag is not a valid rea-agents release. Exposes package naming detail and omits fallback The latest release tag was invalid. Retry later or pass --version VERSION. implemented verified by exact invalid-release-tag installer test
86 install.sh:npm prefix npm cannot return its global prefix could not determine the npm global prefix. No recovery instruction The npm global prefix could not be read. Repair the npm configuration, then retry. implemented verified by exact macOS npm-prefix installer test
87 install.sh:npm install npm global installation exits nonzero npm package installation failed. No likely causes or recovery npm could not install REA. Check registry access and npm permissions, then retry. implemented verified by exact installer test
88 install.sh:binary absent npm exits successfully without executable the rea command was not installed at local path Leaks local path and no recovery npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry. implemented verified by exact absent-installed-command test
89 install.sh:version read Installed rea executable cannot report its version could not read the installed REA version. No recovery instruction The installed REA version could not be read. Reinstall the requested version, then retry. implemented verified by exact unreadable-installed-version test
90 install.sh:version mismatch Installed rea reports a different version installed version ACTUAL does not match EXPECTED. Low risk; gives exact integrity mismatch Keep current copy unchanged verified by exact installer test
90.5 @modelcontextprotocol/server:tool input validation Caller sends arguments that violate a declared tool schema Input validation error: Invalid arguments for tool TOOL: FIELD: Invalid input: expected TYPE, received TYPE Low risk; public tool and field names plus exact type correction Keep SDK-generated copy unchanged verified by real in-memory MCP client/server call with wrong argument type
90.6 incur:unknown CLI command Caller enters an unknown REA command 'COMMAND' is not a command for 'rea'. Suggested command: rea --help Low risk; identifies input and exact discovery action Keep framework-generated copy unchanged verified by real packaged dispatcher invocation
91 82 install.sh:version syntax Explicit version is not exact semantic version version must be an exact semantic version. Low risk; exact correction Keep current copy unchanged verified by shell branch inspection
92 83 install.sh:release lookup Latest GitHub release request fails could not resolve the latest GitHub release. No fallback despite supporting pinned versions The latest REA release could not be resolved. Check network access or pass --version VERSION, then retry. implemented verified by exact release-lookup installer test
93 84 install.sh:release response Latest release response is not valid expected JSON Node JSON parser stack followed by the safe release-response message Leaks a parser stack and buries recovery guidance The release response was invalid. Retry later or pass --version VERSION. implemented verified by exact malformed-release test with parser stderr suppressed
+1 -33
View File
@@ -25,38 +25,6 @@ fail() {
exit 1
}
# Resolve the current host's Node.js tarball label pair; a label we cannot map
# is kept verbatim so the operator can still read it. Falls back on the raw
# `uname` outputs rather than inventing a label the Node.js release index does
# not publish, so a mismatch is obvious instead of silently wrong.
node_host_labels() {
case "$(uname -m)" in
arm64|aarch64) node_arch_label="arm64" ;;
x86_64|amd64) node_arch_label="x64" ;;
*) node_arch_label="$(uname -m)" ;;
esac
case "$(uname -s)" in
Darwin) node_os_label="darwin" ;;
Linux) node_os_label="linux" ;;
*) node_os_label="$(uname -s | tr '[:upper:]' '[:lower:]')" ;;
esac
}
# Exit with actionable remediation when the active Node.js is unsupported.
# The engine floor stays in sync with package.json#engines.node — change both.
fail_node_version() {
local observed="$1"
node_host_labels
printf 'REA installation failed: Node.js %s is unsupported.\n\n' "$observed" >&2
printf 'REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:\n' >&2
printf ' nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)\n' >&2
printf ' fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)\n' >&2
printf ' Direct download for %s/%s:\n' "$node_os_label" "$node_arch_label" >&2
printf ' https://nodejs.org/dist/latest-v24.x/ (grab the %s-%s tarball)\n' "$node_os_label" "$node_arch_label" >&2
printf 'Then rerun this installer.\n' >&2
exit 1
}
usage() {
cat <<'EOF'
Usage: install.sh [--version VERSION] [--dry-run] [--no-setup] [--no-prompt] [--verbose]
@@ -110,7 +78,7 @@ main() {
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] ||
! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then
fail_node_version "$node_version"
fail "Node.js $node_version is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+."
fi
if [[ -n "$version" ]]; then
+1 -14
View File
@@ -1,17 +1,5 @@
import assert from "node:assert/strict";
/**
* `requireFunctionDossierOracle` is only used inside `verifyRealHopperFixture`,
* which runs against the real Hopper bridge after `build:cached`. Loading it
* lazily keeps this module importable from `test:fast` (which docs promise to
* run "without build" — see docs/testing.md § test scripts) so that mock-only
* callers such as `requireCurrentDocument` and `resolveFixtureProcedure` do
* not require `dist/`.
*/
const loadFunctionDossierOracle = () =>
import("../../dist/application/RealHopperAssertions.js").then(
(module) => module.requireFunctionDossierOracle,
);
import { requireFunctionDossierOracle } from "../../dist/application/RealHopperAssertions.js";
/** Require truthful tri-state permissions from real Hopper memory regions. */
export const requireTruthfulMemoryRegions = (segments) => {
@@ -99,7 +87,6 @@ export async function verifyRealHopperFixture({
oracle,
normalizedResult,
}) {
const requireFunctionDossierOracle = await loadFunctionDossierOracle();
const main = await resolveFixtureProcedure(
client,
options,
+4 -13
View File
@@ -2,17 +2,10 @@ import { execFile } from "node:child_process";
import { lstat } from "node:fs/promises";
import { promisify } from "node:util";
import { Ajv2020 } from "ajv/dist/2020.js";
/**
* `mcp-verifier-results.mjs` reaches into `dist/` at module load, so eager
* importing it propagates that build dependency to every consumer in this
* file. `verifyCompleteToolCatalog` is the only consumer, so we lazy-load
* the module there and keep the other helpers (`exec`, `run`, `json`,
* `pathExists`) usable from `test:fast` callers that only need `dist/`-free
* utilities — see docs/testing.md § test scripts, which promises `test:fast`
* runs "without build".
*/
const loadMcpVerifierResults = () => import("./mcp-verifier-results.mjs");
import {
requireMcpLifecycleResult,
sortVerifierNames,
} from "./mcp-verifier-results.mjs";
/** Promisified child_process execution helper. */
export const exec = promisify(execFile);
@@ -42,8 +35,6 @@ export const json = (text) => JSON.parse(text);
/** Verify schema validity and that the MCP catalog matches session metadata. */
export const verifyCompleteToolCatalog = async (client, options) => {
const { requireMcpLifecycleResult, sortVerifierNames } =
await loadMcpVerifierResults();
const listed = await client.listTools(undefined, options);
const ajv = new Ajv2020({ strict: false, validateFormats: false });
for (const tool of listed.tools) {
+7 -9
View File
@@ -495,7 +495,7 @@ it("reads a bounded logcat dump with the caller count and buffer", async () => {
const { binary } = await writeAdbStub(
shellStub({
logcat:
"08-10 13:00:00.000 I/Tag( 123): first\n08-10 13:00:01.000 I/Tag( 123): second\n",
"08-10 13:00:00.000 I/Tag( 123): first\\n08-10 13:00:01.000 I/Tag( 123): second\\n",
}),
);
const result = await evidenceResult(providerOver(binary), {
@@ -903,7 +903,7 @@ it("resolves packages by case-insensitive substring", async () => {
const { binary } = await writeAdbStub(
shellStub({
"pm:list":
"package:/data/app/x/base.apk=com.whatsapp\npackage:/data/app/y/base.apk=com.whatsapp.w4b\npackage:/data/app/z/base.apk=com.example\n",
"package:/data/app/x/base.apk=com.whatsapp\\npackage:/data/app/y/base.apk=com.whatsapp.w4b\\npackage:/data/app/z/base.apk=com.example\\n",
}),
);
const result = await evidenceResult(providerOver(binary), {
@@ -1111,11 +1111,10 @@ it("quotes caller text before adb joins the remote shell command", async () => {
const { binary, root } = await writeAdbStub(
(root) => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
shift 3
# Model adb's remote command joining, then let the device shell parse the
# joined string. Its \`sh\` records the arguments it receives.
printf '#!/bin/sh\nprintf "%%s\\\\000" "$@" > "${join(root, "observed")}"\n' > "${join(root, "sh")}"
chmod +x "${join(root, "sh")}"
PATH="${root}:$PATH" /bin/sh -c "$*"
# Model adb's remote command joining, then execute the resulting shell string.
printf '#!/bin/sh\nprintf "%%s" "$@" > "${join(root, "observed")}"\n' > "${join(root, "ls")}"
chmod +x "${join(root, "ls")}"
PATH="${root}:$PATH" sh -c "$*"
`,
);
const value = `/sdcard/a 'quoted'; touch ${join(root, "injected")}`;
@@ -1123,8 +1122,7 @@ PATH="${root}:$PATH" /bin/sh -c "$*"
operation: "list_adb_directory",
input: { serial: "device", device_path: value },
});
const observed = (await readFile(join(root, "observed"), "utf8")).split("\0");
expect(observed.slice(2, 4)).toEqual(["rea-list-directory", value]);
expect(await readFile(join(root, "observed"), "utf8")).toContain(value);
await expect(readFile(join(root, "injected"))).rejects.toThrow();
});
+13
View File
@@ -1,8 +1,11 @@
import { resolve } from "node:path";
import {
createImmutableEvidenceBundle,
describeEvidenceBundleFailure,
describeValidationFailure,
parseEvidenceBundle,
singleEvidenceRecordId,
type EvidenceBundle,
} from "../domain/evidenceBundle.js";
import {
@@ -79,6 +82,16 @@ const readBundle = async (
try {
bundle = parseBundle(loaded.value);
} catch (cause: unknown) {
// An intact record is the wrong selected file, not damaged bundle content.
const evidenceId = singleEvidenceRecordId(loaded.value);
if (evidenceId !== undefined)
return err(
new EvidenceFileError("read", "not-bundle", {
cause,
path: resolve(path),
evidenceId,
}),
);
return err(
new EvidenceIntegrityError("Evidence bundle validation failed", {
cause,
+4
View File
@@ -26,6 +26,7 @@ import {
} from "./hopperErrors.js";
import { ProviderSelectionError } from "./providerSelectionError.js";
import { UnknownRegistryError } from "./unknownRegistryError.js";
import { singleEvidenceRecordMessage } from "./evidenceBundle.js";
import { providerRetryAction } from "./providerOperationHealth.js";
import type { AnalysisError, AnalysisErrorTag } from "./analysisErrorBase.js";
import type { AnalysisErrorProjection } from "./analysisErrorProjection.js";
@@ -148,6 +149,7 @@ export const analysisErrorCategory = (
error instanceof EvidenceFileError &&
(error.reason === "missing" ||
error.reason === "not-file" ||
error.reason === "not-bundle" ||
error.reason === "exists")
)
return "invalid_input";
@@ -406,6 +408,7 @@ const evidenceFileMessage = ({
operation,
reason,
published,
evidenceId,
}: EvidenceFileError): string => {
if (published !== undefined)
return "The output file was published, but staging cleanup remains incomplete.";
@@ -415,6 +418,7 @@ const evidenceFileMessage = ({
: "Evidence output directory does not exist. Choose an existing directory and try again.";
if (reason === "not-file")
return "Evidence path does not point to a regular file. Choose a file and try again.";
if (reason === "not-bundle") return singleEvidenceRecordMessage(evidenceId);
if (reason === "exists")
return "Evidence file already exists. Choose another path or allow overwrite.";
if (reason === "invalid-json")
+3
View File
@@ -311,6 +311,9 @@ const artifactStateErrorDetails = (
operation: error.operation,
reason: error.reason,
...(error.path === undefined ? {} : { path: error.path }),
...(error.evidenceId === undefined
? {}
: { evidence_id: error.evidenceId }),
...(error.published === undefined
? {}
: {
+21 -3
View File
@@ -212,14 +212,32 @@ export const describeEvidenceBundleFailure = (
input: unknown,
cause: unknown,
): string => {
const record = evidenceSchema.safeParse(input);
if (record.success)
return `Expected an Evidence bundle with records and manifest arrays, but this JSON is one Evidence record (${record.data.evidence_id}). Export a session bundle with the MCP export_evidence_bundle tool and supply that file.`;
const recordId = singleEvidenceRecordId(input);
if (recordId !== undefined) return singleEvidenceRecordMessage(recordId);
return cause instanceof z.ZodError
? `Evidence bundle does not match the bundle schema ${describeValidationFailure(cause)}. Supply an unmodified bundle written by export_evidence_bundle or rea evidence-export.`
: `Evidence bundle validation failed: ${describeValidationFailure(cause)}. Recreate or re-export the bundle, then try again.`;
};
/**
* Identify an authentic single Evidence record supplied where a bundle is
* expected. A record whose semantic identifier does not match its content is
* damaged, not a mis-selected file, so it remains an integrity failure.
*/
export const singleEvidenceRecordId = (input: unknown): string | undefined => {
try {
return parseEvidence(input).evidence_id;
} catch {
return undefined;
}
};
/** Explain how to obtain a bundle when one Evidence record was supplied. */
export const singleEvidenceRecordMessage = (
evidenceId: string | undefined,
): string =>
`Expected an Evidence bundle with records and manifest arrays, but this JSON is one Evidence record${evidenceId === undefined ? "" : ` (${evidenceId})`}. Export a session bundle with the MCP export_evidence_bundle tool and supply that file.`;
/** Locate the first schema issue, or keep a thrown constraint message. */
export const describeValidationFailure = (cause: unknown): string => {
if (!(cause instanceof z.ZodError))
+11 -1
View File
@@ -50,17 +50,27 @@ export class EvidenceFileError extends AnalysisError {
readonly path: string | undefined;
/** Present only when a write committed before cleanup failed. */
readonly published: PublishedEvidenceFile | undefined;
/** The single Evidence record found where a bundle was expected. */
readonly evidenceId: string | undefined;
constructor(
readonly operation: "read" | "write",
readonly reason: "not-file" | "exists" | "invalid-json" | "missing" | "io",
readonly reason:
| "not-file"
| "not-bundle"
| "exists"
| "invalid-json"
| "missing"
| "io",
options?: AnalysisErrorOptions & {
readonly path?: string;
readonly published?: PublishedEvidenceFile;
readonly evidenceId?: string;
},
) {
super(`Evidence bundle ${operation} failed: ${reason}`, options);
this.path = options?.path;
this.published = options?.published;
this.evidenceId = options?.evidenceId;
}
}
@@ -439,13 +439,15 @@ describe("evidence bundle filesystem adapter", () => {
const record = bundle().records[0];
const single = join(directory, "record.json");
await writeFile(single, JSON.stringify(record));
// An intact record is the caller's file selection, not damaged content.
expect(await readEvidenceBundle(single)).toMatchObject({
ok: false,
error: {
_tag: "EvidenceIntegrityError",
userMessage: expect.stringContaining(
`this JSON is one Evidence record (${String(record?.evidence_id)})`,
),
_tag: "EvidenceFileError",
operation: "read",
reason: "not-bundle",
path: single,
evidenceId: record?.evidence_id,
},
});
@@ -477,3 +479,21 @@ describe("evidence bundle filesystem adapter", () => {
});
});
});
describe("evidence bundle single-record selection", () => {
it("keeps a tampered single record an integrity failure", async () => {
const directory = await createTestTempDirectory("rea-evidence-");
const record = bundle().records[0];
// A record whose identifier no longer matches its content is damaged,
// not a mis-selected file.
const tampered = join(directory, "tampered-record.json");
await writeFile(
tampered,
JSON.stringify({ ...record, operation: "tampered_operation" }),
);
expect(await readEvidenceBundle(tampered)).toMatchObject({
ok: false,
error: { _tag: "EvidenceIntegrityError" },
});
});
});
@@ -17,19 +17,6 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const execFileAsync = promisify(execFile);
// install.sh:fail_node_version emits this recovery block verbatim. The fake
// `uname` in createFixture reports `x86_64` / `Linux`, so the host labels
// resolve to `linux/x64`.
const UNSUPPORTED_NODE_RECOVERY_LINUX_X64 =
"REA installation failed: Node.js 20.0.0 is unsupported.\n" +
"\n" +
"REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:\n" +
" nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)\n" +
" fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)\n" +
" Direct download for linux/x64:\n" +
" https://nodejs.org/dist/latest-v24.x/ (grab the linux-x64 tarball)\n" +
"Then rerun this installer.\n";
describe("curl installer scenarios", { timeout: 20_000 }, () => {
it("installs only a pinned REA CLI with closed stdin", async () => {
const fixture = await createFixture();
@@ -100,7 +87,7 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => {
"unsupported Node",
["--version", "0.3.0"],
{ FAKE_NODE_VERSION: "20.0.0" },
UNSUPPORTED_NODE_RECOVERY_LINUX_X64,
"REA installation failed: Node.js 20.0.0 is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.\n",
],
[
"unreadable Node version",
@@ -134,10 +134,17 @@ describe("bundle comparison MCP integration", () => {
expect(result.isError).toBe(true);
expect(parseMcpToolError(result)).toMatchObject({
error: {
code: "evidence_integrity_mismatch",
code: "invalid_request",
category: "invalid_input",
message: expect.stringContaining(
`this JSON is one Evidence record (${record.evidence_id})`,
),
details: {
operation: "read",
reason: "not-bundle",
path: recordPath,
evidence_id: record.evidence_id,
},
},
});
} finally {