[GH-ISSUE #7613] [Bug] Resource-only sessions advertise three artifact tools that their dispatch refuses #4545

Closed
opened 2026-10-31 00:50:39 +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/7613

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

After open_bundle_resources accepts a generated app bundle without CFBundleExecutable, binary_session {} identifies an open bundle_resources session but advertises inspect_artifact, extract_artifact, and trace_dylib_resolution with available: true, reason: "available", and null remediation. binary_session {"detail":"complete"} gives the same readiness facts.

On the same unchanged connection, these requests all return isError: true and capability_unavailable:

  • inspect_artifact {}
  • extract_artifact {"integrity_policy":"fail"}
  • trace_dylib_resolution {"roots":["Contents/MacOS/Absent"],"architecture":"arm64"}

Each error identifies provider_id: "bundle-resources" and says the active session contains only app resource access. Discovery still advertises all three after the failures. The four documented resource operations succeed in that session, including a valid keyed archive selected by its bundle-relative path.

Reproduction

  1. Use the built source checkout at the revision below with Node 24.22.1 on PATH. Save the following as probe.py in an empty working directory outside the checkout. The script creates its app and keyed archive, isolates HOME/XDG/TMPDIR, closes the resource session, and deletes its fixture directory.
import json, os, pathlib, plistlib, subprocess, sys, tempfile, threading

repo = pathlib.Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(dir='.', prefix='resource-check-') as folder:
    work = pathlib.Path(folder).resolve()
    app = work / 'Example.app'
    resources = app / 'Contents' / 'Resources'
    resources.mkdir(parents=True)
    (app / 'Contents' / 'Info.plist').write_bytes(plistlib.dumps({
        'CFBundleIdentifier': 'org.example.verifier', 'CFBundleName': 'Example'}))
    (resources / 'archive.plist').write_bytes(plistlib.dumps({
        '$archiver': 'NSKeyedArchiver', '$version': 100000,
        '$objects': ['$null', 'hello'], '$top': {'root': plistlib.UID(1)}},
        fmt=plistlib.FMT_BINARY))
    env = {'PATH': os.environ['PATH'], 'REA_LOG_LEVEL': 'silent'}
    for key in ('HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME',
                'XDG_STATE_HOME', 'TMPDIR'):
        directory = work / key.lower()
        directory.mkdir()
        env[key] = str(directory)
    p = subprocess.Popen(['node', str(repo / 'scripts/rea.mjs'), '--mcp'],
        cwd=work, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
        stderr=subprocess.DEVNULL, text=True)
    timer = threading.Timer(50, p.kill)
    timer.start()
    counter = 0
    def send(message):
        p.stdin.write(json.dumps(message) + '\n')
        p.stdin.flush()
    def request(method, params):
        global counter
        counter += 1
        send({'jsonrpc': '2.0', 'id': counter, 'method': method, 'params': params})
        for line in p.stdout:
            message = json.loads(line)
            if message.get('id') == counter:
                if 'error' in message: raise RuntimeError(message['error'])
                return message['result']
        raise RuntimeError('server exited before replying')
    def call(name, args):
        reply = request('tools/call', {'name': name, 'arguments': args})
        body = reply.get('structuredContent') or json.loads(reply['content'][0]['text'])
        return reply, body
    def discover(label, args):
        _, body = call('binary_session', args)
        status = body['result']
        print(label, json.dumps({'open': status['open'],
            'session_kind': status.get('session_kind')}))
        for name in ('inspect_artifact', 'extract_artifact', 'trace_dylib_resolution'):
            group = next(g for g in status['tool_availability']['groups'] if name in g['names'])
            print(label, name, json.dumps({k: group[k] for k in
                ('available', 'reason', 'remediation')}))
    try:
        request('initialize', {'protocolVersion': '2025-11-25', 'capabilities': {},
            'clientInfo': {'name': 'resource-verifier', 'version': '1'}})
        send({'jsonrpc': '2.0', 'method': 'notifications/initialized'})
        reply, opened = call('open_bundle_resources', {'path': str(app)})
        assert not reply.get('isError'), opened
        print('executable_sha256', opened['result']['executable_sha256'])
        discover('summary', {})
        discover('complete', {'detail': 'complete'})
        for name, args in (
            ('inspect_artifact', {}),
            ('extract_artifact', {'integrity_policy': 'fail'}),
            ('trace_dylib_resolution', {'roots': ['Contents/MacOS/Absent'], 'architecture': 'arm64'})):
            reply, body = call(name, args)
            print('call', name, reply.get('isError', False), json.dumps(body))
        for name, args in (
            ('inspect_plist', {}), ('decode_interface_builder', {}),
            ('inspect_asset_catalog', {}),
            ('inspect_keyed_archive', {'path': 'Contents/Resources/archive.plist'})):
            reply, body = call(name, args)
            print('control', name, reply.get('isError', False))
            assert not reply.get('isError'), body
        discover('after-calls', {})
        reply, body = call('close_binary', {})
        assert not reply.get('isError'), body
    finally:
        p.stdin.close()
        try: p.wait(timeout=10)
        except subprocess.TimeoutExpired:
            p.terminate()
            p.wait(timeout=5)
        timer.cancel()
        print('server_exit', p.returncode)
  1. Run python3 probe.py path-to-rea, replacing path-to-rea with the built checkout directory. No executable fixture or analysis engine is required.

  2. Compare the summary and complete readiness records with the three call failures, the four successful control records, and the unchanged after-calls records. The inline script was executed once unchanged; two earlier complete runs of an independently authored fuller driver produced the same mismatch. All three complete runs closed the session and printed server exit 0.

Expected behavior

Current-session discovery should mark these three operations unavailable while the resource-only session is active, with an explanation of the session restriction and recovery through open_binary when executable analysis is wanted. Supported resource operations should retain their availability.

The existing contract directs clients to select callable operations from binary_session.tool_availability, retaining availability, reason and remediation for each tool. It explicitly supports resource sessions without an executable declaration, identifies their four resource operations, and excludes binary-only operations from that session. This expectation concerns an unconditional session prerequisite; it does not require discovery to guarantee success for every argument or resource file.

Impact and scope

The tested discovery result gives a false readiness signal for three operations that immediately refuse the current session. A client following that result receives predictable failures and discovers the session restriction only after invocation. The same session's actual resource operations remain usable.

A practical client workaround is to inspect the correctly returned session_kind: "bundle_resources" and restrict session-bound artifact requests to the four documented resource operations, handling typed refusals if called directly. That avoids these failed selections without changing REA. The generated bundle and this macOS host were tested; other hosts, other session kinds, downstream client behavior and successful executable analysis were not tested. No data loss or security boundary crossing was observed.

REA version

Exact stdout of node scripts/rea.mjs --version (followed by a newline):

6.8.0

Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a. The supplied compiled runtime was used without rebuilding; the initialized MCP server also identified version 6.8.0.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; arm64. Production newline-delimited JSON-RPC stdio entry point node scripts/rea.mjs --mcp, with isolated HOME, XDG directories and TMPDIR. No MCP client SDK was needed. No executable target, Hopper, or GUI app was opened.

Target and artifact context

A generated Example.app directory containing Contents/Info.plist with a bundle identifier and name but no executable declaration, plus a binary NSKeyedArchiver resource containing one string. There is no executable file. Opening reports executable_sha256: null; status has no deep-provider binding. Empty Interface Builder and asset-catalog inventories serve as resource-lane controls, not real asset-decoding coverage.

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

Observed through the inline production-stdio client, condensed to the relevant records:

executable_sha256 None
summary {"open": true, "session_kind": "bundle_resources"}
summary inspect_artifact {"available": true, "reason": "available", "remediation": null}
summary extract_artifact {"available": true, "reason": "available", "remediation": null}
summary trace_dylib_resolution {"available": true, "reason": "available", "remediation": null}
complete inspect_artifact {"available": true, "reason": "available", "remediation": null}
complete extract_artifact {"available": true, "reason": "available", "remediation": null}
complete trace_dylib_resolution {"available": true, "reason": "available", "remediation": null}
control inspect_plist False
control decode_interface_builder False
control inspect_asset_catalog False
control inspect_keyed_archive False
server_exit 0

False in each control is the printed isError value. All three call records have this error shape, with details.operation naming the requested tool:

{
  "code": "capability_unavailable",
  "category": "unsupported_provider",
  "retryable": false,
  "details": {
    "provider_id": "bundle-resources",
    "operation": "inspect_artifact",
    "reason": "This operation requires an executable analysis target; the active session contains only app resource access."
  }
}

The error's top-level remediation says to choose another analysis or target. The after-calls discovery records repeat the same three available/available/null entries.

Contract locators at the baseline: docs/mcp-contracts.md:614-623 defines callable discovery; docs/mcp-contracts.md:694-703 defines the resource lane and executable-analysis switch. Public registration and status composition are at src/server/registerSessionTools.ts:297-311 and src/server/registerSessionStatusTool.ts:110-155.

Source-only explanation checked against the fuller driver's actual status: src/composition/binary.ts:112-123 filters provider descriptors to the four resource operations. Runtime capabilities likewise contains exactly those four, excluding all three falsely ready tools. src/application/CapabilityInventory.ts:249-269 passes open state, target kind/format and descriptors into availability without a session-kind fact. At src/application/CapabilityInventory.ts:794-807, an absent artifact-provider descriptor falls back to available. Dispatch at src/application/binary/BinarySession.ts:1187-1197 rejects operations outside the resource allow-list before invoking their provider.

Typed outcome or recovery expected

In both summary and complete status, these three operations should report available: false with a reason and remediation consistent with their active resource-only session. Preserve their typed direct-call refusals, the complete canonical tool catalog, the four working resource operations, and successful close_binary behavior. These are proposed restoration checks; no implementation change or repaired build was tested.

Analysis or suspected cause

Source-derived: the resource capability list already reflects the resource-only dispatch surface. The downstream inventory treats missing artifact-provider descriptors as available once open is true, without checking this session kind. This explains the contradiction more directly than a provider-capability union leaking unrestricted operations. The observed refusal originates in the session's first resource gate; the composed resource client has a later protective gate as well.

Additional context

Duplicate checks covered open and closed issues, issue comments, and open/closed/merged PRs. Related reports have different restoration criteria:

  • #7310 addresses address-context discovery with no open target; #7399 and #7397 address address context and navigation on plist targets. #7419 addresses debugger host restrictions.
  • Closed #5731, fixed by merged #5763, concerns ZIP/plist BinaryTarget applicability. This resource-only session has no BinaryTarget and the three operation descriptors are absent. The earlier fix does not claim this lane's readiness behavior; this report does not establish a regression of its ZIP/plist checks.
  • Closed #5589 restored resource inspection without an executable declaration. Its closure explicitly keeps binary/snapshot operations inapplicable. All four resource controls pass here. Open #5764 documents the existing lane and changes no runtime code.

NOT_ENOUGH_INFO: a last known good revision and the first affected revision were not established. This report covers the tested baseline only. Prepared with AI assistance and independently reproduced.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Three current-session readiness entries repeatedly contradict valid immediate invocation, causing bounded discovery-guided selection failures while resource inspection remains functional and refusals stay typed.
  • Workaround status: available
  • Workaround basis: The observed status correctly identifies bundle_resources; callers can use that fact and the documented four-operation resource set to avoid these unsupported session-bound requests.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7613 ### 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 After `open_bundle_resources` accepts a generated app bundle without `CFBundleExecutable`, `binary_session {}` identifies an open `bundle_resources` session but advertises `inspect_artifact`, `extract_artifact`, and `trace_dylib_resolution` with `available: true`, `reason: "available"`, and null remediation. `binary_session {"detail":"complete"}` gives the same readiness facts. On the same unchanged connection, these requests all return `isError: true` and `capability_unavailable`: - `inspect_artifact {}` - `extract_artifact {"integrity_policy":"fail"}` - `trace_dylib_resolution {"roots":["Contents/MacOS/Absent"],"architecture":"arm64"}` Each error identifies `provider_id: "bundle-resources"` and says the active session contains only app resource access. Discovery still advertises all three after the failures. The four documented resource operations succeed in that session, including a valid keyed archive selected by its bundle-relative path. ### Reproduction 1. Use the built source checkout at the revision below with Node 24.22.1 on PATH. Save the following as `probe.py` in an empty working directory outside the checkout. The script creates its app and keyed archive, isolates HOME/XDG/TMPDIR, closes the resource session, and deletes its fixture directory. ```python import json, os, pathlib, plistlib, subprocess, sys, tempfile, threading repo = pathlib.Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(dir='.', prefix='resource-check-') as folder: work = pathlib.Path(folder).resolve() app = work / 'Example.app' resources = app / 'Contents' / 'Resources' resources.mkdir(parents=True) (app / 'Contents' / 'Info.plist').write_bytes(plistlib.dumps({ 'CFBundleIdentifier': 'org.example.verifier', 'CFBundleName': 'Example'})) (resources / 'archive.plist').write_bytes(plistlib.dumps({ '$archiver': 'NSKeyedArchiver', '$version': 100000, '$objects': ['$null', 'hello'], '$top': {'root': plistlib.UID(1)}}, fmt=plistlib.FMT_BINARY)) env = {'PATH': os.environ['PATH'], 'REA_LOG_LEVEL': 'silent'} for key in ('HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'TMPDIR'): directory = work / key.lower() directory.mkdir() env[key] = str(directory) p = subprocess.Popen(['node', str(repo / 'scripts/rea.mjs'), '--mcp'], cwd=work, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True) timer = threading.Timer(50, p.kill) timer.start() counter = 0 def send(message): p.stdin.write(json.dumps(message) + '\n') p.stdin.flush() def request(method, params): global counter counter += 1 send({'jsonrpc': '2.0', 'id': counter, 'method': method, 'params': params}) for line in p.stdout: message = json.loads(line) if message.get('id') == counter: if 'error' in message: raise RuntimeError(message['error']) return message['result'] raise RuntimeError('server exited before replying') def call(name, args): reply = request('tools/call', {'name': name, 'arguments': args}) body = reply.get('structuredContent') or json.loads(reply['content'][0]['text']) return reply, body def discover(label, args): _, body = call('binary_session', args) status = body['result'] print(label, json.dumps({'open': status['open'], 'session_kind': status.get('session_kind')})) for name in ('inspect_artifact', 'extract_artifact', 'trace_dylib_resolution'): group = next(g for g in status['tool_availability']['groups'] if name in g['names']) print(label, name, json.dumps({k: group[k] for k in ('available', 'reason', 'remediation')})) try: request('initialize', {'protocolVersion': '2025-11-25', 'capabilities': {}, 'clientInfo': {'name': 'resource-verifier', 'version': '1'}}) send({'jsonrpc': '2.0', 'method': 'notifications/initialized'}) reply, opened = call('open_bundle_resources', {'path': str(app)}) assert not reply.get('isError'), opened print('executable_sha256', opened['result']['executable_sha256']) discover('summary', {}) discover('complete', {'detail': 'complete'}) for name, args in ( ('inspect_artifact', {}), ('extract_artifact', {'integrity_policy': 'fail'}), ('trace_dylib_resolution', {'roots': ['Contents/MacOS/Absent'], 'architecture': 'arm64'})): reply, body = call(name, args) print('call', name, reply.get('isError', False), json.dumps(body)) for name, args in ( ('inspect_plist', {}), ('decode_interface_builder', {}), ('inspect_asset_catalog', {}), ('inspect_keyed_archive', {'path': 'Contents/Resources/archive.plist'})): reply, body = call(name, args) print('control', name, reply.get('isError', False)) assert not reply.get('isError'), body discover('after-calls', {}) reply, body = call('close_binary', {}) assert not reply.get('isError'), body finally: p.stdin.close() try: p.wait(timeout=10) except subprocess.TimeoutExpired: p.terminate() p.wait(timeout=5) timer.cancel() print('server_exit', p.returncode) ``` 2. Run `python3 probe.py path-to-rea`, replacing `path-to-rea` with the built checkout directory. No executable fixture or analysis engine is required. 3. Compare the summary and complete readiness records with the three `call` failures, the four successful `control` records, and the unchanged `after-calls` records. The inline script was executed once unchanged; two earlier complete runs of an independently authored fuller driver produced the same mismatch. All three complete runs closed the session and printed server exit 0. ### Expected behavior Current-session discovery should mark these three operations unavailable while the resource-only session is active, with an explanation of the session restriction and recovery through `open_binary` when executable analysis is wanted. Supported resource operations should retain their availability. The existing contract directs clients to select callable operations from `binary_session.tool_availability`, retaining availability, reason and remediation for each tool. It explicitly supports resource sessions without an executable declaration, identifies their four resource operations, and excludes binary-only operations from that session. This expectation concerns an unconditional session prerequisite; it does not require discovery to guarantee success for every argument or resource file. ### Impact and scope The tested discovery result gives a false readiness signal for three operations that immediately refuse the current session. A client following that result receives predictable failures and discovers the session restriction only after invocation. The same session's actual resource operations remain usable. A practical client workaround is to inspect the correctly returned `session_kind: "bundle_resources"` and restrict session-bound artifact requests to the four documented resource operations, handling typed refusals if called directly. That avoids these failed selections without changing REA. The generated bundle and this macOS host were tested; other hosts, other session kinds, downstream client behavior and successful executable analysis were not tested. No data loss or security boundary crossing was observed. ### REA version Exact stdout of `node scripts/rea.mjs --version` (followed by a newline): ```text 6.8.0 ``` Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. The supplied compiled runtime was used without rebuilding; the initialized MCP server also identified version 6.8.0. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1; arm64. Production newline-delimited JSON-RPC stdio entry point `node scripts/rea.mjs --mcp`, with isolated HOME, XDG directories and TMPDIR. No MCP client SDK was needed. No executable target, Hopper, or GUI app was opened. ### Target and artifact context A generated `Example.app` directory containing `Contents/Info.plist` with a bundle identifier and name but no executable declaration, plus a binary NSKeyedArchiver resource containing one string. There is no executable file. Opening reports `executable_sha256: null`; status has no deep-provider binding. Empty Interface Builder and asset-catalog inventories serve as resource-lane controls, not real asset-decoding coverage. ### Interface MCP ### Earliest failure stage Transport or result projection ### Relevant output or evidence Observed through the inline production-stdio client, condensed to the relevant records: ```text executable_sha256 None summary {"open": true, "session_kind": "bundle_resources"} summary inspect_artifact {"available": true, "reason": "available", "remediation": null} summary extract_artifact {"available": true, "reason": "available", "remediation": null} summary trace_dylib_resolution {"available": true, "reason": "available", "remediation": null} complete inspect_artifact {"available": true, "reason": "available", "remediation": null} complete extract_artifact {"available": true, "reason": "available", "remediation": null} complete trace_dylib_resolution {"available": true, "reason": "available", "remediation": null} control inspect_plist False control decode_interface_builder False control inspect_asset_catalog False control inspect_keyed_archive False server_exit 0 ``` `False` in each control is the printed `isError` value. All three `call` records have this error shape, with `details.operation` naming the requested tool: ```json { "code": "capability_unavailable", "category": "unsupported_provider", "retryable": false, "details": { "provider_id": "bundle-resources", "operation": "inspect_artifact", "reason": "This operation requires an executable analysis target; the active session contains only app resource access." } } ``` The error's top-level remediation says to choose another analysis or target. The `after-calls` discovery records repeat the same three available/available/null entries. Contract locators at the baseline: `docs/mcp-contracts.md:614-623` defines callable discovery; `docs/mcp-contracts.md:694-703` defines the resource lane and executable-analysis switch. Public registration and status composition are at `src/server/registerSessionTools.ts:297-311` and `src/server/registerSessionStatusTool.ts:110-155`. Source-only explanation checked against the fuller driver's actual status: `src/composition/binary.ts:112-123` filters provider descriptors to the four resource operations. Runtime `capabilities` likewise contains exactly those four, excluding all three falsely ready tools. `src/application/CapabilityInventory.ts:249-269` passes open state, target kind/format and descriptors into availability without a session-kind fact. At `src/application/CapabilityInventory.ts:794-807`, an absent artifact-provider descriptor falls back to available. Dispatch at `src/application/binary/BinarySession.ts:1187-1197` rejects operations outside the resource allow-list before invoking their provider. ### Typed outcome or recovery expected In both summary and complete status, these three operations should report `available: false` with a reason and remediation consistent with their active resource-only session. Preserve their typed direct-call refusals, the complete canonical tool catalog, the four working resource operations, and successful `close_binary` behavior. These are proposed restoration checks; no implementation change or repaired build was tested. ### Analysis or suspected cause Source-derived: the resource capability list already reflects the resource-only dispatch surface. The downstream inventory treats missing artifact-provider descriptors as available once `open` is true, without checking this session kind. This explains the contradiction more directly than a provider-capability union leaking unrestricted operations. The observed refusal originates in the session's first resource gate; the composed resource client has a later protective gate as well. ### Additional context Duplicate checks covered open and closed issues, issue comments, and open/closed/merged PRs. Related reports have different restoration criteria: - [#7310](https://github.com/morluto/rea/issues/7310) addresses address-context discovery with no open target; [#7399](https://github.com/morluto/rea/issues/7399) and [#7397](https://github.com/morluto/rea/issues/7397) address address context and navigation on plist targets. [#7419](https://github.com/morluto/rea/issues/7419) addresses debugger host restrictions. - Closed [#5731](https://github.com/morluto/rea/issues/5731), fixed by merged [#5763](https://github.com/morluto/rea/pull/5763), concerns ZIP/plist BinaryTarget applicability. This resource-only session has no BinaryTarget and the three operation descriptors are absent. The earlier fix does not claim this lane's readiness behavior; this report does not establish a regression of its ZIP/plist checks. - Closed [#5589](https://github.com/morluto/rea/issues/5589) restored resource inspection without an executable declaration. Its closure explicitly keeps binary/snapshot operations inapplicable. All four resource controls pass here. Open [#5764](https://github.com/morluto/rea/pull/5764) documents the existing lane and changes no runtime code. NOT_ENOUGH_INFO: a last known good revision and the first affected revision were not established. This report covers the tested baseline only. Prepared with AI assistance and independently reproduced. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Three current-session readiness entries repeatedly contradict valid immediate invocation, causing bounded discovery-guided selection failures while resource inspection remains functional and refusals stay typed. - Workaround status: available - Workaround basis: The observed status correctly identifies bundle_resources; callers can use that fact and the documented four-operation resource set to avoid these unsupported session-bound requests.
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#4545
No description provided.