Skip to content

fix: UE 5.8 survival -- Capability Ladder + bAllowAnyRemoteFunctionCall (v0.3.1) - #20

Merged
JosephOIbrahim merged 3 commits into
masterfrom
fix/ue58-preflight
Jul 8, 2026
Merged

fix: UE 5.8 survival -- Capability Ladder + bAllowAnyRemoteFunctionCall (v0.3.1)#20
JosephOIbrahim merged 3 commits into
masterfrom
fix/ue58-preflight

Conversation

@JosephOIbrahim

@JosephOIbrahim JosephOIbrahim commented Jul 8, 2026

Copy link
Copy Markdown
Owner

5.8 survival -- the Capability Ladder

The bridge was silently broken on UE 5.8. 5.8 defaults
RemoteControlSettings.bAllowAnyRemoteFunctionCall to False, which blocks every
/remote/object/call with HTTP 400 "not allowed by remote control settings". So
execute_python and every codegen tool failed live -- while ue_status and the
mock-only CI stayed green. Connectivity was measured; capability never was.

Fixed

  • bAllowAnyRemoteFunctionCall=True in Config/DefaultRemoteControl.ini --
    the one line that unblocks every tool on 5.8 (localhost-only, single-trusted-
    operator model; the bridge already permits arbitrary Python exec over RC).
  • The Remote Control error body is no longer swallowed -- the clients
    returned only "Client error '400 '"; they now preserve the RC body that says
    why.

