[GH-ISSUE #6068] [Bug] Managed source changes return Evidence integrity errors and incorrect recovery #3710

Closed
opened 2026-10-31 00:43:16 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6068

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

inspect_managed_artifact, inspect_managed_members, and inspect_managed_native_boundaries return evidence_integrity_mismatch, retryable: false, and instructions to recreate or re-import Evidence when the selected assembly changes during source acquisition. The failure concerns the assembly on disk, before this call creates its inspection Evidence.

Executed through in-memory MCP transport using the production server, sessions, managed provider, parser and file reads. For each tool, both a source rewrite after target admission and a source rewrite during snapshot acquisition produced this diagnostic. Unchanged controls and reruns after the replacement became stable succeeded.

Reproduction

In a prepared source checkout with locked dependencies, save the following as tests/boundary/mcp/managedSourceChange.repro.test.ts. Set REPRO_ROOT to an existing disposable writable directory and run:

node node_modules/vitest/vitest.mjs run tests/boundary/mcp/managedSourceChange.repro.test.ts --maxWorkers=1

The wrappers call the real provider and bounded reader. They synchronize actual assembly writes at the two acquisition boundaries; they do not fabricate errors or replace parser results. The assertions characterize the current incorrect diagnostic, so the observation test passes on the affected revision. One run exercises six changed-source cases, three unchanged controls and six stable-source recovery calls.

import { mkdtemp, writeFile, rm, realpath } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it, vi } from "vitest";
import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js";
import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js";
import { ManagedStaticProvider } from "../../../src/dotnet/ManagedStaticProvider.js";
import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js";
import * as bounded from "../../../src/process/BoundedFileBytes.js";
import { createServer } from "../../../src/server/createServer.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";

it("observes managed changed-source diagnostics over MCP with actual file replacement", async () => {
  if (!process.env.REPRO_ROOT) throw new Error("Set REPRO_ROOT to a disposable writable directory");
  const root = await realpath(await mkdtemp(join(process.env.REPRO_ROOT, "managed-source-")));
  const path = join(root, "assembly.dll");
  const originalBytes = buildManagedPeFixture();
  const replacementBytes = buildManagedPeFixture({ methodName: "Other" });
  let mode: "none" | "after-admission" | "during-read" = "none";
  let active = false;
  let mutated = false;
  let producerTag: string | undefined;
  let causeName: string | undefined;
  const originalCreate = ManagedStaticProvider.prototype.createClient;
  const originalRead = bounded.readBoundedFileBytes;
  vi.spyOn(ManagedStaticProvider.prototype, "createClient").mockImplementation(function (target) {
    const client = originalCreate.call(this, target);
    const execute = client.execute.bind(client);
    client.execute = async (...args) => {
      if (args[0] === "health") return execute(...args);
      active = true;
      try {
        if (mode === "after-admission" && !mutated) {
          await writeFile(path, replacementBytes);
          mutated = true;
        }
        const result = await execute(...args);
        if (!result.ok) {
          producerTag = result.error._tag;
          causeName = result.error.cause instanceof Error ? result.error.cause.constructor.name : undefined;
        }
        return result;
      } finally { active = false; }
    };
    return client;
  });
  vi.spyOn(bounded, "readBoundedFileBytes").mockImplementation(async (...args) => {
    const result = await originalRead(...args);
    if (active && mode === "during-read" && !mutated) {
      await writeFile(path, replacementBytes);
      mutated = true;
    }
    return result;
  });
  const session = composeBinarySession(new AnalysisProviderRegistry([]), [new ManagedStaticProvider()]);
  const server = createServer({ kind: "session", session }, {
    availabilityPolicy: () => ({ processCaptureEnabled: false, investigationInputRoots: 0, browserObservationEnabled: false, electronObservationEnabled: false }),
  });
  const client = new Client({ name: "managed-source-change-repro", version: "1.0.0" });
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
  try {
    await server.connect(serverTransport);
    await client.connect(clientTransport);
    for (const operation of ["inspect_managed_artifact", "inspect_managed_members", "inspect_managed_native_boundaries"]) {
      mode = "none";
      await writeFile(path, originalBytes);
      const control = await client.callTool({ name: operation, arguments: { path } });
      expect(control.isError).not.toBe(true);
      for (const selectedMode of ["after-admission", "during-read"] as const) {
        await writeFile(path, originalBytes);
        mode = selectedMode;
        mutated = false;
        producerTag = undefined;
        causeName = undefined;
        const result = await client.callTool({ name: operation, arguments: { path } });
        const error = parseMcpToolError(result).error;
        expect(mutated).toBe(true);
        expect(result.structuredContent).toBeUndefined();
        expect(producerTag).toBe("EvidenceIntegrityError");
        expect(error.code).toBe("evidence_integrity_mismatch");
        expect(error.category).toBe("integrity_mismatch");
        if (selectedMode === "during-read") expect(causeName).toBe("RegularFileChangedError");
        console.log(JSON.stringify({ operation, mode, producerTag, causeName, error }).split(path).join("<assembly>"));
        mode = "none";
        const recovery = await client.callTool({ name: operation, arguments: { path } });
        expect(recovery.isError).not.toBe(true);
        console.log(JSON.stringify({ operation, changedSourceRerunSucceeded: true }));
      }
    }
    await client.ping();
  } finally {
    await client.close();
    await server.close();
    expect((await session.close()).ok).toBe(true);
    vi.restoreAllMocks();
    await rm(root, { recursive: true, force: true });
  }
}, 30000);

