testsAndMisc/python_pkg/phone_guard/_remote.py
Krzysztof kuhy Rudnicki aa37852296 phone_guard: lease or block raw adb; android_ui drives a session's own virtual display
A raw `adb shell input tap` took no lease, so one session's tap landed in
another session's app (2026-09-25). python_pkg.phone_guard classifies every
adb call in a Bash command and, as the Claude Code PreToolUse hook, takes
the matching phone_lease (one app / real screen / whole phone) or exits 2:
key/text input without -d, input on another session's virtual display, or
a lease still held elsewhere after 40 s.

android_ui gains --display self|<id>: it drives only this session's
phone_vd display under that app's lease, and every input carries -d.
uiautomator sees display 0 only, so virtual-display trees come from a tiny
instrumentation APK (A11yDump.java) built from source without Gradle.

Prunes the oldest evidence artifact to stay under the 60-file cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpA6wW2DKiovQADTdjWJoH
2026-09-25 16:09:45 +02:00

120 lines
3.6 KiB
Python

"""Per-tool rules for ``adb shell`` commands, for :mod:`classify`.
Split out of ``classify.py`` for the 250-line cap. Each handler takes the
remote command's arguments (after the tool name) and returns the
:class:`~python_pkg.phone_guard.classify.Need` without a serial; the caller
stamps the serial on.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from python_pkg.phone_guard._need import SCREEN, WHOLE_PHONE, Need
if TYPE_CHECKING:
from collections.abc import Callable
_PM_PACKAGE = {
"clear",
"uninstall",
"disable",
"disable-user",
"enable",
"hide",
"unhide",
"suspend",
"unsuspend",
"grant",
"revoke",
}
_IDLE_WRITE = {"force-idle", "step", "unforce", "enable", "disable", "force-inactive"}
# `cmd connectivity set-package-networking-enabled <bool> <package>`
_NETWORKING_PKG_INDEX = 3
def _display_flag(args: list[str], flag: str) -> int | None:
if flag in args:
i = args.index(flag)
if i + 1 < len(args) and args[i + 1].isdigit():
return int(args[i + 1])
return None
# `pm` options that take a value, so the value is not mistaken for the package.
_PM_VALUED = {"--user", "-u"}
def _package_after(args: list[str], verb: str) -> str | None:
rest = args[args.index(verb) + 1 :]
i = 0
while i < len(rest):
if rest[i] in _PM_VALUED:
i += 2
elif rest[i].startswith("-"):
i += 1
else:
return rest[i]
return None
def _am(rest: list[str]) -> Need:
first = rest[0] if rest else ""
if first in {"force-stop", "kill", "stop-app"} and len(rest) > 1:
return Need(scope=f"pkg:{rest[-1]}")
if first in {"start", "start-activity", "startservice", "start-foreground-service"}:
display = _display_flag(rest, "--display")
return Need(display=display) if display else Need(scope=SCREEN)
return Need()
def _pm(rest: list[str]) -> Need:
if not rest or rest[0] not in _PM_PACKAGE:
return Need()
package = _package_after(rest, rest[0])
return Need(scope=f"pkg:{package}" if package else SCREEN)
def _cmd(rest: list[str]) -> Need:
if rest[:2] != ["connectivity", "set-package-networking-enabled"]:
is_set = (
rest[:1] == ["connectivity"]
and rest[1:2] != []
and rest[1].startswith(("set-", "airplane-mode"))
)
return Need(scope=WHOLE_PHONE) if is_set else Need()
package = rest[3] if len(rest) > _NETWORKING_PKG_INDEX else None
return Need(scope=f"pkg:{package}") if package else Need(scope=WHOLE_PHONE)
def _dumpsys(rest: list[str]) -> Need:
writes = (rest[:1] == ["deviceidle"] and rest[1:2] and rest[1] in _IDLE_WRITE) or (
rest[:1] == ["battery"] and rest[1:2] and rest[1] in {"set", "unplug", "reset"}
)
return Need(scope=WHOLE_PHONE) if writes else Need()
def _settings(rest: list[str]) -> Need:
return Need(scope=WHOLE_PHONE) if rest[:1] == ["put"] else Need()
def _wm(rest: list[str]) -> Need:
changes = rest[:1] in (["size"], ["density"]) and len(rest) > 1
return Need(scope=SCREEN) if changes else Need()
REMOTE: dict[str, Callable[[list[str]], Need]] = {
"am": _am,
"pm": _pm,
"cmd": _cmd,
"dumpsys": _dumpsys,
"settings": _settings,
"wm": _wm,
"monkey": lambda _rest: Need(scope=SCREEN),
"svc": lambda _rest: Need(scope=WHOLE_PHONE),
"reboot": lambda _rest: Need(scope=WHOLE_PHONE),
"setprop": lambda _rest: Need(scope=WHOLE_PHONE),
"stop": lambda _rest: Need(scope=WHOLE_PHONE),
"start": lambda _rest: Need(scope=WHOLE_PHONE),
}