Added -- the Capability Ladder (so this can't recur silently)

  • ue_preflight + ue_health_check(deep=true): a runtime probe that
    separates reachable (a GET) from permitted / capable (a real function
    call) / round-trip (the full Python path), stops at the first failure, and
    returns the named cause + one-line fix with the raw RC body as evidence.
  • A diagnosis map (error signature -> cause -> fix) -- every future live
    failure becomes a rule.
  • 13 tests incl. the golden failure test: the 5.8 block must surface as
    rung Permitted naming bAllowAnyRemoteFunctionCall.

Why it matters: connectivity, permission, and capability are three different
signals. The bridge measured one. A health check can never again read green
while the bridge is dead -- and any failure now names its own fix.

Summary by CodeRabbit

  • New Features

    • Added a new preflight check to help verify connection, permissions, and full round-trip capability.
    • Added a deeper health check option and a dedicated preflight tool with clearer diagnostic output.
  • Bug Fixes

    • Improved handling of Remote Control failures so error messages now include the server’s response details.
    • Restored compatibility with UE 5.8+ by enabling the required Remote Control setting in the default configuration.
  • Tests

    • Added coverage for capability checks, failure diagnosis, and preserved error details.

Joseph Ibrahim and others added 3 commits July 8, 2026 10:34
UE 5.8 added RemoteControlSettings.bAllowAnyRemoteFunctionCall (default False),
which blocks EVERY /remote/object/call with HTTP 400 "not allowed by remote
control settings". The bridge worked on 5.7 (remote function calls allowed) and
silently broke on 5.8 -- every codegen tool (execute_python, actors, spatial,
lighting, ...) fails. Mock-only tests never make the RC call, so CI stayed
green while live was dead.

Fix: bAllowAnyRemoteFunctionCall=True in Config/DefaultRemoteControl.ini
(localhost-only, single-trusted-operator; the bridge already permits arbitrary
Python exec over RC, so no real added exposure). Documented the gotcha in
remote_control/execution.py. Root-caused live against UE 5.8 (exact error body
+ engine header RemoteControlSettings.h:351); the full live round trip was not
re-verified in-session (needs the project's RC setting applied).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…to a named fix

Connectivity, permission, and capability are three different properties; the
bridge only measured connectivity (ue_status = a GET). So it could report
"connected" and pass mock-only CI while every codegen tool 400s live -- which is
exactly what UE 5.8's bAllowAnyRemoteFunctionCall=False default caused.

- remote_control/preflight.py: preflight(rc) runs a ladder
  Reachable -> Permitted -> Capable -> RoundTrip, stops at the first red, and
  returns a named cause + one-line fix; a data-driven diagnose() map keyed on the
  RC error body (5.8 function block, python-exec disabled, passphrase, unreachable,
  ...); http_error_detail() preserves the RC response body the clients dropped.
- async_client / sync_client: surface the RC error body instead of the bare
  "Client error '400 '"; add _probe_call for the ladder.
- ue_preflight MCP tool + ue_health_check(deep=true) in mcp_server.py (inline,
  like ue_status/ue_health_check -- out of the tier table + exec-sim registry).
- tests/test_preflight.py: 13 tests incl. the golden FAILURE test -- the 5.8 block
  must surface as rung Permitted naming bAllowAnyRemoteFunctionCall.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unctionCall)

Bump ue_mcp/__version__.py to 0.3.1 (single version source; tag must match) and
add the CHANGELOG entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes a UE 5.8 regression that silently blocks remote-control calls by setting bAllowAnyRemoteFunctionCall=True in config, adds a new preflight "capability ladder" module diagnosing reachability/permission/round-trip failures, wires it into an ue_preflight MCP tool and deep health check, preserves HTTP error bodies in clients, adds tests, and bumps version to 0.3.1.

Changes

Capability Ladder for UE 5.8 Remote Control

Layer / File(s) Summary
Config override for UE 5.8 regression
Config/DefaultRemoteControl.ini, remote_control/execution.py
Sets bAllowAnyRemoteFunctionCall=True with explanatory comments and documents the requirement in the payload-building docstring.
Preflight capability ladder implementation
remote_control/preflight.py, remote_control/__init__.py
Adds RUNGS, Diagnosis, PreflightResult, http_error_detail, diagnose, evidence/return-value helpers, and the async preflight() function probing reachability, permission/capability, and round-trip; re-exports the new API.
Client error-detail preservation
remote_control/async_client.py, remote_control/sync_client.py
Imports http_error_detail, adds a _probe_call helper, and updates execute_python's error handling to return preserved error detail instead of a generic connection-failure message.
MCP tool wiring
ue_mcp/mcp_server.py
Imports preflight, adds a deep parameter to ue_health_check to run the preflight ladder, and registers a new ue_preflight MCP tool.
Preflight test suite
tests/test_preflight.py
Adds fake doubles, a FakeRC test client, golden-failure test for the UE 5.8 block, ladder rung tests, diagnose tests, and http_error_detail tests.
Changelog and version bump
CHANGELOG.md, ue_mcp/__version__.py
Documents the regression, fixes, and capability ladder in a 0.3.1 entry; bumps version metadata to 0.3.1.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPTool as ue_preflight/ue_health_check
  participant preflight
  participant RemoteControlClient
  participant diagnose

  MCPTool->>preflight: preflight(rc)
  preflight->>RemoteControlClient: GET /remote/info
  alt unreachable
    RemoteControlClient-->>preflight: exception
    preflight->>diagnose: diagnose(status, body, exc)
    diagnose-->>preflight: Diagnosis(cause, fix)
    preflight-->>MCPTool: PreflightResult(rung=Reachable)
  else reachable
    preflight->>RemoteControlClient: PUT /remote/object/call
    alt not permitted/capable
      RemoteControlClient-->>preflight: error status/body
      preflight->>diagnose: diagnose(status, body, exc)
      diagnose-->>preflight: Diagnosis(cause, fix)
      preflight-->>MCPTool: PreflightResult(rung=Permitted/Capable)
    else capable
      preflight->>RemoteControlClient: execute_python(sentinel)
      RemoteControlClient-->>preflight: result
      preflight-->>MCPTool: PreflightResult(ok=True, rung=RoundTrip)
    end
  end
  MCPTool-->>MCPTool: return JSON (ok/rung/cause/fix/evidence)
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main UE 5.8 fix, capability ladder addition, and version bump.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ue58-preflight

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
remote_control/preflight.py (2)

86-100: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The "python" body heuristic is fragile for distinguishing error subtypes.

Line 87 uses if "python" in b to distinguish "Remote Python execution disabled" from "function calls blocked by default." The actual UE 5.8 error body wording may not reliably contain "python" for the python-execution case, which could cause misdiagnosis. Consider matching against a more specific substring (e.g., "remote python" or the setting name itself) or documenting the exact error body strings expected from each case.

♻️ Suggested refinement
     if status == 400 and "not allowed by remote control settings" in b:
-        if "python" in b:
+        if "bEnableRemotePythonExecution" in b or "remote python" in b:
             return Diagnosis(
                 "Remote Python execution is disabled "
                 "(RemoteControlSettings.bEnableRemotePythonExecution = False).",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@remote_control/preflight.py` around lines 86 - 100, The error subtype check
in preflight handling is too brittle because the Remote Python case is
identified only by checking for a generic “python” substring in the response
body. Update the Diagnosis selection logic in the preflight flow to use a more
specific discriminator tied to the actual Remote Control error wording or
setting name (for example, the remote Python execution message or
RemoteControlSettings.bEnableRemotePythonExecution), so the branch between
Remote Python disabled and generic remote function-call blocking is reliable
even if the body text changes.

144-193: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

preflight() RoundTrip rung lacks exception guard around execute_python.

Line 181 calls rc.execute_python(...) without a try/except. If execute_python raises (e.g., transient connection drop after the Permitted probe passed), the exception propagates unhandled and the caller gets a crash instead of a PreflightResult with a named cause. The contract says it returns a dict, but a defensive guard would make the ladder robust against partial failures.

🛡️ Suggested defensive guard
     # Rung 3 -- RoundTrip (the full write -> exec -> poll -> result Python path)
-    result = await rc.execute_python('print("RESULT: PREFLIGHT_OK")')
+    try:
+        result = await rc.execute_python('print("RESULT: PREFLIGHT_OK")')
+    except Exception as exc:
+        return PreflightResult(
+            False,
+            "RoundTrip",
+            f"execute_python raised an exception ({type(exc).__name__}).",
+            "Check the Remote Control connection and Python Editor Script Plugin.",
+            _evidence(None, None, exc),
+            engine_version=version,
+        )
     if result.get("result") != "PREFLIGHT_OK":
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@remote_control/preflight.py` around lines 144 - 193, The preflight ladder in
preflight() should defensively handle failures from rc.execute_python during the
RoundTrip rung instead of letting exceptions escape. Wrap the execute_python
call in a try/except, and on any raised exception return a failed
PreflightResult with rung "RoundTrip", a clear cause/fix, and evidence derived
from the exception so the caller gets a structured result rather than a crash.
Keep the change localized to preflight() and preserve the existing
engine_version/version handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@remote_control/async_client.py`:
- Around line 97-108: The _probe_call helper still lets some transport failures
escape, so its “never raises” behavior is incomplete. Update the exception
handling in _probe_call to catch the full httpx.TransportError base class
instead of only specific subclasses, while keeping the existing return shape of
(status, body, exc). Make sure the change preserves the preflight ladder
behavior in AsyncClient and continues to return the captured exception for all
transport-related failures.

---

Nitpick comments:
In `@remote_control/preflight.py`:
- Around line 86-100: The error subtype check in preflight handling is too
brittle because the Remote Python case is identified only by checking for a
generic “python” substring in the response body. Update the Diagnosis selection
logic in the preflight flow to use a more specific discriminator tied to the
actual Remote Control error wording or setting name (for example, the remote
Python execution message or RemoteControlSettings.bEnableRemotePythonExecution),
so the branch between Remote Python disabled and generic remote function-call
blocking is reliable even if the body text changes.
- Around line 144-193: The preflight ladder in preflight() should defensively
handle failures from rc.execute_python during the RoundTrip rung instead of
letting exceptions escape. Wrap the execute_python call in a try/except, and on
any raised exception return a failed PreflightResult with rung "RoundTrip", a
clear cause/fix, and evidence derived from the exception so the caller gets a
structured result rather than a crash. Keep the change localized to preflight()
and preserve the existing engine_version/version handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ee209462-e728-4da1-88f1-c910d4c762bf

📥 Commits

Reviewing files that changed from the base of the PR and between b4f0e56 and 0411c92.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • Config/DefaultRemoteControl.ini
  • remote_control/__init__.py
  • remote_control/async_client.py
  • remote_control/execution.py
  • remote_control/preflight.py
  • remote_control/sync_client.py
  • tests/test_preflight.py
  • ue_mcp/__version__.py
  • ue_mcp/mcp_server.py

Comment on lines +97 to +108
async def _probe_call(self, path: str, method: str = "GET", payload: dict | None = None):
"""Low-level RC call for the capability preflight: returns
(status, body, exc) and never raises, so the ladder can inspect the
real status code and response body instead of a swallowed exception."""
try:
if method == "GET":
r = await self._client.get(path)
else:
r = await self._client.put(path, json=payload)
return r.status_code, r.text, None
except (httpx.ConnectError, httpx.TimeoutException) as e:
return None, "", e

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file outline ==\n'
ast-grep outline remote_control/async_client.py --view expanded || true

printf '\n== relevant lines ==\n'
sed -n '80,125p' remote_control/async_client.py | cat -n

printf '\n== httpx dependency references ==\n'
rg -n 'httpx|TransportError|ConnectError|TimeoutException|ReadError|WriteError|NetworkError|ProtocolError' -S pyproject.toml requirements*.txt setup*.py poetry.lock uv.lock Pipfile* . 2>/dev/null || true

printf '\n== installed httpx version / exception hierarchy probe ==\n'
python3 - <<'PY'
import sys
try:
    import httpx
    print("httpx_version:", getattr(httpx, "__version__", "unknown"))
    names = ["TransportError", "ConnectError", "TimeoutException", "ReadError", "WriteError", "NetworkError", "ProtocolError"]
    for name in names:
        obj = getattr(httpx, name, None)
        if obj is None:
            print(name, ": missing")
            continue
        bases = [b.__name__ for b in obj.__mro__[1:4]]
        print(f"{name}: bases={bases}")
except Exception as e:
    print("probe_failed:", type(e).__name__, e)
    sys.exit(0)
PY

Repository: JosephOIbrahim/UnrealEngine_Bridge

Length of output: 2257


🌐 Web query:

HTTPX exception hierarchy TransportError ConnectError TimeoutException ReadError WriteError NetworkError ProtocolError documentation

💡 Result:

The HTTPX exception hierarchy is organized under a root base class called HTTPError [1][2]. Below this, RequestError serves as the primary base for exceptions occurring during the request process [3][1]. The specific exceptions you requested all fall under the TransportError branch, which itself is a subclass of RequestError [1][2]. The hierarchy for these components is structured as follows [1][2]: 1. RequestError (Base for request process errors) - TransportError (Base for transport layer exceptions) - TimeoutException (Base for all timeout-related errors) - ConnectTimeout - ReadTimeout - WriteTimeout - PoolTimeout - NetworkError (Base for network-related errors) - ConnectError - ReadError - WriteError - CloseError - ProtocolError (Base for protocol-related errors) - LocalProtocolError - RemoteProtocolError In summary, TransportError acts as the parent for TimeoutException, NetworkError, and ProtocolError [1][2]. NetworkError specifically branches into ConnectError, ReadError, and WriteError [1][2]. These exceptions are designed to allow developers to catch errors at varying levels of granularity—from catching a specific error (e.g., ConnectError) to catching an entire category (e.g., NetworkError or TransportError) [3].

Citations:


Catch the full transport-error base class in _probe_call. ReadError, WriteError, and ProtocolError still escape here because they all inherit from httpx.TransportError, so the “never raises” contract can still be broken on transport failures. Catching the base class matches the docstring and keeps the preflight ladder from crashing.

🛡️ Proposed fix
-    except (httpx.ConnectError, httpx.TimeoutException) as e:
+    except httpx.TransportError as e:
         return None, "", e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def _probe_call(self, path: str, method: str = "GET", payload: dict | None = None):
"""Low-level RC call for the capability preflight: returns
(status, body, exc) and never raises, so the ladder can inspect the
real status code and response body instead of a swallowed exception."""
try:
if method == "GET":
r = await self._client.get(path)
else:
r = await self._client.put(path, json=payload)
return r.status_code, r.text, None
except (httpx.ConnectError, httpx.TimeoutException) as e:
return None, "", e
async def _probe_call(self, path: str, method: str = "GET", payload: dict | None = None):
"""Low-level RC call for the capability preflight: returns
(status, body, exc) and never raises, so the ladder can inspect the
real status code and response body instead of a swallowed exception."""
try:
if method == "GET":
r = await self._client.get(path)
else:
r = await self._client.put(path, json=payload)
return r.status_code, r.text, None
except httpx.TransportError as e:
return None, "", e
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@remote_control/async_client.py` around lines 97 - 108, The _probe_call helper
still lets some transport failures escape, so its “never raises” behavior is
incomplete. Update the exception handling in _probe_call to catch the full
httpx.TransportError base class instead of only specific subclasses, while
keeping the existing return shape of (status, body, exc). Make sure the change
preserves the preflight ladder behavior in AsyncClient and continues to return
the captured exception for all transport-related failures.

@JosephOIbrahim
JosephOIbrahim merged commit f01c263 into master Jul 8, 2026
3 checks passed
@JosephOIbrahim
JosephOIbrahim deleted the fix/ue58-preflight branch July 8, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant