[GH-ISSUE #5731] [Bug] binary_session advertises Apple artifact operations for unsupported ZIP and plist targets #3478

Closed
opened 2026-10-31 00:40:56 +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/5731

Before submitting

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

Area

Provider or capability discovery

Observed behavior

After open_binary opens a generated ZIP, MCP binary_session {} advertises decode_interface_builder, inspect_asset_catalog, inspect_keyed_archive, and trace_dylib_resolution as available with no remediation. Immediately calling each with {} returns unsupported_target. On standalone plist targets, the first two and dylib resolution show the same mismatch. Keyed archive inspection correctly succeeds on a valid keyed plist and should remain available for that target class.

Reproduction

From a built source checkout with locked dependencies installed, run this isolated stdio MCP reproduction. It generates only ZIP/plist fixtures; no deep-analysis provider or GUI is needed.

work_dir="$(mktemp -d)"
python3 - "$work_dir" <<'PY'
import json, os, pathlib, plistlib, select, subprocess, sys, zipfile

work = pathlib.Path(sys.argv[1])
for name in ("home", "cfg", "data", "cache", "state", "tmp"):
    (work / name).mkdir()
with zipfile.ZipFile(work / "sample.zip", "w") as archive:
    archive.writestr("a.txt", "hello\n")
(work / "plain.plist").write_bytes(plistlib.dumps({"hello": "world"}))
(work / "keyed.plist").write_bytes(plistlib.dumps({
    "$archiver": "NSKeyedArchiver", "$version": 100000,
    "$objects": ["$null", "hello"], "$top": {"root": plistlib.UID(1)}
}, fmt=plistlib.FMT_BINARY))

server = subprocess.Popen([
    "env", "-i", "PATH=" + os.environ["PATH"], "HOME=" + str(work / "home"),
    "XDG_CONFIG_HOME=" + str(work / "cfg"), "XDG_DATA_HOME=" + str(work / "data"),
    "XDG_CACHE_HOME=" + str(work / "cache"), "XDG_STATE_HOME=" + str(work / "state"),
    "TMPDIR=" + str(work / "tmp"), "REA_LOG_LEVEL=silent",
    "node", "scripts/rea.mjs", "mcp"
], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
   stderr=(work / "stderr.log").open("w"), text=True, bufsize=1)
next_id = 0

def request(method, params=None, notification=False):
    global next_id
    next_id += 1
    message = {"jsonrpc": "2.0", "method": method}
    if params is not None:
        message["params"] = params
    if not notification:
        message["id"] = next_id
    server.stdin.write(json.dumps(message) + "\n")
    server.stdin.flush()
    if notification:
        return
    while True:
        if not select.select([server.stdout], [], [], 30)[0]:
            raise RuntimeError("MCP reply timeout")
        line = server.stdout.readline()
        if not line:
            raise RuntimeError("MCP server exited")
        reply = json.loads(line)
        if reply.get("id") == next_id:
            return reply["result"]

def call(name, arguments=None):
    result = request("tools/call", {"name": name, "arguments": arguments or {}})
    structured = result.get("structuredContent", {})
    return structured.get("result") or json.loads(result["content"][0]["text"])

tools = {"decode_interface_builder", "inspect_asset_catalog",
         "inspect_keyed_archive", "trace_dylib_resolution"}

def discovery(label):
    session = call("binary_session")
    for group in session["tool_availability"]["groups"]:
        names = sorted(tools.intersection(group["names"]))
        if names:
            print(label, names, group["available"], group["reason"], group["remediation"])

try:
    request("initialize", {"protocolVersion": "2025-11-25", "capabilities": {},
            "clientInfo": {"name": "reproduction", "version": "1"}})
    request("notifications/initialized", notification=True)
    discovery("before-open")
    for filename in ("sample.zip", "plain.plist", "keyed.plist"):
        opened = call("open_binary", {"path": str(work / filename)})
        print(filename, "opened", opened["kind"], opened["format"])
        discovery(filename)
        for name in sorted(tools):
            result = call(name)
            error = result.get("error")
            print(filename, name,
                  (error["code"], error["details"].get("reason")) if error else "success")
        call("close_binary")
finally:
    server.stdin.close()
    try:
        server.wait(timeout=15)
    except subprocess.TimeoutExpired:
        server.terminate()
        server.wait(timeout=5)
PY

The independent verifier ran this sequence at the baseline below. Before filing, I built current main with npm run build:cached and replayed the same fixture/discovery/invocation sequence there; the mismatch persists. Each run closes all three targets, and the MCP server exits 0.

Expected behavior

binary_session.tool_availability should identify target applicability before invocation, consistent with the documented discovery contract. All four operations should be unavailable for ZIP with reason:"target_unsupported" and remediation naming their supported target classes. For standalone plists, only Interface Builder, asset catalog, and dylib resolution should be unavailable for this reason; keyed archive inspection should remain available and validate content when called. Dylib resolution must retain standalone Mach-O support as well as app-bundle support.

Impact and scope

P2: clients following discovery select operations that deterministically refuse their current target, wasting calls and receiving target prerequisites only after invocation. Both tested revisions reproduce this for the generated ZIP and plist fixtures. The refusals are typed and recoverable; callers can use the documented accepted target classes and handle unsupported_target instead of relying solely on discovery. No data loss or security boundary crossing was observed. Other hosts and downstream client occurrence frequency were not measured.

REA version

rea-agents 6.7.0, verified baseline 9d8e7803326a382cbbb3ffdb900f5e93e5442d68. Rechecked after building fetched origin/main at d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f (also 6.7.0): still reproduces. The latest published release is rea-agents-6.7.0; these are source-checkout reproductions.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1; arm64. HOME, XDG directories, and TMPDIR isolated; REA_LOG_LEVEL=silent. Hopper, Ghidra, and IDA are not involved.

Target and artifact context

Generated ZIP containing one text entry; ordinary XML plist; binary NSKeyedArchiver plist containing one string root. ZIP opens as kind:"archive", format:"zip"; both plists open as kind:"artifact", format:"plist".

Interface

MCP

Earliest failure stage

Setup or provider admission

Relevant output or evidence

Observed on both tested builds, condensed from the production stdio MCP responses:

before-open: all four unavailable / target_required / open_binary remediation
ZIP: all four available / available / null remediation
ZIP decode_interface_builder -> unsupported_target: requires an active .app bundle target
ZIP inspect_asset_catalog -> unsupported_target: requires an active .app bundle target
ZIP inspect_keyed_archive -> unsupported_target: requires an active plist or .app bundle
ZIP trace_dylib_resolution -> unsupported_target: requires an active Mach-O or .app bundle target
both plists: all four available / available / null remediation
both plists: decode_interface_builder, inspect_asset_catalog, trace_dylib_resolution -> unsupported_target
plain plist inspect_keyed_archive -> unsupported_target: no $archiver key
keyed plist inspect_keyed_archive -> success
server exit: 0

The plain-plist content refusal does not establish a target-class discovery defect for keyed archive inspection; the valid keyed-plist success is the distinguishing control. Evidence provenance: observed; local verification: independently reproduced; reproduction completeness: complete for these fixtures.

Typed outcome or recovery expected

Unavailable discovery facts must identify target_unsupported and the accepted target classes while preserving operation-level typed refusals. Keep the before-open target_required behavior and the valid keyed-plist success. Proposed additional restoration controls: supported app bundles retain availability for applicable artifact operations, and standalone Mach-O retains dylib resolution availability. Those two positive controls were not exercised here; no patch was tested.

Analysis or suspected cause

Source-derived reasoning: CapabilityInventory.ts:682-718 checks no-target state and a fixed set of Mach-O operations but lacks these four operations' target constraints. Their available provider descriptors therefore win. The execution guards in ArtifactProvider.ts and its asset-catalog/dylib-resolution methods reject the same targets. This agrees with the runtime mismatch; no bundled dependency was identified as its cause.

Additional context

Still reproduces after #1166. Its merged fix #1176 explicitly lists these four artifact operations under “Remaining gaps”; it fixed missing-target discovery and selected Mach-O tools, leaving this after-open applicability case unresolved. Merged #1161 established the operation-level unsupported_target refusals, which should remain.

Related #5711 concerns unsupported-host pcap discovery; #5447 concerns false target requirements for independent tools; #5729 concerns the missing-target navigation reason; #5730 concerns Linux-only tools on macOS. Their fixes would leave this after-open unsupported-target false positive unchanged. Prepared with AI assistance and independently verified. NOT_ENOUGH_INFO: last known good revision and first affected revision were not established.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5731 ### 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 Provider or capability discovery ### Observed behavior After `open_binary` opens a generated ZIP, MCP `binary_session {}` advertises `decode_interface_builder`, `inspect_asset_catalog`, `inspect_keyed_archive`, and `trace_dylib_resolution` as available with no remediation. Immediately calling each with `{}` returns `unsupported_target`. On standalone plist targets, the first two and dylib resolution show the same mismatch. Keyed archive inspection correctly succeeds on a valid keyed plist and should remain available for that target class. ### Reproduction From a built source checkout with locked dependencies installed, run this isolated stdio MCP reproduction. It generates only ZIP/plist fixtures; no deep-analysis provider or GUI is needed. ```sh work_dir="$(mktemp -d)" python3 - "$work_dir" <<'PY' import json, os, pathlib, plistlib, select, subprocess, sys, zipfile work = pathlib.Path(sys.argv[1]) for name in ("home", "cfg", "data", "cache", "state", "tmp"): (work / name).mkdir() with zipfile.ZipFile(work / "sample.zip", "w") as archive: archive.writestr("a.txt", "hello\n") (work / "plain.plist").write_bytes(plistlib.dumps({"hello": "world"})) (work / "keyed.plist").write_bytes(plistlib.dumps({ "$archiver": "NSKeyedArchiver", "$version": 100000, "$objects": ["$null", "hello"], "$top": {"root": plistlib.UID(1)} }, fmt=plistlib.FMT_BINARY)) server = subprocess.Popen([ "env", "-i", "PATH=" + os.environ["PATH"], "HOME=" + str(work / "home"), "XDG_CONFIG_HOME=" + str(work / "cfg"), "XDG_DATA_HOME=" + str(work / "data"), "XDG_CACHE_HOME=" + str(work / "cache"), "XDG_STATE_HOME=" + str(work / "state"), "TMPDIR=" + str(work / "tmp"), "REA_LOG_LEVEL=silent", "node", "scripts/rea.mjs", "mcp" ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=(work / "stderr.log").open("w"), text=True, bufsize=1) next_id = 0 def request(method, params=None, notification=False): global next_id next_id += 1 message = {"jsonrpc": "2.0", "method": method} if params is not None: message["params"] = params if not notification: message["id"] = next_id server.stdin.write(json.dumps(message) + "\n") server.stdin.flush() if notification: return while True: if not select.select([server.stdout], [], [], 30)[0]: raise RuntimeError("MCP reply timeout") line = server.stdout.readline() if not line: raise RuntimeError("MCP server exited") reply = json.loads(line) if reply.get("id") == next_id: return reply["result"] def call(name, arguments=None): result = request("tools/call", {"name": name, "arguments": arguments or {}}) structured = result.get("structuredContent", {}) return structured.get("result") or json.loads(result["content"][0]["text"]) tools = {"decode_interface_builder", "inspect_asset_catalog", "inspect_keyed_archive", "trace_dylib_resolution"} def discovery(label): session = call("binary_session") for group in session["tool_availability"]["groups"]: names = sorted(tools.intersection(group["names"])) if names: print(label, names, group["available"], group["reason"], group["remediation"]) try: request("initialize", {"protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": {"name": "reproduction", "version": "1"}}) request("notifications/initialized", notification=True) discovery("before-open") for filename in ("sample.zip", "plain.plist", "keyed.plist"): opened = call("open_binary", {"path": str(work / filename)}) print(filename, "opened", opened["kind"], opened["format"]) discovery(filename) for name in sorted(tools): result = call(name) error = result.get("error") print(filename, name, (error["code"], error["details"].get("reason")) if error else "success") call("close_binary") finally: server.stdin.close() try: server.wait(timeout=15) except subprocess.TimeoutExpired: server.terminate() server.wait(timeout=5) PY ``` The independent verifier ran this sequence at the baseline below. Before filing, I built current main with `npm run build:cached` and replayed the same fixture/discovery/invocation sequence there; the mismatch persists. Each run closes all three targets, and the MCP server exits 0. ### Expected behavior `binary_session.tool_availability` should identify target applicability before invocation, consistent with [the documented discovery contract](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/docs/mcp-contracts.md#L470). All four operations should be unavailable for ZIP with `reason:"target_unsupported"` and remediation naming their supported target classes. For standalone plists, only Interface Builder, asset catalog, and dylib resolution should be unavailable for this reason; keyed archive inspection should remain available and validate content when called. Dylib resolution must retain standalone Mach-O support as well as app-bundle support. ### Impact and scope P2: clients following discovery select operations that deterministically refuse their current target, wasting calls and receiving target prerequisites only after invocation. Both tested revisions reproduce this for the generated ZIP and plist fixtures. The refusals are typed and recoverable; callers can use the documented accepted target classes and handle `unsupported_target` instead of relying solely on discovery. No data loss or security boundary crossing was observed. Other hosts and downstream client occurrence frequency were not measured. ### REA version `rea-agents` 6.7.0, verified baseline `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`. Rechecked after building fetched `origin/main` at `d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f` (also 6.7.0): still reproduces. The latest published release is `rea-agents-6.7.0`; these are source-checkout reproductions. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1; arm64. HOME, XDG directories, and TMPDIR isolated; `REA_LOG_LEVEL=silent`. Hopper, Ghidra, and IDA are not involved. ### Target and artifact context Generated ZIP containing one text entry; ordinary XML plist; binary NSKeyedArchiver plist containing one string root. ZIP opens as `kind:"archive", format:"zip"`; both plists open as `kind:"artifact", format:"plist"`. ### Interface MCP ### Earliest failure stage Setup or provider admission ### Relevant output or evidence Observed on both tested builds, condensed from the production stdio MCP responses: ```text before-open: all four unavailable / target_required / open_binary remediation ZIP: all four available / available / null remediation ZIP decode_interface_builder -> unsupported_target: requires an active .app bundle target ZIP inspect_asset_catalog -> unsupported_target: requires an active .app bundle target ZIP inspect_keyed_archive -> unsupported_target: requires an active plist or .app bundle ZIP trace_dylib_resolution -> unsupported_target: requires an active Mach-O or .app bundle target both plists: all four available / available / null remediation both plists: decode_interface_builder, inspect_asset_catalog, trace_dylib_resolution -> unsupported_target plain plist inspect_keyed_archive -> unsupported_target: no $archiver key keyed plist inspect_keyed_archive -> success server exit: 0 ``` The plain-plist content refusal does not establish a target-class discovery defect for keyed archive inspection; the valid keyed-plist success is the distinguishing control. Evidence provenance: observed; local verification: independently reproduced; reproduction completeness: complete for these fixtures. ### Typed outcome or recovery expected Unavailable discovery facts must identify `target_unsupported` and the accepted target classes while preserving operation-level typed refusals. Keep the before-open `target_required` behavior and the valid keyed-plist success. Proposed additional restoration controls: supported app bundles retain availability for applicable artifact operations, and standalone Mach-O retains dylib resolution availability. Those two positive controls were not exercised here; no patch was tested. ### Analysis or suspected cause Source-derived reasoning: [CapabilityInventory.ts:682-718](https://github.com/morluto/rea/blob/d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f/src/application/CapabilityInventory.ts#L682-L718) checks no-target state and a fixed set of Mach-O operations but lacks these four operations' target constraints. Their available provider descriptors therefore win. The execution guards in [ArtifactProvider.ts](https://github.com/morluto/rea/blob/d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f/src/artifacts/ArtifactProvider.ts#L232-L278) and its asset-catalog/dylib-resolution methods reject the same targets. This agrees with the runtime mismatch; no bundled dependency was identified as its cause. ### Additional context Still reproduces after #1166. Its merged fix #1176 explicitly lists these four artifact operations under “Remaining gaps”; it fixed missing-target discovery and selected Mach-O tools, leaving this after-open applicability case unresolved. Merged #1161 established the operation-level `unsupported_target` refusals, which should remain. Related #5711 concerns unsupported-host pcap discovery; #5447 concerns false target requirements for independent tools; #5729 concerns the missing-target navigation reason; #5730 concerns Linux-only tools on macOS. Their fixes would leave this after-open unsupported-target false positive unchanged. Prepared with AI assistance and independently verified. NOT_ENOUGH_INFO: last known good revision and first affected revision 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#3478
No description provided.