Expected behavior

Preserve refusal of changed bytes, but return artifact_changed and guidance to wait for a stable source and rerun the managed operation. This is source acquisition rather than validation of supplied Evidence.

The artifact-change type explicitly represents a selected artifact changing during acquisition. Its recovery explains stabilization and reacquisition; its projection retains operation, path, reason and stable-artifact-read boundary. It is retryable. In contrast, EvidenceIntegrityError is for Evidence identity, schema or bundle-manifest validation. The Electron fuse reader already maps the same regular-file change signal to the artifact-change type.

Impact and scope

P2: changed-source rejection works, but callers get the wrong machine-readable code, retryability and recovery action. Recreating unchanged Evidence does not stabilize a concurrently rebuilt assembly. Rerunning the public tool after the new file became stable succeeded in all six recovery cases; this is an observed workaround.

Both source-change windows reproduced for all three managed inspection tools in each of two focused runs. No successful stale analysis, corrupted output or security boundary crossing was observed. Natural race frequency, downstream retry automation, real application assemblies, CLI transport and other platforms were not measured. The same digest guard exists in path-based managed member comparison, but that workflow was inspected only.

REA version

Package manifest rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e.

Runtime and platform

macOS 27.0.1 arm64; Node.js v26.12.0; locked Vitest 5.0.3. Node is admitted by the package engine range; the repository's exact Node 24.18.0 pin was not separately exercised.

Target and artifact context

Repository-generated PE/CLI assembly from buildManagedPeFixture(), replaced with a second valid fixture whose method name is Other. Real regular-file writes and reads; assembly code is never loaded or executed. No external engine or simulated analysis provider.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Each changed-source request returned isError: true, no structuredContent, and this public error shape:

{
  "code": "evidence_integrity_mismatch",
  "category": "integrity_mismatch",
  "message": "Evidence is invalid or has changed. Recreate or re-import it, then try again.",
  "retryable": false,
  "remediation": {
    "action": "Evidence is invalid or has changed. Recreate or re-import it, then try again."
  }
}

After-admission cases retain a details.reason beginning Managed artifact digest changed after open: expected ... observed .... During-read cases retain Managed source changed while inspect_managed_artifact was reading ... (or the corresponding operation). The latter producer errors preserve a real RegularFileChangedError cause. Both digest values remain available in the after-admission reason; this report does not claim they disappear.

The focused observation test passed. Its controls, stable-source reruns, final MCP ping and explicit session close also passed.

Typed outcome or recovery expected

artifact_changed / integrity_mismatch, retryable: true, with operation, selected path and change reason, directing the caller to stabilize and reacquire the source. The category remains integrity_mismatch; the code, retryability, source context and recovery distinguish the cases.

Analysis or suspected cause

Two producing checks independently choose the Evidence error type: managed read normalization translates RegularFileChangedError to EvidenceIntegrityError; the provider digest check uses the same type for bytes differing from admission. Changing only one leaves the other failure path.

