[GH-ISSUE #5831] [Bug] Missing Hopper bookmark APIs return bridge_exception instead of capability_unavailable #3531

Closed
opened 2026-10-31 00:41:33 +00:00 by bsakaguchi · 2 comments
Owner

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

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

An executed Python-bridge boundary test sent valid authenticated list_bookmarks and unset_bookmark requests to the production bridge with a fake bound Hopper document. When the fake document omitted getBookmarks, hasBookmarkAtAddress, or removeBookmarkAtAddress, the terminal reply reported type: "bridge_exception" and message: "AttributeError: Hopper bridge operation failed".

The guarded controls, missing hasBookmarkAtAddress in set_bookmark and missing getBookmarkName in list_bookmarks, instead returned capability_unavailable with the missing API named. Normal listing and removal succeeded.

This reproduces the bridge's handling of absent methods, using repository fakes rather than a real Hopper installation. No particular affected Hopper release was established. The downstream public execution_failure classification is source-derived; the public CLI/MCP server was not executed.

Reproduction

From the source checkout with locked dependencies installed, save this test as src/hopper/repro.test.ts, then run npm run test:local -- src/hopper/repro.test.ts. The essential body below is the executed test, with its temporary filename replaced. It reuses the repository's document and authenticated socket-pair fixtures and runs the production Python transport. The verifier ran it with isolated HOME and XDG_CONFIG_HOME directories.

import { execFileSync } from "node:child_process";
import { expect, it } from "vitest";

it("classifies absent bookmark APIs as unavailable at the authenticated bridge boundary", () => {
  const stdout = execFileSync("python3", ["-B", "-c", String.raw`
import json
import runpy

fixture = runpy.run_path("tests/fixtures/hopperBookmarkMutationProbe.py")
transport = runpy.run_path("tests/fixtures/hopperBridgeFacadeProbe.py")
bridge = transport["load_bridge"]("bridge/hopper_bridge.py")
bridge["REA_TOKEN"] = "probe-token"
BaseDocument = fixture["FakeDocument"]
Provider = fixture["FakeDocumentProvider"]

class Document(BaseDocument):
    missing = None
    def __getattribute__(self, name):
        if name == object.__getattribute__(self, "missing"):
            raise AttributeError(name)
        return super().__getattribute__(name)

def call(method, missing):
    document = Document()
    document.names[0x401000] = "fixture"
    document.missing = missing
    Provider.document = document
    bridge["REA_TARGET_PATH"] = document.getExecutableFilePath()
    bridge["_configure_hopper_api"](Provider)
    replies = transport["bridge_replies"](bridge, [
        transport["request"](method, {} if method == "list_bookmarks" else {"address": "0x401000"})
    ])
    return {"method": method, "missing": missing, "reply": replies[0]}

cases = [
    ("list_bookmarks", None),
    ("unset_bookmark", None),
    ("set_bookmark", "hasBookmarkAtAddress"),
    ("list_bookmarks", "getBookmarkName"),
    ("list_bookmarks", "getBookmarks"),
    ("unset_bookmark", "hasBookmarkAtAddress"),
    ("unset_bookmark", "removeBookmarkAtAddress"),
]
print(json.dumps([call(*case) for case in cases], sort_keys=True))
`], { encoding: "utf8", timeout: 5_000 });
  const results = JSON.parse(stdout);
  console.log(JSON.stringify(results, null, 2));
  expect(results[0].reply.result).toEqual([{ address: "0x401000", name: "fixture" }]);
  expect(results[1].reply.result).toBe(true);
  expect(results.slice(2).map((item: any) => item.reply.error.type)).toEqual(
    Array(5).fill("capability_unavailable"),
  );
});

One run exercised seven cases. The two success controls passed. The final assertion failed because the three unguarded missing APIs returned bridge_exception. Remove the temporary test afterward. This is not a real-Hopper reproduction.

Expected behavior

Absent admitted provider APIs should use the typed capability outcome and preserve a reason naming the unavailable API. This follows the repository invariant to model expected boundary failures with tagged errors and preserve meaningful reasons (AGENTS.md). The bridge explicitly defines CapabilityUnavailableError as the case where an active Hopper build lacks an admitted public API operation (error definition). This applies to absent bookmark methods; it does not depend solely on another provider's convention.

Impact and scope

P2: under the reproduced missing-method conditions, callers receive an internal bridge-failure classification and lose the actionable missing-API reason. Source inspection shows that this also selects the public execution_failure code instead of capability_unavailable. The operation already cannot complete without its native method; the defect is how that limitation is represented.

The observed scope is the fake-document bridge boundary on one host. Real Hopper versions, platform frequency, and downstream automation effects remain unknown. No real target was modified, no binary-byte corruption was established, and no security boundary was crossed. No workaround for the incorrect classification was verified.

REA version

rea-agents 6.7.0; source baseline a5f982de8d.

Runtime and platform

Node.js v26.12.0; Python 3.12.15; Darwin 27.0.0 arm64; repository fake Hopper document, no Hopper application.

Target and artifact context

Synthetic bound document from tests/fixtures/hopperBookmarkMutationProbe.py; one mapped bookmark at 0x401000; no real target or artifact.

Interface

Python bridge

Earliest failure stage

Transport or result projection

Relevant output or evidence

Trimmed actual test output, with the temporary test filename omitted:

{"method":"list_bookmarks","missing":"getBookmarks","reply":{"error":{"code":-32000,"message":"AttributeError: Hopper bridge operation failed","type":"bridge_exception"},"id":1}}
{"method":"unset_bookmark","missing":"hasBookmarkAtAddress","reply":{"error":{"code":-32000,"message":"AttributeError: Hopper bridge operation failed","type":"bridge_exception"},"id":1}}
{"method":"unset_bookmark","missing":"removeBookmarkAtAddress","reply":{"error":{"code":-32000,"message":"AttributeError: Hopper bridge operation failed","type":"bridge_exception"},"id":1}}

The JSON objects above are compacted from the test's pretty-printed output. The failing assertion printed:

- Expected
+ Received

  [
    "capability_unavailable",
    "capability_unavailable",
-   "capability_unavailable",
-   "capability_unavailable",
-   "capability_unavailable",
+   "bridge_exception",
+   "bridge_exception",
+   "bridge_exception",
  ]

 Test Files  1 failed (1)
      Tests  1 failed (1)

Source references checked at the baseline:

Typed outcome or recovery expected

Return type: "capability_unavailable" with the missing bookmark API named. Preserve that classification as AnalysisCapabilityUnavailableError and public code capability_unavailable through client normalization and projection.

Analysis or suspected cause

Source-derived: the listing and removal branches access native methods directly, so Python raises AttributeError before the bridge can produce its expected capability error. Guarding these API lookups at their owning bridge boundary, consistently with the existing bookmark guards, would retain the intended classification and reason. No fix was implemented or tested.

Additional context

Open and closed issue searches and PR searches for Hopper/bookmark, capability/bridge_exception, getBookmarks, and hasBookmarkAtAddress found no matching report. Issue 5475 was read and concerns stale names after successful bookmark writes, a separate failure. The existence of the guards alone does not prove any released Hopper build lacks these APIs.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5831 ### 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 An executed Python-bridge boundary test sent valid authenticated `list_bookmarks` and `unset_bookmark` requests to the production bridge with a fake bound Hopper document. When the fake document omitted `getBookmarks`, `hasBookmarkAtAddress`, or `removeBookmarkAtAddress`, the terminal reply reported `type: "bridge_exception"` and `message: "AttributeError: Hopper bridge operation failed"`. The guarded controls, missing `hasBookmarkAtAddress` in `set_bookmark` and missing `getBookmarkName` in `list_bookmarks`, instead returned `capability_unavailable` with the missing API named. Normal listing and removal succeeded. This reproduces the bridge's handling of absent methods, using repository fakes rather than a real Hopper installation. No particular affected Hopper release was established. The downstream public `execution_failure` classification is source-derived; the public CLI/MCP server was not executed. ### Reproduction From the source checkout with locked dependencies installed, save this test as `src/hopper/repro.test.ts`, then run `npm run test:local -- src/hopper/repro.test.ts`. The essential body below is the executed test, with its temporary filename replaced. It reuses the repository's document and authenticated socket-pair fixtures and runs the production Python transport. The verifier ran it with isolated HOME and XDG_CONFIG_HOME directories. ```ts import { execFileSync } from "node:child_process"; import { expect, it } from "vitest"; it("classifies absent bookmark APIs as unavailable at the authenticated bridge boundary", () => { const stdout = execFileSync("python3", ["-B", "-c", String.raw` import json import runpy fixture = runpy.run_path("tests/fixtures/hopperBookmarkMutationProbe.py") transport = runpy.run_path("tests/fixtures/hopperBridgeFacadeProbe.py") bridge = transport["load_bridge"]("bridge/hopper_bridge.py") bridge["REA_TOKEN"] = "probe-token" BaseDocument = fixture["FakeDocument"] Provider = fixture["FakeDocumentProvider"] class Document(BaseDocument): missing = None def __getattribute__(self, name): if name == object.__getattribute__(self, "missing"): raise AttributeError(name) return super().__getattribute__(name) def call(method, missing): document = Document() document.names[0x401000] = "fixture" document.missing = missing Provider.document = document bridge["REA_TARGET_PATH"] = document.getExecutableFilePath() bridge["_configure_hopper_api"](Provider) replies = transport["bridge_replies"](bridge, [ transport["request"](method, {} if method == "list_bookmarks" else {"address": "0x401000"}) ]) return {"method": method, "missing": missing, "reply": replies[0]} cases = [ ("list_bookmarks", None), ("unset_bookmark", None), ("set_bookmark", "hasBookmarkAtAddress"), ("list_bookmarks", "getBookmarkName"), ("list_bookmarks", "getBookmarks"), ("unset_bookmark", "hasBookmarkAtAddress"), ("unset_bookmark", "removeBookmarkAtAddress"), ] print(json.dumps([call(*case) for case in cases], sort_keys=True)) `], { encoding: "utf8", timeout: 5_000 }); const results = JSON.parse(stdout); console.log(JSON.stringify(results, null, 2)); expect(results[0].reply.result).toEqual([{ address: "0x401000", name: "fixture" }]); expect(results[1].reply.result).toBe(true); expect(results.slice(2).map((item: any) => item.reply.error.type)).toEqual( Array(5).fill("capability_unavailable"), ); }); ``` One run exercised seven cases. The two success controls passed. The final assertion failed because the three unguarded missing APIs returned `bridge_exception`. Remove the temporary test afterward. This is not a real-Hopper reproduction. ### Expected behavior Absent admitted provider APIs should use the typed capability outcome and preserve a reason naming the unavailable API. This follows the repository invariant to model expected boundary failures with tagged errors and preserve meaningful reasons ([AGENTS.md](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/AGENTS.md#L31-L31)). The bridge explicitly defines `CapabilityUnavailableError` as the case where an active Hopper build lacks an admitted public API operation ([error definition](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/bridge/hopper_bridge.py#L129-L130)). This applies to absent bookmark methods; it does not depend solely on another provider's convention. ### Impact and scope P2: under the reproduced missing-method conditions, callers receive an internal bridge-failure classification and lose the actionable missing-API reason. Source inspection shows that this also selects the public `execution_failure` code instead of `capability_unavailable`. The operation already cannot complete without its native method; the defect is how that limitation is represented. The observed scope is the fake-document bridge boundary on one host. Real Hopper versions, platform frequency, and downstream automation effects remain unknown. No real target was modified, no binary-byte corruption was established, and no security boundary was crossed. No workaround for the incorrect classification was verified. ### REA version rea-agents 6.7.0; source baseline a5f982de8dbe4b092cc20ab89d492b7e7028b758. ### Runtime and platform Node.js v26.12.0; Python 3.12.15; Darwin 27.0.0 arm64; repository fake Hopper document, no Hopper application. ### Target and artifact context Synthetic bound document from tests/fixtures/hopperBookmarkMutationProbe.py; one mapped bookmark at 0x401000; no real target or artifact. ### Interface Python bridge ### Earliest failure stage Transport or result projection ### Relevant output or evidence Trimmed actual test output, with the temporary test filename omitted: ```json {"method":"list_bookmarks","missing":"getBookmarks","reply":{"error":{"code":-32000,"message":"AttributeError: Hopper bridge operation failed","type":"bridge_exception"},"id":1}} {"method":"unset_bookmark","missing":"hasBookmarkAtAddress","reply":{"error":{"code":-32000,"message":"AttributeError: Hopper bridge operation failed","type":"bridge_exception"},"id":1}} {"method":"unset_bookmark","missing":"removeBookmarkAtAddress","reply":{"error":{"code":-32000,"message":"AttributeError: Hopper bridge operation failed","type":"bridge_exception"},"id":1}} ``` The JSON objects above are compacted from the test's pretty-printed output. The failing assertion printed: ```text - Expected + Received [ "capability_unavailable", "capability_unavailable", - "capability_unavailable", - "capability_unavailable", - "capability_unavailable", + "bridge_exception", + "bridge_exception", + "bridge_exception", ] Test Files 1 failed (1) Tests 1 failed (1) ``` Source references checked at the baseline: - Public operations are admitted by the [listing contract](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/contracts/officialToolContracts.ts#L101-L105) and [removal contract](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/contracts/officialToolContracts.ts#L236-L239). The [provider execution path](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/hopper/HopperProvider.ts#L348-L356) forwards them to the client without a per-bookmark-API guard. - [Listing](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/bridge/hopper_bridge.py#L1005-L1006) and [removal](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/bridge/hopper_bridge.py#L1046-L1053) call the missing methods directly. The [name getter](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/bridge/hopper_bridge.py#L239-L245) and [set-bookmark guard](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/bridge/hopper_bridge.py#L1013-L1017) demonstrate the typed behavior for the control cases. - The [authenticated dispatch](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/bridge/hopper_bridge.py#L1070-L1101), [exception response](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/bridge/hopper_bridge.py#L1118-L1131), and [diagnostic classifier](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/bridge/hopper_bridge.py#L1153-L1174) were executed by the reproduction. - Downstream behavior is source-derived: [client normalization](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/hopper/HopperClient.ts#L609-L630) converts only capability diagnostics. The [public projection](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/domain/analysisErrorProjection.ts#L242-L262) assigns distinct public codes to typed capability failures and remote bridge errors. ### Typed outcome or recovery expected Return `type: "capability_unavailable"` with the missing bookmark API named. Preserve that classification as `AnalysisCapabilityUnavailableError` and public code `capability_unavailable` through client normalization and projection. ### Analysis or suspected cause Source-derived: the listing and removal branches access native methods directly, so Python raises `AttributeError` before the bridge can produce its expected capability error. Guarding these API lookups at their owning bridge boundary, consistently with the existing bookmark guards, would retain the intended classification and reason. No fix was implemented or tested. ### Additional context Open and closed issue searches and PR searches for Hopper/bookmark, capability/bridge_exception, getBookmarks, and hasBookmarkAtAddress found no matching report. [Issue 5475](https://github.com/morluto/rea/issues/5475) was read and concerns stale names after successful bookmark writes, a separate failure. The existence of the guards alone does not prove any released Hopper build lacks these APIs.
Author
Owner

@XinwangTao commented on GitHub (Oct 21, 2026):

Confirmed — absent bookmark APIs (getBookmarks / removeBookmarkAtAddress / hasBookmarkAtAddress in unset_bookmark) surface as a generic AttributeError bridge_exception, while the guarded paths correctly report capability_unavailable naming the missing API. That asymmetry makes the same "feature not in this Hopper build" failure look like a transport fault in some calls. Mapping those unguarded attribute misses to the same capability_unavailable classification (and naming the missing API) would keep diagnosis consistent across all bookmark operations.

<!-- gh-comment-id:6312132950 --> @XinwangTao commented on GitHub (Oct 21, 2026): Confirmed — absent bookmark APIs (getBookmarks / removeBookmarkAtAddress / hasBookmarkAtAddress in unset_bookmark) surface as a generic AttributeError bridge_exception, while the guarded paths correctly report capability_unavailable naming the missing API. That asymmetry makes the same "feature not in this Hopper build" failure look like a transport fault in some calls. Mapping those unguarded attribute misses to the same capability_unavailable classification (and naming the missing API) would keep diagnosis consistent across all bookmark operations.
Author
Owner

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

At d87e73f2db50be72d33a4b3c7199af35518b441c, bookmark API lookup is guarded for listing and removal. The regression assertions require capability_unavailable and the exact missing API name for all three reported paths.

I ran the cited regression file on this revision; all passed as part of the focused 28-file, 603-test run. Provider-facing checks here use controlled fixtures; this does not claim a run with the real analysis engine. Closing as completed on main.

<!-- gh-comment-id:6380650968 --> @coygeek commented on GitHub (Oct 24, 2026): At `d87e73f2db50be72d33a4b3c7199af35518b441c`, [bookmark API lookup is guarded](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/bridge/hopper_bridge.py#L327-L337) for [listing](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/bridge/hopper_bridge.py#L1114-L1115) and [removal](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/bridge/hopper_bridge.py#L1140-L1149). The [regression assertions](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/providers/hopper/hopperBookmarks.test.ts#L63-L78) require `capability_unavailable` and the exact missing API name for all three reported paths. I ran the cited regression file on this revision; all passed as part of the focused 28-file, 603-test run. Provider-facing checks here use controlled fixtures; this does not claim a run with the real analysis engine. 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#3531
No description provided.