[GH-ISSUE #7428] [Bug] inspect_address_context returns an empty bookmark list after containment lookup fails #4432

Closed
opened 2026-10-31 00:49:45 +00:00 by bsakaguchi · 0 comments
Owner

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

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 listing a bookmark at 0x401000, MCP inspect_address_context {"address":"0x00401000","document":"fixture"} returns a successful result with bookmarks: {"state":"available","value":[]} when resolve_containing_procedure reports AnalysisProtocolError("containment read failed"). The procedure facet is correctly unavailable. Calling list_bookmarks directly still returns the bookmark. Successful containment with the same padded input, and failed containment with canonical input 0x401000, both return the matching bookmark.

Reproduction

From a fresh checkout at the revision below, use Node 24.22.1 on PATH. This exercises the production MCP server, session admission, and aggregate query with repository provider fixtures; it does not run a native analysis engine. The three-case test ran twice with the same result: two controls pass and the failed-containment/padded-input case fails. The second run only reduced the logged Evidence fields. The temporary test basename below replaces the verifier's temporary basename.

npm ci && npm run build:cached
WORK="$(mktemp -d)"
mkdir -p "$WORK"/home "$WORK"/config "$WORK"/data "$WORK"/cache "$WORK"/state "$WORK"/tmp
export HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" XDG_DATA_HOME="$WORK/data"
export XDG_CACHE_HOME="$WORK/cache" XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/tmp"
cat > tests/boundary/mcp/addressBookmarks.test.ts <<'TEST'
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { Client, InMemoryTransport } from '@modelcontextprotocol/client';
import { expect, it } from 'vitest';
import { createTestBinarySession } from '../../fixtures/binarySession.js';
import { createTestTempDirectory } from '../../fixtures/temporaryDirectory.js';
import { createAnalysisExecution } from '../../../src/application/AnalysisProvider.js';
import { AnalysisProtocolError } from '../../../src/domain/analysisErrorCore.js';
import { err, ok } from '../../../src/domain/result.js';
import { createServer } from '../../../src/server/createServer.js';

it.each([
  ['resolved padded', true, '0x00401000'],
  ['failed canonical', false, '0x401000'],
  ['failed padded', false, '0x00401000'],
] as const)('%s preserves bookmark knowledge', async (label, resolves, address) => {
  const identity = { id: 'fixture', name: 'Fixture analysis provider', version: '1' };
  const bookmark = { address: '0x401000', name: 'review' };
  const directory = await createTestTempDirectory('rea-bookmark-context-');
  const target = join(directory, 'fixture.com');
  await writeFile(target, Buffer.from([0xb8, 0x00, 0x4c, 0xcd, 0x21]));
  const session = createTestBinarySession(() => ({
    execute: async (operation) => {
      if (operation === 'resolve_containing_procedure' && !resolves)
        return err(new AnalysisProtocolError('containment read failed'));
      const value = operation === 'resolve_containing_procedure'
        ? { query_address: '0x401000', found: false, procedure: null, reason: 'not_in_procedure' }
        : operation === 'list_bookmarks' ? [bookmark]
        : operation === 'address_name' ? 'entry' : null;
      return ok(createAnalysisExecution(value, identity));
    },
    close: async () => ok(null),
  }));
  const server = createServer({ kind: 'session', session });
  const client = new Client({ name: 'bookmark-verifier', version: '1' });
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
  try {
    await server.connect(serverTransport);
    await client.connect(clientTransport);
    const opened = await client.callTool({ name: 'open_binary', arguments: { path: target, format: 'dos-com' } });
    expect(opened.isError).not.toBe(true);
    const direct = await client.callTool({ name: 'list_bookmarks', arguments: { document: 'fixture' } });
    const response = await client.callTool({ name: 'inspect_address_context', arguments: { address, document: 'fixture' } });
    expect(direct.isError).not.toBe(true);
    expect(response.isError).not.toBe(true);
    const result = (response.structuredContent as any).result;
    console.log(JSON.stringify({ label, direct: (direct.structuredContent as any).normalized_result, procedure: result.procedure, bookmarks: result.bookmarks }));
    expect(result.procedure.state).toBe(resolves ? 'available' : 'unavailable');
    expect(result.bookmarks).not.toEqual({ state: 'available', value: [] });
    if (result.bookmarks.state === 'available') expect(result.bookmarks.value).toEqual([bookmark]);
  } finally {
    await client.close();
    await server.close();
  }
});
TEST
npx vitest run tests/boundary/mcp/addressBookmarks.test.ts
RESULT=$?
rm tests/boundary/mcp/addressBookmarks.test.ts
exit "$RESULT"

Expected behavior

Return the matching bookmark after establishing the address identity, or mark bookmark matching unavailable when that identity cannot be established. Do not report definite absence by comparing an unnormalized selector with normalized provider addresses. src/contracts/sessionToolContracts.ts:181 promises “matching bookmarks”; docs/mcp-contracts.md:1146 describes facet-local unavailable outcomes, and docs/tool-design.md:65 requires missing coverage to remain unknown rather than absence. Existing src/application/AnalysisContextQueries.test.ts also expects normalized matching after successful resolution.

Impact and scope

P2: one aggregate inspection facet reports an incorrect negative observation while the provider's bookmark data remains intact. The observed trigger combines a failed containment operation with a differently formatted address. Direct list_bookmarks returned the bookmark in every executed case; using the canonical address also worked during the injected failure. Real-provider occurrence frequency, other failure types, and downstream client effects were not measured. No target mutation or security boundary crossing was involved.

REA version

Exact node scripts/rea.mjs --version output: 6.8.0. Source commit: f2277454b8c5356abbe311ad6dbb1dd3a3e334a5 (fetched main, rebuilt with npm run build:cached).

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; darwin / arm64, measured locally. In-memory MCP client transport and simulated provider; no Hopper, Ghidra, or IDA runtime was used.

Target and artifact context

Inline five-byte DOS COM fixture (b8 00 4c cd 21), admitted by open_binary with format: "dos-com". Bookmark and containment observations come from the simulated provider, not analysis of those bytes. HOME, all XDG directories, and TMPDIR were isolated.

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

Reduced observed output, preserving the actual direct-list and aggregate bookmark values:

{"label":"resolved padded","direct":[{"address":"0x401000","name":"review"}],"bookmarks":{"state":"available","value":[{"address":"0x401000","name":"review"}]}}
{"label":"failed canonical","direct":[{"address":"0x401000","name":"review"}],"bookmarks":{"state":"available","value":[{"address":"0x401000","name":"review"}]}}
{"label":"failed padded","direct":[{"address":"0x401000","name":"review"}],"bookmarks":{"state":"available","value":[]}}

Both failure cases return procedure.state: "unavailable" with reason containment read failed; all three aggregate calls succeed at the MCP level. Vitest reports Tests 1 failed | 2 passed (3), with the padded failure asserting that the bookmark result must not equal {state: 'available', value: []}. This is an observed boundary reproduction; no fix was applied.

Typed outcome or recovery expected

Preserve a typed unavailable bookmark facet if canonical matching cannot be established, or return the actual matching bookmark. Proposed restoration checks: the failed padded case no longer returns false absence, successful normalized matching remains available, and unrelated bookmarks stay filtered out. Restoration has not been tested.

Analysis or suspected cause

Source-derived: src/application/AnalysisContextQueries.ts:114 parses the containment response, but lines 130-135 fall back to the raw caller address when resolution fails. matchingBookmarks at lines 179-181 then reports an available list after exact string equality. The fallback therefore compares 0x00401000 with 0x401000. The Hopper bridge's address parser accepts prefixed hexadecimal spellings (bridge/hopper_bridge.py:340), while its resolver and bookmark listing emit _hex addresses (lines 961-966 and 1114-1115); this provider compatibility observation is from source, not a real-Hopper run.

Additional context

Closed #5901 and PR #6420 concern malformed procedure output becoming absence in get_navigation_context; the present failure is bookmark matching in inspect_address_context. Open #7310 and #7399 concern discovery readiness, before the facet computation tested here. Closed #5831 concerns failures of bookmark API availability; this fixture's bookmark listing succeeds. No open issue with the same behavior and restoration criterion was found. NOT_ENOUGH_INFO: first affected revision and last known good version were not established.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7428 ## 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 listing a bookmark at `0x401000`, MCP `inspect_address_context {"address":"0x00401000","document":"fixture"}` returns a successful result with `bookmarks: {"state":"available","value":[]}` when `resolve_containing_procedure` reports `AnalysisProtocolError("containment read failed")`. The procedure facet is correctly unavailable. Calling `list_bookmarks` directly still returns the bookmark. Successful containment with the same padded input, and failed containment with canonical input `0x401000`, both return the matching bookmark. ## Reproduction From a fresh checkout at the revision below, use Node 24.22.1 on PATH. This exercises the production MCP server, session admission, and aggregate query with repository provider fixtures; it does not run a native analysis engine. The three-case test ran twice with the same result: two controls pass and the failed-containment/padded-input case fails. The second run only reduced the logged Evidence fields. The temporary test basename below replaces the verifier's temporary basename. ```sh npm ci && npm run build:cached WORK="$(mktemp -d)" mkdir -p "$WORK"/home "$WORK"/config "$WORK"/data "$WORK"/cache "$WORK"/state "$WORK"/tmp export HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" XDG_DATA_HOME="$WORK/data" export XDG_CACHE_HOME="$WORK/cache" XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/tmp" cat > tests/boundary/mcp/addressBookmarks.test.ts <<'TEST' import { writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { Client, InMemoryTransport } from '@modelcontextprotocol/client'; import { expect, it } from 'vitest'; import { createTestBinarySession } from '../../fixtures/binarySession.js'; import { createTestTempDirectory } from '../../fixtures/temporaryDirectory.js'; import { createAnalysisExecution } from '../../../src/application/AnalysisProvider.js'; import { AnalysisProtocolError } from '../../../src/domain/analysisErrorCore.js'; import { err, ok } from '../../../src/domain/result.js'; import { createServer } from '../../../src/server/createServer.js'; it.each([ ['resolved padded', true, '0x00401000'], ['failed canonical', false, '0x401000'], ['failed padded', false, '0x00401000'], ] as const)('%s preserves bookmark knowledge', async (label, resolves, address) => { const identity = { id: 'fixture', name: 'Fixture analysis provider', version: '1' }; const bookmark = { address: '0x401000', name: 'review' }; const directory = await createTestTempDirectory('rea-bookmark-context-'); const target = join(directory, 'fixture.com'); await writeFile(target, Buffer.from([0xb8, 0x00, 0x4c, 0xcd, 0x21])); const session = createTestBinarySession(() => ({ execute: async (operation) => { if (operation === 'resolve_containing_procedure' && !resolves) return err(new AnalysisProtocolError('containment read failed')); const value = operation === 'resolve_containing_procedure' ? { query_address: '0x401000', found: false, procedure: null, reason: 'not_in_procedure' } : operation === 'list_bookmarks' ? [bookmark] : operation === 'address_name' ? 'entry' : null; return ok(createAnalysisExecution(value, identity)); }, close: async () => ok(null), })); const server = createServer({ kind: 'session', session }); const client = new Client({ name: 'bookmark-verifier', version: '1' }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); try { await server.connect(serverTransport); await client.connect(clientTransport); const opened = await client.callTool({ name: 'open_binary', arguments: { path: target, format: 'dos-com' } }); expect(opened.isError).not.toBe(true); const direct = await client.callTool({ name: 'list_bookmarks', arguments: { document: 'fixture' } }); const response = await client.callTool({ name: 'inspect_address_context', arguments: { address, document: 'fixture' } }); expect(direct.isError).not.toBe(true); expect(response.isError).not.toBe(true); const result = (response.structuredContent as any).result; console.log(JSON.stringify({ label, direct: (direct.structuredContent as any).normalized_result, procedure: result.procedure, bookmarks: result.bookmarks })); expect(result.procedure.state).toBe(resolves ? 'available' : 'unavailable'); expect(result.bookmarks).not.toEqual({ state: 'available', value: [] }); if (result.bookmarks.state === 'available') expect(result.bookmarks.value).toEqual([bookmark]); } finally { await client.close(); await server.close(); } }); TEST npx vitest run tests/boundary/mcp/addressBookmarks.test.ts RESULT=$? rm tests/boundary/mcp/addressBookmarks.test.ts exit "$RESULT" ``` ## Expected behavior Return the matching bookmark after establishing the address identity, or mark bookmark matching unavailable when that identity cannot be established. Do not report definite absence by comparing an unnormalized selector with normalized provider addresses. `src/contracts/sessionToolContracts.ts:181` promises “matching bookmarks”; `docs/mcp-contracts.md:1146` describes facet-local unavailable outcomes, and `docs/tool-design.md:65` requires missing coverage to remain unknown rather than absence. Existing `src/application/AnalysisContextQueries.test.ts` also expects normalized matching after successful resolution. ## Impact and scope P2: one aggregate inspection facet reports an incorrect negative observation while the provider's bookmark data remains intact. The observed trigger combines a failed containment operation with a differently formatted address. Direct `list_bookmarks` returned the bookmark in every executed case; using the canonical address also worked during the injected failure. Real-provider occurrence frequency, other failure types, and downstream client effects were not measured. No target mutation or security boundary crossing was involved. ## REA version Exact `node scripts/rea.mjs --version` output: `6.8.0`. Source commit: `f2277454b8c5356abbe311ad6dbb1dd3a3e334a5` (fetched main, rebuilt with `npm run build:cached`). ## Runtime and platform Node.js `v24.22.1`; macOS `27.0.1`; `darwin` / `arm64`, measured locally. In-memory MCP client transport and simulated provider; no Hopper, Ghidra, or IDA runtime was used. ## Target and artifact context Inline five-byte DOS COM fixture (`b8 00 4c cd 21`), admitted by `open_binary` with `format: "dos-com"`. Bookmark and containment observations come from the simulated provider, not analysis of those bytes. HOME, all XDG directories, and TMPDIR were isolated. ## Interface MCP ## Earliest failure stage Transport or result projection ## Relevant output or evidence Reduced observed output, preserving the actual direct-list and aggregate bookmark values: ```json {"label":"resolved padded","direct":[{"address":"0x401000","name":"review"}],"bookmarks":{"state":"available","value":[{"address":"0x401000","name":"review"}]}} {"label":"failed canonical","direct":[{"address":"0x401000","name":"review"}],"bookmarks":{"state":"available","value":[{"address":"0x401000","name":"review"}]}} {"label":"failed padded","direct":[{"address":"0x401000","name":"review"}],"bookmarks":{"state":"available","value":[]}} ``` Both failure cases return `procedure.state: "unavailable"` with reason `containment read failed`; all three aggregate calls succeed at the MCP level. Vitest reports `Tests 1 failed | 2 passed (3)`, with the padded failure asserting that the bookmark result must not equal `{state: 'available', value: []}`. This is an observed boundary reproduction; no fix was applied. ## Typed outcome or recovery expected Preserve a typed unavailable bookmark facet if canonical matching cannot be established, or return the actual matching bookmark. Proposed restoration checks: the failed padded case no longer returns false absence, successful normalized matching remains available, and unrelated bookmarks stay filtered out. Restoration has not been tested. ## Analysis or suspected cause Source-derived: `src/application/AnalysisContextQueries.ts:114` parses the containment response, but lines 130-135 fall back to the raw caller address when resolution fails. `matchingBookmarks` at lines 179-181 then reports an available list after exact string equality. The fallback therefore compares `0x00401000` with `0x401000`. The Hopper bridge's address parser accepts prefixed hexadecimal spellings (`bridge/hopper_bridge.py:340`), while its resolver and bookmark listing emit `_hex` addresses (lines 961-966 and 1114-1115); this provider compatibility observation is from source, not a real-Hopper run. ## Additional context Closed [#5901](https://github.com/morluto/rea/issues/5901) and [PR #6420](https://github.com/morluto/rea/pull/6420) concern malformed procedure output becoming absence in `get_navigation_context`; the present failure is bookmark matching in `inspect_address_context`. Open [#7310](https://github.com/morluto/rea/issues/7310) and [#7399](https://github.com/morluto/rea/issues/7399) concern discovery readiness, before the facet computation tested here. Closed [#5831](https://github.com/morluto/rea/issues/5831) concerns failures of bookmark API availability; this fixture's bookmark listing succeeds. No open issue with the same behavior and restoration criterion was found. NOT_ENOUGH_INFO: first affected revision and last known good version were not 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#4432
No description provided.