The MCP adapter calls isolated managed execution, which opens and executes a fresh managed-only session on every call. Therefore changing a DLL between two completed calls is not sufficient: this reproduction changes it inside one call's admission/acquisition window. CLI uses the same managed execution path, but was not executed here.

Source-only additional scope: path-based managed comparison has an equivalent digest check. The existing provider test already expects EvidenceIntegrityError after a source change; its expectation needs to reflect source-artifact semantics rather than preserve the current classification.

Additional context

Related managed-Evidence classification reports have different producers and repairs: #5988 concerns contradictory identities within supplied managed Evidence; #5990 and its comment concern authentic Evidence chosen for a wrong workflow role; #5985 concerns managed entry cancellation losing its abort cause. #5978 covers Apktool snapshot-copy classification and #5969 covers Wakaru's final source fingerprint. None changes these managed source-acquisition guards. #4381 and merged PR #4446 concern hash-cancellation descriptor ownership, and #5939 concerns Ghidra NativeAOT private-snapshot parsing.

Fetched upstream main 8e3bf56e11bcb56d0e8754fdfa32a7a9f004ca13 leaves the relevant managed guards and artifact-change/Evidence-integrity mappings unchanged. That later revision was inspected, not executed. No last-known-good version was established.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6068 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior `inspect_managed_artifact`, `inspect_managed_members`, and `inspect_managed_native_boundaries` return `evidence_integrity_mismatch`, `retryable: false`, and instructions to recreate or re-import Evidence when the selected assembly changes during source acquisition. The failure concerns the assembly on disk, before this call creates its inspection Evidence. Executed through in-memory MCP transport using the production server, sessions, managed provider, parser and file reads. For each tool, both a source rewrite after target admission and a source rewrite during snapshot acquisition produced this diagnostic. Unchanged controls and reruns after the replacement became stable succeeded. ### Reproduction In a prepared source checkout with locked dependencies, save the following as `tests/boundary/mcp/managedSourceChange.repro.test.ts`. Set `REPRO_ROOT` to an existing disposable writable directory and run: ```sh node node_modules/vitest/vitest.mjs run tests/boundary/mcp/managedSourceChange.repro.test.ts --maxWorkers=1 ``` The wrappers call the real provider and bounded reader. They synchronize actual assembly writes at the two acquisition boundaries; they do not fabricate errors or replace parser results. The assertions characterize the current incorrect diagnostic, so the observation test passes on the affected revision. One run exercises six changed-source cases, three unchanged controls and six stable-source recovery calls. ```ts import { mkdtemp, writeFile, rm, realpath } from "node:fs/promises"; import { join } from "node:path"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { expect, it, vi } from "vitest"; import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js"; import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js"; import { ManagedStaticProvider } from "../../../src/dotnet/ManagedStaticProvider.js"; import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js"; import * as bounded from "../../../src/process/BoundedFileBytes.js"; import { createServer } from "../../../src/server/createServer.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; it("observes managed changed-source diagnostics over MCP with actual file replacement", async () => { if (!process.env.REPRO_ROOT) throw new Error("Set REPRO_ROOT to a disposable writable directory"); const root = await realpath(await mkdtemp(join(process.env.REPRO_ROOT, "managed-source-"))); const path = join(root, "assembly.dll"); const originalBytes = buildManagedPeFixture(); const replacementBytes = buildManagedPeFixture({ methodName: "Other" }); let mode: "none" | "after-admission" | "during-read" = "none"; let active = false; let mutated = false; let producerTag: string | undefined; let causeName: string | undefined; const originalCreate = ManagedStaticProvider.prototype.createClient; const originalRead = bounded.readBoundedFileBytes; vi.spyOn(ManagedStaticProvider.prototype, "createClient").mockImplementation(function (target) { const client = originalCreate.call(this, target); const execute = client.execute.bind(client); client.execute = async (...args) => { if (args[0] === "health") return execute(...args); active = true; try { if (mode === "after-admission" && !mutated) { await writeFile(path, replacementBytes); mutated = true; } const result = await execute(...args); if (!result.ok) { producerTag = result.error._tag; causeName = result.error.cause instanceof Error ? result.error.cause.constructor.name : undefined; } return result; } finally { active = false; } }; return client; }); vi.spyOn(bounded, "readBoundedFileBytes").mockImplementation(async (...args) => { const result = await originalRead(...args); if (active && mode === "during-read" && !mutated) { await writeFile(path, replacementBytes); mutated = true; } return result; }); const session = composeBinarySession(new AnalysisProviderRegistry([]), [new ManagedStaticProvider()]); const server = createServer({ kind: "session", session }, { availabilityPolicy: () => ({ processCaptureEnabled: false, investigationInputRoots: 0, browserObservationEnabled: false, electronObservationEnabled: false }), }); const client = new Client({ name: "managed-source-change-repro", version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); try { await server.connect(serverTransport); await client.connect(clientTransport); for (const operation of ["inspect_managed_artifact", "inspect_managed_members", "inspect_managed_native_boundaries"]) { mode = "none"; await writeFile(path, originalBytes); const control = await client.callTool({ name: operation, arguments: { path } }); expect(control.isError).not.toBe(true); for (const selectedMode of ["after-admission", "during-read"] as const) { await writeFile(path, originalBytes); mode = selectedMode; mutated = false; producerTag = undefined; causeName = undefined; const result = await client.callTool({ name: operation, arguments: { path } }); const error = parseMcpToolError(result).error; expect(mutated).toBe(true); expect(result.structuredContent).toBeUndefined(); expect(producerTag).toBe("EvidenceIntegrityError"); expect(error.code).toBe("evidence_integrity_mismatch"); expect(error.category).toBe("integrity_mismatch"); if (selectedMode === "during-read") expect(causeName).toBe("RegularFileChangedError"); console.log(JSON.stringify({ operation, mode, producerTag, causeName, error }).split(path).join("<assembly>")); mode = "none"; const recovery = await client.callTool({ name: operation, arguments: { path } }); expect(recovery.isError).not.toBe(true); console.log(JSON.stringify({ operation, changedSourceRerunSucceeded: true })); } } await client.ping(); } finally { await client.close(); await server.close(); expect((await session.close()).ok).toBe(true); vi.restoreAllMocks(); await rm(root, { recursive: true, force: true }); } }, 30000); ``` ### Expected behavior Preserve refusal of changed bytes, but return `artifact_changed` and guidance to wait for a stable source and rerun the managed operation. This is source acquisition rather than validation of supplied Evidence. The [artifact-change type](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorCore.ts#L205-L216) explicitly represents a selected artifact changing during acquisition. Its [recovery](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorPresentation.ts#L129-L130) explains stabilization and reacquisition; its [projection](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorProjection.ts#L311-L317) retains operation, path, reason and `stable-artifact-read` boundary. It is [retryable](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorProjection.ts#L594-L600). In contrast, [EvidenceIntegrityError](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/evidenceErrors.ts#L13-L25) is for Evidence identity, schema or bundle-manifest validation. The [Electron fuse reader](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/electron/ElectronFuseFailures.ts#L119-L122) already maps the same regular-file change signal to the artifact-change type. ### Impact and scope P2: changed-source rejection works, but callers get the wrong machine-readable code, retryability and recovery action. Recreating unchanged Evidence does not stabilize a concurrently rebuilt assembly. Rerunning the public tool after the new file became stable succeeded in all six recovery cases; this is an observed workaround. Both source-change windows reproduced for all three managed inspection tools in each of two focused runs. No successful stale analysis, corrupted output or security boundary crossing was observed. Natural race frequency, downstream retry automation, real application assemblies, CLI transport and other platforms were not measured. The same digest guard exists in path-based managed member comparison, but that workflow was inspected only. ### REA version Package manifest `rea-agents` 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`. ### Runtime and platform macOS 27.0.1 arm64; Node.js v26.12.0; locked Vitest 5.0.3. Node is admitted by the package engine range; the repository's exact Node 24.18.0 pin was not separately exercised. ### Target and artifact context Repository-generated PE/CLI assembly from `buildManagedPeFixture()`, replaced with a second valid fixture whose method name is `Other`. Real regular-file writes and reads; assembly code is never loaded or executed. No external engine or simulated analysis provider. ### Interface MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Each changed-source request returned `isError: true`, no `structuredContent`, and this public error shape: ```json { "code": "evidence_integrity_mismatch", "category": "integrity_mismatch", "message": "Evidence is invalid or has changed. Recreate or re-import it, then try again.", "retryable": false, "remediation": { "action": "Evidence is invalid or has changed. Recreate or re-import it, then try again." } } ``` After-admission cases retain a `details.reason` beginning `Managed artifact digest changed after open: expected ... observed ...`. During-read cases retain `Managed source changed while inspect_managed_artifact was reading ...` (or the corresponding operation). The latter producer errors preserve a real `RegularFileChangedError` cause. Both digest values remain available in the after-admission reason; this report does not claim they disappear. The focused observation test passed. Its controls, stable-source reruns, final MCP ping and explicit session close also passed. ### Typed outcome or recovery expected `artifact_changed` / `integrity_mismatch`, `retryable: true`, with operation, selected path and change reason, directing the caller to stabilize and reacquire the source. The category remains `integrity_mismatch`; the code, retryability, source context and recovery distinguish the cases. ### Analysis or suspected cause Two producing checks independently choose the Evidence error type: [managed read normalization](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/dotnet/ManagedSnapshot.ts#L182-L201) translates `RegularFileChangedError` to `EvidenceIntegrityError`; [the provider digest check](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/dotnet/ManagedStaticProvider.ts#L99-L115) uses the same type for bytes differing from admission. Changing only one leaves the other failure path. The [MCP adapter](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/server/registerManagedTools.ts#L54-L59) calls [isolated managed execution](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/DirectAnalysis.ts#L180-L200), which opens and executes a fresh managed-only session on every call. Therefore changing a DLL between two completed calls is not sufficient: this reproduction changes it inside one call's admission/acquisition window. CLI uses the same managed execution path, but was not executed here. Source-only additional scope: [path-based managed comparison](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/managed/ManagedMemberComparisonService.ts#L91-L94) has an equivalent digest check. The [existing provider test](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/tests/boundary/providers/managed/managedStaticProvider.test.ts#L256-L264) already expects `EvidenceIntegrityError` after a source change; its expectation needs to reflect source-artifact semantics rather than preserve the current classification. ### Additional context Related managed-Evidence classification reports have different producers and repairs: [#5988](https://github.com/morluto/rea/issues/5988) concerns contradictory identities within supplied managed Evidence; [#5990](https://github.com/morluto/rea/issues/5990) and its comment concern authentic Evidence chosen for a wrong workflow role; #5985 concerns managed entry cancellation losing its abort cause. #5978 covers Apktool snapshot-copy classification and #5969 covers Wakaru's final source fingerprint. None changes these managed source-acquisition guards. #4381 and merged PR #4446 concern hash-cancellation descriptor ownership, and #5939 concerns Ghidra NativeAOT private-snapshot parsing. Fetched upstream main `8e3bf56e11bcb56d0e8754fdfa32a7a9f004ca13` leaves the relevant managed guards and artifact-change/Evidence-integrity mappings unchanged. That later revision was inspected, not executed. No last-known-good version was established.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Both acquisition failure owners now use AnalysisArtifactChangedError: digest disagreement after admission and RegularFileChangedError during a stable snapshot. ManagedStaticProvider uses that shared identity check for the acquired snapshot before interpreting it. Existing tests assert artifact_changed/retryable true for a real rewrite during read and typed artifact-changed for stale digest.

Implementation: ManagedSnapshot.ts, ManagedSnapshot.ts.

The relevant regression checks in managedStaticProvider.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380752129 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Both acquisition failure owners now use AnalysisArtifactChangedError: digest disagreement after admission and RegularFileChangedError during a stable snapshot. ManagedStaticProvider uses that shared identity check for the acquired snapshot before interpreting it. Existing tests assert artifact_changed/retryable true for a real rewrite during read and typed artifact-changed for stale digest. Implementation: [ManagedSnapshot.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/dotnet/ManagedSnapshot.ts#L44), [ManagedSnapshot.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/dotnet/ManagedSnapshot.ts#L273). The relevant regression checks in [managedStaticProvider.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/providers/managed/managedStaticProvider.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3710
No description provided.