[GH-ISSUE #5901] [Bug] get_navigation_context turns malformed procedure replies into successful absence #3580

Closed
opened 2026-10-31 00:42:05 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

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

Area

MCP or agent integration

Observed behavior

With a simulated provider returning malformed successful resolve_containing_procedure output, MCP get_navigation_context {} returns success with procedure: null. The reply is identical to a valid observation that the cursor lies outside any procedure. Tested malformed values included null, found: true with a null procedure, found: true with a missing procedure name, and a missing found discriminant.

This was executed through the production MCP server and session/provider routing with the repository's in-memory MCP harness. No real analysis engine emitted these payloads during this test.

Reproduction

In the source checkout with locked dependencies installed, save the following as tests/boundary/mcp/navigationProcedureContract.test.ts and run:

./node_modules/.bin/vitest run tests/boundary/mcp/navigationProcedureContract.test.ts --maxWorkers=1

The exact test below ran with six passing observation assertions. Four malformed cases returned successful null procedure values; the valid absence and valid found-procedure controls also passed. These assertions document the current incorrect behavior rather than asserting a proposed fix.

import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { ok } from "../../../src/domain/result.js";
import { containingProcedureResolution } from "../../../src/contracts/toolOutputSchemaPrimitives.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";

const absent = { query_address: "0x401000", found: false, procedure: null, reason: "not_in_procedure" };
const cases = [
  { label: "null reply", payload: null, valid: false, name: null },
  { label: "found true with null procedure", payload: { query_address: "0x401000", found: true, procedure: null }, valid: false, name: null },
  { label: "found true without a name", payload: { query_address: "0x401000", found: true, procedure: { address: "0x401000" } }, valid: false, name: null },
  { label: "missing found discriminant", payload: { query_address: "0x401000", procedure: null }, valid: false, name: null },
  { label: "valid absence", payload: absent, valid: true, name: null },
  { label: "valid procedure", payload: { query_address: "0x401000", found: true, procedure: { address: "0x401000", name: "main", classification: null, body: { available: false, reason: "fixture" } } }, valid: true, name: "main" },
];
it.each(cases)("MCP navigation: $label", async ({label, payload, valid, name}) => {
  expect(containingProcedureResolution.safeParse(payload).success).toBe(valid);
  const calls: string[] = [];
  const session = createTestBinarySession(() => ({
    execute: async operation => {
      calls.push(operation);
      return observed(operation === "current_document" ? "fixture"
        : operation === "current_address" ? "0x401000"
        : operation === "resolve_containing_procedure" ? payload : null);
    },
    close: async () => ok(null),
  }));
  const server = createServer({kind: "session", session});
  const client = new Client({name: "navigation-contract-test", version: "1"});
  const [ct, st] = InMemoryTransport.createLinkedPair();
  try {
    await server.connect(st);
    await client.connect(ct);
    const opened = await client.callTool({name: "open_binary", arguments: {path: process.execPath}});
    expect(opened.isError).not.toBe(true);
    const result = await client.callTool({name: "get_navigation_context", arguments: {}});
    console.log(JSON.stringify({label, inputValid: valid, isError: result.isError ?? false, structuredContent: result.structuredContent}));
    expect(result.isError).not.toBe(true);
    expect(result.structuredContent).toMatchObject({result: {document: "fixture", address: "0x401000", procedure: name}});
    expect(calls).toContain("resolve_containing_procedure");
  } finally {
    await Promise.allSettled([client.close(), server.close(), session.close()]);
  }
});

Expected behavior

Malformed procedure-resolution output should remain an unreadable/unknown observation, rather than become a successful claim that no procedure contains the current address. Preserve successful procedure: null for valid absence, and the name for a valid found procedure.

The aggregate-context documentation assigns null to a cursor outside a procedure. The resolver schema distinguishes found and absent outcomes. The boundary ownership rules require preserving uncertainty and parsing unknown boundary values with typed failures.

Impact and scope

P2: malformed provider replies silently turn an unknown procedure fact into absence. Scope demonstrated here is four injected malformed payloads at the shared provider seam. The MCP output schema accepts the resulting null, so callers receive no error or diagnostic indicating that the provider reply was unusable. Real-provider frequency, downstream client consequences, and a practical workaround were not established. No data loss or security boundary crossing was observed.

REA version

rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e. Fetched main at c324d31b45fcab77e4ed7320ed60f0bf5a4ba188: the relevant query, registration, schemas, and provider envelope are unchanged. Runtime reproduction was at the stated baseline.

Runtime and platform

macOS 27.0.1 arm64; Node.js v26.12.0; repository-locked Vitest 5.0.3. Simulated provider, production MCP server, in-memory MCP client transport. The checkout pins Node 24.18.0; this run used the supplied Node 26 runtime.

Target and artifact context

process.execPath was admitted by open_binary as a local target for the session harness. Analysis responses came from the simulated provider. No target code was executed and no native analysis engine or GUI was started.

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

Reduced actual MCP responses:

{"label":"found true with null procedure","inputValid":false,"isError":false,"structuredContent":{"result":{"document":"fixture","address":"0x401000","procedure":null}}}
{"label":"valid absence","inputValid":true,"isError":false,"structuredContent":{"result":{"document":"fixture","address":"0x401000","procedure":null}}}
{"label":"valid procedure","inputValid":true,"isError":false,"structuredContent":{"result":{"document":"fixture","address":"0x401000","procedure":"main"}}}

The test independently checks each payload against containingProcedureResolution, opens a target, calls the public navigation tool, and checks that the resolver operation was invoked. Final focused run: one file passed, six tests passed.

Typed outcome or recovery expected

For malformed resolver output, a typed unreadable_output failure identifying resolve_containing_procedure, consistent with the query's existing AnalysisOutputError handling for malformed document/address observations. Proposed restoration checks: all four malformed cases return that failure; valid absence still returns null; a valid found procedure still returns its name. No patch or restoration was tested.

Analysis or suspected cause

Source-derived explanation: getNavigationContext passes successful resolver output to containingProcedureName. That helper returns null for every unrecognized shape, conflating malformed output with valid absence. The navigation output schema permits null, so final SDK validation cannot recover the distinction.

Additional context

Related #2516 and PR #2836 addressed malformed current-document/current-address values and their SDK validation failure. Their fix is already present here; this report concerns the remaining procedure-result interpretation, which produces schema-valid success. #5729 concerns missing-target discovery recovery, a separate contract and fix.

NOT_ENOUGH_INFO: no real-provider malformed reply, occurrence frequency, or regression window was established.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5901 ### 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 MCP or agent integration ### Observed behavior With a simulated provider returning malformed successful `resolve_containing_procedure` output, MCP `get_navigation_context {}` returns success with `procedure: null`. The reply is identical to a valid observation that the cursor lies outside any procedure. Tested malformed values included `null`, `found: true` with a null procedure, `found: true` with a missing procedure name, and a missing `found` discriminant. This was executed through the production MCP server and session/provider routing with the repository's in-memory MCP harness. No real analysis engine emitted these payloads during this test. ### Reproduction In the source checkout with locked dependencies installed, save the following as `tests/boundary/mcp/navigationProcedureContract.test.ts` and run: ```sh ./node_modules/.bin/vitest run tests/boundary/mcp/navigationProcedureContract.test.ts --maxWorkers=1 ``` The exact test below ran with six passing observation assertions. Four malformed cases returned successful null procedure values; the valid absence and valid found-procedure controls also passed. These assertions document the current incorrect behavior rather than asserting a proposed fix. ```ts import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { expect, it } from "vitest"; import { ok } from "../../../src/domain/result.js"; import { containingProcedureResolution } from "../../../src/contracts/toolOutputSchemaPrimitives.js"; import { createServer } from "../../../src/server/createServer.js"; import { observed } from "../../fixtures/analysisExecution.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; const absent = { query_address: "0x401000", found: false, procedure: null, reason: "not_in_procedure" }; const cases = [ { label: "null reply", payload: null, valid: false, name: null }, { label: "found true with null procedure", payload: { query_address: "0x401000", found: true, procedure: null }, valid: false, name: null }, { label: "found true without a name", payload: { query_address: "0x401000", found: true, procedure: { address: "0x401000" } }, valid: false, name: null }, { label: "missing found discriminant", payload: { query_address: "0x401000", procedure: null }, valid: false, name: null }, { label: "valid absence", payload: absent, valid: true, name: null }, { label: "valid procedure", payload: { query_address: "0x401000", found: true, procedure: { address: "0x401000", name: "main", classification: null, body: { available: false, reason: "fixture" } } }, valid: true, name: "main" }, ]; it.each(cases)("MCP navigation: $label", async ({label, payload, valid, name}) => { expect(containingProcedureResolution.safeParse(payload).success).toBe(valid); const calls: string[] = []; const session = createTestBinarySession(() => ({ execute: async operation => { calls.push(operation); return observed(operation === "current_document" ? "fixture" : operation === "current_address" ? "0x401000" : operation === "resolve_containing_procedure" ? payload : null); }, close: async () => ok(null), })); const server = createServer({kind: "session", session}); const client = new Client({name: "navigation-contract-test", version: "1"}); const [ct, st] = InMemoryTransport.createLinkedPair(); try { await server.connect(st); await client.connect(ct); const opened = await client.callTool({name: "open_binary", arguments: {path: process.execPath}}); expect(opened.isError).not.toBe(true); const result = await client.callTool({name: "get_navigation_context", arguments: {}}); console.log(JSON.stringify({label, inputValid: valid, isError: result.isError ?? false, structuredContent: result.structuredContent})); expect(result.isError).not.toBe(true); expect(result.structuredContent).toMatchObject({result: {document: "fixture", address: "0x401000", procedure: name}}); expect(calls).toContain("resolve_containing_procedure"); } finally { await Promise.allSettled([client.close(), server.close(), session.close()]); } }); ``` ### Expected behavior Malformed procedure-resolution output should remain an unreadable/unknown observation, rather than become a successful claim that no procedure contains the current address. Preserve successful `procedure: null` for valid absence, and the name for a valid found procedure. The [aggregate-context documentation](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/mcp-contracts.md#L925-L934) assigns null to a cursor outside a procedure. The [resolver schema](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/contracts/toolOutputSchemaPrimitives.ts#L557-L569) distinguishes found and absent outcomes. The [boundary ownership rules](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/AGENTS.md#L18-L34) require preserving uncertainty and parsing unknown boundary values with typed failures. ### Impact and scope P2: malformed provider replies silently turn an unknown procedure fact into absence. Scope demonstrated here is four injected malformed payloads at the shared provider seam. The MCP output schema accepts the resulting null, so callers receive no error or diagnostic indicating that the provider reply was unusable. Real-provider frequency, downstream client consequences, and a practical workaround were not established. No data loss or security boundary crossing was observed. ### REA version `rea-agents` 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`. Fetched main at `c324d31b45fcab77e4ed7320ed60f0bf5a4ba188`: the relevant query, registration, schemas, and provider envelope are unchanged. Runtime reproduction was at the stated baseline. ### Runtime and platform macOS 27.0.1 arm64; Node.js v26.12.0; repository-locked Vitest 5.0.3. Simulated provider, production MCP server, in-memory MCP client transport. The checkout pins Node 24.18.0; this run used the supplied Node 26 runtime. ### Target and artifact context `process.execPath` was admitted by `open_binary` as a local target for the session harness. Analysis responses came from the simulated provider. No target code was executed and no native analysis engine or GUI was started. ### Interface MCP ### Earliest failure stage Transport or result projection ### Relevant output or evidence Reduced actual MCP responses: ```json {"label":"found true with null procedure","inputValid":false,"isError":false,"structuredContent":{"result":{"document":"fixture","address":"0x401000","procedure":null}}} {"label":"valid absence","inputValid":true,"isError":false,"structuredContent":{"result":{"document":"fixture","address":"0x401000","procedure":null}}} {"label":"valid procedure","inputValid":true,"isError":false,"structuredContent":{"result":{"document":"fixture","address":"0x401000","procedure":"main"}}} ``` The test independently checks each payload against `containingProcedureResolution`, opens a target, calls the public navigation tool, and checks that the resolver operation was invoked. Final focused run: one file passed, six tests passed. ### Typed outcome or recovery expected For malformed resolver output, a typed `unreadable_output` failure identifying `resolve_containing_procedure`, consistent with the query's existing `AnalysisOutputError` handling for malformed document/address observations. Proposed restoration checks: all four malformed cases return that failure; valid absence still returns null; a valid found procedure still returns its name. No patch or restoration was tested. ### Analysis or suspected cause Source-derived explanation: [getNavigationContext](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/AnalysisContextQueries.ts#L64-L81) passes successful resolver output to [containingProcedureName](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/AnalysisContextQueries.ts#L158-L170). That helper returns null for every unrecognized shape, conflating malformed output with valid absence. The [navigation output schema](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/contracts/toolOutputSchemaGroups.ts#L572-L578) permits null, so final SDK validation cannot recover the distinction. ### Additional context Related #2516 and PR #2836 addressed malformed current-document/current-address values and their SDK validation failure. Their fix is already present here; this report concerns the remaining procedure-result interpretation, which produces schema-valid success. #5729 concerns missing-target discovery recovery, a separate contract and fix. NOT_ENOUGH_INFO: no real-provider malformed reply, occurrence frequency, or regression window was established.
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#3580
No description provided.