fix(navigation): harden obstacle guard and structural sanity checks against phantom keyboards and reply-box traps
This commit is contained in:
@@ -47,21 +47,27 @@ class ObstacleGuardPlugin(BehaviorPlugin):
|
||||
logger.warning("⚠️ [ObstacleGuard] System permission dialog detected. Dismissing with BACK...")
|
||||
ctx.device.press("back")
|
||||
sleep(1.5 * ctx.sleep_mod)
|
||||
return BehaviorResult(executed=True, should_skip=True)
|
||||
res = BehaviorResult(executed=True, should_skip=True)
|
||||
res.skip_type = "no_scroll"
|
||||
return res
|
||||
|
||||
# ── Foreign App Takeover (e.g. browser opened, wrong app in foreground) ──
|
||||
if situation == SituationType.OBSTACLE_FOREIGN_APP:
|
||||
logger.warning("⚠️ [ObstacleGuard] Foreign app detected. Pressing BACK to recover...")
|
||||
ctx.device.press("back")
|
||||
sleep(1.5 * ctx.sleep_mod)
|
||||
return BehaviorResult(executed=True, should_skip=True)
|
||||
res = BehaviorResult(executed=True, should_skip=True)
|
||||
res.skip_type = "no_scroll"
|
||||
return res
|
||||
|
||||
# ── On-Screen Keyboard (e.g. hallucinated click on comment field) ──
|
||||
if situation == SituationType.OBSTACLE_KEYBOARD:
|
||||
logger.warning("⚠️ [ObstacleGuard] On-screen Keyboard is open. Pressing BACK to dismiss...")
|
||||
ctx.device.press("back")
|
||||
sleep(1.0 * ctx.sleep_mod)
|
||||
return BehaviorResult(executed=True, should_skip=True)
|
||||
res = BehaviorResult(executed=True, should_skip=True)
|
||||
res.skip_type = "no_scroll"
|
||||
return res
|
||||
|
||||
# ── Instagram Modal / Overlay (survey, "Not Now" prompt, creation flow) ──
|
||||
if situation == SituationType.OBSTACLE_MODAL:
|
||||
@@ -88,6 +94,8 @@ class ObstacleGuardPlugin(BehaviorPlugin):
|
||||
else:
|
||||
ctx.shared_state["consecutive_marker_misses"] = misses + 1
|
||||
|
||||
return BehaviorResult(executed=True, should_skip=True) # Restart loop for same post or next
|
||||
res = BehaviorResult(executed=True, should_skip=True) # Restart loop for same post or next
|
||||
res.skip_type = "no_scroll"
|
||||
return res
|
||||
|
||||
return BehaviorResult(executed=False)
|
||||
|
||||
@@ -1134,6 +1134,9 @@ def _run_zero_latency_feed_loop(
|
||||
elif skip_type == "fast":
|
||||
_humanized_scroll(device, is_skip=True)
|
||||
sleep(1.0 * sleep_mod)
|
||||
elif skip_type == "no_scroll":
|
||||
logger.debug("⏭️ Skipping post without scrolling (e.g., dismissing obstacle).")
|
||||
sleep(1.0 * sleep_mod)
|
||||
else:
|
||||
_humanized_scroll(device, is_skip=False)
|
||||
sleep(1.5 * sleep_mod)
|
||||
|
||||
@@ -347,8 +347,21 @@ class SituationalAwarenessEngine:
|
||||
"com.apple.android.inputmethod",
|
||||
}
|
||||
if any(pkg in keyboard_pkgs for pkg in packages):
|
||||
logger.info("📱 [SAE Perceive] On-screen Keyboard explicitly detected. Treating as obstacle.")
|
||||
return SituationType.OBSTACLE_KEYBOARD
|
||||
# Differentiate between actual EditText focus and layout-induced phantom keyboard signals
|
||||
is_actual_keyboard = False
|
||||
if 'focused="true"' in xml_dump and "EditText" in xml_dump:
|
||||
if re.search(r'class="[^"]*EditText"[^>]*focused="true"', xml_dump) or re.search(
|
||||
r'focused="true"[^>]*class="[^"]*EditText"', xml_dump
|
||||
):
|
||||
is_actual_keyboard = True
|
||||
|
||||
if is_actual_keyboard:
|
||||
logger.info(
|
||||
"📱 [SAE Perceive] On-screen Keyboard explicitly detected (EditText focused). Treating as obstacle."
|
||||
)
|
||||
return SituationType.OBSTACLE_KEYBOARD
|
||||
else:
|
||||
logger.debug("📱 [SAE Perceive] Phantom keyboard signal detected (No focused EditText). Ignoring.")
|
||||
|
||||
# ── Foreign Environment Detection (package-based) ──
|
||||
# If the main app package is completely absent from the UI hierarchy,
|
||||
|
||||
@@ -85,6 +85,18 @@ class TelepathicEngine:
|
||||
filtered_candidates.append(c)
|
||||
candidates = filtered_candidates
|
||||
|
||||
# 2.5 Structural Sanity Check (Filter out traps before VLM sees them)
|
||||
safe_candidates = []
|
||||
for c in candidates:
|
||||
if self._structural_sanity_check(c, intent_description):
|
||||
safe_candidates.append(c)
|
||||
|
||||
if not safe_candidates:
|
||||
logger.warning(f"All candidates failed structural sanity check for intent: '{intent_description}'")
|
||||
return None
|
||||
|
||||
candidates = safe_candidates
|
||||
|
||||
# 3. Resolve intent against candidates
|
||||
best_node = self._resolver.resolve(intent_description, candidates, device=device)
|
||||
|
||||
@@ -224,13 +236,18 @@ class TelepathicEngine:
|
||||
def _is_instagram_context(self, xml_string: str) -> bool:
|
||||
return "com.instagram.android" in xml_string
|
||||
|
||||
def _structural_sanity_check(self, node: dict, intent_description: str, screen_height: int = 2400) -> bool:
|
||||
def _structural_sanity_check(self, node: SpatialNode, intent_description: str, screen_height: int = 2400) -> bool:
|
||||
"""
|
||||
Structural guard to ensure nodes are in valid locations for their intents.
|
||||
"""
|
||||
intent = intent_description.lower()
|
||||
y = node.get("y", 0)
|
||||
semantic = (node.get("semantic_string", "") or "").lower()
|
||||
y = node.center_y
|
||||
semantic = ((node.text or "") + " " + (node.content_desc or "") + " " + (node.resource_id or "")).lower()
|
||||
|
||||
# 0. EXTREME GUARD: NEVER pick an input field (EditText) or reply box unless explicitly asked to type/reply.
|
||||
if "edittext" in (node.class_name or "").lower() or "reply" in semantic:
|
||||
if "type" not in intent and "reply" not in intent and "message" not in intent and "search" not in intent:
|
||||
return False
|
||||
|
||||
# 1. Post Username Guard
|
||||
if "post username" in intent or "author username" in intent:
|
||||
@@ -277,7 +294,7 @@ class TelepathicEngine:
|
||||
|
||||
# 6. Block massive layout containers UNLESS specifically looking for feed/post
|
||||
MAX_CONTAINER_AREA = 500000
|
||||
area = node.get("area", 0)
|
||||
area = node.area
|
||||
|
||||
is_feed_or_post = "feed" in intent or "post" in intent
|
||||
is_grid_item = "grid" in intent or "list" in intent
|
||||
|
||||
@@ -11,15 +11,15 @@ def test_media_intent_rejects_grid_containers():
|
||||
engine = TelepathicEngine()
|
||||
screen_height = 2400
|
||||
|
||||
from GramAddict.core.perception.spatial_parser import SpatialNode
|
||||
|
||||
# Mock node representing a massive RecyclerView containing the entire grid
|
||||
# Area is 1080 * 2400 = 2592000 > MAX_CONTAINER_AREA (500000)
|
||||
massive_grid_container = {
|
||||
"semantic_string": "id context: 'swipeable nav view pager inner recycler view'",
|
||||
"area": 2592000,
|
||||
"y": 1200,
|
||||
"class_name": "androidx.recyclerview.widget.RecyclerView",
|
||||
"resource_id": "com.instagram.android:id/swipeable_nav_view_pager_inner_recycler_view",
|
||||
}
|
||||
massive_grid_container = SpatialNode(
|
||||
bounds=(0, 0, 1080, 2400),
|
||||
class_name="androidx.recyclerview.widget.RecyclerView",
|
||||
resource_id="com.instagram.android:id/swipeable_nav_view_pager_inner_recycler_view",
|
||||
)
|
||||
|
||||
# Intent
|
||||
intent = "first image post in profile grid"
|
||||
|
||||
Reference in New Issue
Block a user