mirror of
https://github.com/Such-Software/adaptive-multi-protocol-browser.git
synced 2026-10-17 14:31:46 +02:00
Add mobile platform capability planning
This commit is contained in:
@@ -36,6 +36,7 @@ python3 -m ampbrowser plan gemini://wownero.org/
|
||||
python3 -m ampbrowser open https://wownero.org/
|
||||
python3 -m ampbrowser open http://example.b32.i2p/ --yes
|
||||
python3 -m ampbrowser open http://example.b32.i2p/ --config examples/config.toml
|
||||
python3 -m ampbrowser open http://example.b32.i2p/ --platform android
|
||||
python3 -m ampbrowser docs generate
|
||||
python3 -m ampbrowser docs generate --check
|
||||
python3 tools/docs_check.py
|
||||
@@ -46,6 +47,7 @@ python3 -m unittest discover -s tests
|
||||
|
||||
- [Architecture](docs/architecture.md)
|
||||
- [Configuration](docs/configuration.md)
|
||||
- [Mobile browser](docs/mobile-browser.md)
|
||||
- [Open flow](docs/open-flow.md)
|
||||
- [Transport management](docs/transport-management.md)
|
||||
- [Test fixtures](docs/test-fixtures.md)
|
||||
|
||||
+28
-6
@@ -8,6 +8,7 @@ from .config import load_config
|
||||
from .docsgen import generate_docs
|
||||
from .launch import prepare_open
|
||||
from .plan import plan_url
|
||||
from .platforms import PLATFORM_CHOICES
|
||||
from .routing import route_url
|
||||
from .transports import inspect_transports
|
||||
|
||||
@@ -22,10 +23,12 @@ def main(argv: list[str] | None = None) -> int:
|
||||
plan_parser = subcommands.add_parser("plan", help="Plan how a URL would be opened.")
|
||||
plan_parser.add_argument("url")
|
||||
plan_parser.add_argument("--config", type=Path, help="Path to an AMPB config file.")
|
||||
plan_parser.add_argument("--platform", choices=PLATFORM_CHOICES, help="Target platform capability profile.")
|
||||
|
||||
open_parser = subcommands.add_parser("open", help="Prepare a transport-aware open plan.")
|
||||
open_parser.add_argument("url")
|
||||
open_parser.add_argument("--config", type=Path, help="Path to an AMPB config file.")
|
||||
open_parser.add_argument("--platform", choices=PLATFORM_CHOICES, help="Target platform capability profile.")
|
||||
open_parser.add_argument(
|
||||
"--yes",
|
||||
action="store_true",
|
||||
@@ -50,9 +53,15 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if args.command == "route":
|
||||
return _cmd_route(args.url)
|
||||
if args.command == "plan":
|
||||
return _cmd_plan(args.url, config_path=args.config)
|
||||
return _cmd_plan(args.url, config_path=args.config, platform=args.platform)
|
||||
if args.command == "open":
|
||||
return _cmd_open(args.url, consent=args.yes, dry_run=args.dry_run, config_path=args.config)
|
||||
return _cmd_open(
|
||||
args.url,
|
||||
consent=args.yes,
|
||||
dry_run=args.dry_run,
|
||||
config_path=args.config,
|
||||
platform=args.platform,
|
||||
)
|
||||
if args.command == "inspect":
|
||||
return _cmd_inspect()
|
||||
if args.command == "docs":
|
||||
@@ -75,9 +84,9 @@ def _cmd_route(url: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_plan(url: str, *, config_path: Path | None) -> int:
|
||||
def _cmd_plan(url: str, *, config_path: Path | None, platform: str | None) -> int:
|
||||
config = load_config(Path.cwd(), config_path)
|
||||
plan = plan_url(url, config=config)
|
||||
plan = plan_url(url, config=config, platform=platform)
|
||||
endpoint = plan.status.endpoint if plan.status else "-"
|
||||
running = str(plan.status.running).lower() if plan.status else "false"
|
||||
installed = str(plan.status.installed).lower() if plan.status else "false"
|
||||
@@ -90,6 +99,9 @@ def _cmd_plan(url: str, *, config_path: Path | None) -> int:
|
||||
f"installed={installed} "
|
||||
f"running={running} "
|
||||
f"endpoint={endpoint} "
|
||||
f"platform={plan.platform_capability.platform} "
|
||||
f"platform_browse={plan.platform_capability.browse} "
|
||||
f"platform_manage={plan.platform_capability.manage} "
|
||||
f"policy={plan.policy_mode} "
|
||||
f"requires_consent={str(plan.requires_consent).lower()} "
|
||||
f"action=\"{plan.action}\" "
|
||||
@@ -98,9 +110,16 @@ def _cmd_plan(url: str, *, config_path: Path | None) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_open(url: str, *, consent: bool, dry_run: bool, config_path: Path | None) -> int:
|
||||
def _cmd_open(
|
||||
url: str,
|
||||
*,
|
||||
consent: bool,
|
||||
dry_run: bool,
|
||||
config_path: Path | None,
|
||||
platform: str | None,
|
||||
) -> int:
|
||||
config = load_config(Path.cwd(), config_path)
|
||||
open_plan = prepare_open(url, consent=consent, dry_run=dry_run, config=config)
|
||||
open_plan = prepare_open(url, consent=consent, dry_run=dry_run, config=config, platform=platform)
|
||||
browse_plan = open_plan.browse_plan
|
||||
setup_steps = "|".join(open_plan.setup_steps) if open_plan.setup_steps else "-"
|
||||
message = _safe(open_plan.message)
|
||||
@@ -111,6 +130,9 @@ def _cmd_open(url: str, *, consent: bool, dry_run: bool, config_path: Path | Non
|
||||
f"profile={browse_plan.route.profile} "
|
||||
f"status={open_plan.status} "
|
||||
f"dry_run={str(open_plan.dry_run).lower()} "
|
||||
f"platform={browse_plan.platform_capability.platform} "
|
||||
f"platform_browse={browse_plan.platform_capability.browse} "
|
||||
f"platform_manage={browse_plan.platform_capability.manage} "
|
||||
f"policy={browse_plan.policy_mode} "
|
||||
f"requires_consent={str(browse_plan.requires_consent).lower()} "
|
||||
f"consent_granted={str(open_plan.consent_granted).lower()} "
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
|
||||
from .metadata import ROUTE_RULES, TRANSPORT_DEFINITIONS
|
||||
from .platforms import PLATFORM_CAPABILITIES
|
||||
|
||||
|
||||
GENERATED_DIR = Path("docs/generated")
|
||||
@@ -12,6 +13,7 @@ def generate_docs(root: Path, *, check: bool = False) -> list[Path]:
|
||||
docs = {
|
||||
GENERATED_DIR / "route-rules.md": _route_rules_doc(),
|
||||
GENERATED_DIR / "transports.md": _transports_doc(),
|
||||
GENERATED_DIR / "platform-capabilities.md": _platform_capabilities_doc(),
|
||||
}
|
||||
changed: list[Path] = []
|
||||
for rel_path, content in docs.items():
|
||||
@@ -57,3 +59,16 @@ def _transports_doc() -> str:
|
||||
f"`{transport.profile}` | {transport.note} |"
|
||||
)
|
||||
return _header("Generated Transports") + "\n".join(rows) + "\n"
|
||||
|
||||
|
||||
def _platform_capabilities_doc() -> str:
|
||||
rows = [
|
||||
"| Transport | Platform | Browse | Adopt | Manage | Install | Note |",
|
||||
"| --- | --- | --- | --- | --- | --- | --- |",
|
||||
]
|
||||
for capability in PLATFORM_CAPABILITIES:
|
||||
rows.append(
|
||||
f"| `{capability.transport}` | `{capability.platform}` | `{capability.browse}` | "
|
||||
f"`{capability.adopt}` | `{capability.manage}` | `{capability.install}` | {capability.note} |"
|
||||
)
|
||||
return _header("Generated Platform Capabilities") + "\n".join(rows) + "\n"
|
||||
|
||||
+28
-8
@@ -24,9 +24,10 @@ def prepare_open(
|
||||
consent: bool = False,
|
||||
dry_run: bool = True,
|
||||
config: AppConfig | None = None,
|
||||
platform: str | None = None,
|
||||
) -> OpenPlan:
|
||||
config = config or default_config()
|
||||
browse_plan = plan_url(raw_url, config=config)
|
||||
browse_plan = plan_url(raw_url, config=config, platform=platform)
|
||||
profile_path = config.profile_path(browse_plan.route.profile)
|
||||
proxy = _proxy_for(browse_plan)
|
||||
|
||||
@@ -89,12 +90,31 @@ def _setup_steps(browse_plan: BrowsePlan) -> tuple[str, ...]:
|
||||
return ()
|
||||
|
||||
steps: list[str] = []
|
||||
platform = browse_plan.platform_capability.platform
|
||||
if not browse_plan.status.installed:
|
||||
steps.append(f"install {browse_plan.route.transport}")
|
||||
steps.extend(
|
||||
[
|
||||
f"start managed {browse_plan.route.transport}",
|
||||
f"wait for {browse_plan.status.endpoint}",
|
||||
]
|
||||
)
|
||||
if platform == "android":
|
||||
steps.append(f"install or enable Android {browse_plan.route.transport} provider")
|
||||
elif platform == "ios":
|
||||
steps.append(f"enable foreground iOS {browse_plan.route.transport} session")
|
||||
else:
|
||||
steps.append(f"install {browse_plan.route.transport}")
|
||||
if platform == "android":
|
||||
steps.append("start visible Android foreground service")
|
||||
steps.extend(
|
||||
[
|
||||
f"start managed Android {browse_plan.route.transport} transport",
|
||||
f"wait for {browse_plan.status.endpoint}",
|
||||
]
|
||||
)
|
||||
return tuple(steps)
|
||||
elif platform == "ios":
|
||||
steps.append("start foreground-only iOS session")
|
||||
steps.extend(
|
||||
[
|
||||
f"start foreground-only iOS {browse_plan.route.transport} transport",
|
||||
f"wait for {browse_plan.status.endpoint}",
|
||||
]
|
||||
)
|
||||
return tuple(steps)
|
||||
steps.extend([f"start managed {browse_plan.route.transport}", f"wait for {browse_plan.status.endpoint}"])
|
||||
return tuple(steps)
|
||||
|
||||
+61
-13
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .config import AppConfig, default_config
|
||||
from .platforms import PlatformCapability, capability_for
|
||||
from .routing import Route, route_url
|
||||
from .transports import TransportStatus, inspect_transport
|
||||
|
||||
@@ -11,22 +12,39 @@ from .transports import TransportStatus, inspect_transport
|
||||
class BrowsePlan:
|
||||
route: Route
|
||||
status: TransportStatus | None
|
||||
platform_capability: PlatformCapability
|
||||
action: str
|
||||
requires_consent: bool = False
|
||||
prompt: str = ""
|
||||
policy_mode: str = "adopt-or-prompt-manage"
|
||||
|
||||
|
||||
def plan_url(raw_url: str, *, config: AppConfig | None = None) -> BrowsePlan:
|
||||
def plan_url(
|
||||
raw_url: str,
|
||||
*,
|
||||
config: AppConfig | None = None,
|
||||
platform: str | None = None,
|
||||
) -> BrowsePlan:
|
||||
config = config or default_config()
|
||||
route = route_url(raw_url)
|
||||
status = inspect_transport(route.transport)
|
||||
policy_mode = config.transport_mode(route.transport)
|
||||
platform_capability = capability_for(route.transport, platform)
|
||||
|
||||
if platform_capability.browse == "unsupported":
|
||||
return BrowsePlan(
|
||||
route=route,
|
||||
status=status,
|
||||
platform_capability=platform_capability,
|
||||
action=f"blocked by platform: {route.transport} unsupported on {platform_capability.platform}",
|
||||
policy_mode=policy_mode,
|
||||
)
|
||||
|
||||
if policy_mode == "disabled":
|
||||
return BrowsePlan(
|
||||
route=route,
|
||||
status=status,
|
||||
platform_capability=platform_capability,
|
||||
action=f"blocked by policy: {route.transport} disabled",
|
||||
policy_mode=policy_mode,
|
||||
)
|
||||
@@ -34,6 +52,7 @@ def plan_url(raw_url: str, *, config: AppConfig | None = None) -> BrowsePlan:
|
||||
return BrowsePlan(
|
||||
route=route,
|
||||
status=status,
|
||||
platform_capability=platform_capability,
|
||||
action="open with clearnet profile",
|
||||
policy_mode=policy_mode,
|
||||
)
|
||||
@@ -41,6 +60,7 @@ def plan_url(raw_url: str, *, config: AppConfig | None = None) -> BrowsePlan:
|
||||
return BrowsePlan(
|
||||
route=route,
|
||||
status=status,
|
||||
platform_capability=platform_capability,
|
||||
action="adopt existing transport",
|
||||
policy_mode=policy_mode,
|
||||
)
|
||||
@@ -48,24 +68,51 @@ def plan_url(raw_url: str, *, config: AppConfig | None = None) -> BrowsePlan:
|
||||
return BrowsePlan(
|
||||
route=route,
|
||||
status=status,
|
||||
platform_capability=platform_capability,
|
||||
action=f"blocked by policy: {route.transport} mode is adopt",
|
||||
policy_mode=policy_mode,
|
||||
)
|
||||
if status and status.manage_supported:
|
||||
action = "prompt to start managed transport"
|
||||
prompt = (
|
||||
f"{route.transport} is not running. AMPB can start a managed {route.transport} "
|
||||
f"transport for {route.normalized}."
|
||||
)
|
||||
if not status.installed:
|
||||
action = "prompt to install and start managed transport"
|
||||
prompt = (
|
||||
f"{route.transport} is not installed or running. AMPB can install and start "
|
||||
f"a managed {route.transport} transport for {route.normalized}."
|
||||
)
|
||||
if status and status.manage_supported and not platform_capability.can_manage_setup:
|
||||
return BrowsePlan(
|
||||
route=route,
|
||||
status=status,
|
||||
platform_capability=platform_capability,
|
||||
action=(
|
||||
f"blocked by platform: {route.transport} managed setup is "
|
||||
f"{platform_capability.manage} on {platform_capability.platform}"
|
||||
),
|
||||
policy_mode=policy_mode,
|
||||
)
|
||||
if status and status.manage_supported:
|
||||
if platform_capability.manage == "foreground-only":
|
||||
action = "prompt to start foreground-only transport session"
|
||||
prompt = (
|
||||
f"{route.transport} is not running. AMPB can start a foreground-only "
|
||||
f"{route.transport} session for {route.normalized} on {platform_capability.platform}."
|
||||
)
|
||||
else:
|
||||
action = "prompt to start managed transport"
|
||||
prompt = (
|
||||
f"{route.transport} is not running. AMPB can start a managed {route.transport} "
|
||||
f"transport for {route.normalized}."
|
||||
)
|
||||
if not status.installed:
|
||||
if platform_capability.manage == "foreground-only":
|
||||
action = "prompt to enable foreground-only transport session"
|
||||
prompt = (
|
||||
f"{route.transport} is not available. AMPB can enable a foreground-only "
|
||||
f"{route.transport} session for {route.normalized} on {platform_capability.platform}."
|
||||
)
|
||||
else:
|
||||
action = "prompt to install and start managed transport"
|
||||
prompt = (
|
||||
f"{route.transport} is not installed or running. AMPB can install and start "
|
||||
f"a managed {route.transport} transport for {route.normalized}."
|
||||
)
|
||||
return BrowsePlan(
|
||||
route=route,
|
||||
status=status,
|
||||
platform_capability=platform_capability,
|
||||
action=action,
|
||||
requires_consent=True,
|
||||
prompt=prompt,
|
||||
@@ -74,6 +121,7 @@ def plan_url(raw_url: str, *, config: AppConfig | None = None) -> BrowsePlan:
|
||||
return BrowsePlan(
|
||||
route=route,
|
||||
status=status,
|
||||
platform_capability=platform_capability,
|
||||
action="blocked until adapter is configured",
|
||||
policy_mode=policy_mode,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import sys
|
||||
|
||||
|
||||
PLATFORM_CHOICES = ("desktop", "android", "ios")
|
||||
MANAGED_SETUP_VALUES = {"ready", "planned", "foreground-only"}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlatformCapability:
|
||||
transport: str
|
||||
platform: str
|
||||
browse: str
|
||||
adopt: str
|
||||
manage: str
|
||||
install: str
|
||||
note: str
|
||||
|
||||
@property
|
||||
def can_manage_setup(self) -> bool:
|
||||
return self.manage in MANAGED_SETUP_VALUES
|
||||
|
||||
|
||||
PLATFORM_CAPABILITIES = (
|
||||
PlatformCapability("clearnet", "desktop", "ready", "ready", "unsupported", "unsupported", "Use system browser or isolated desktop profile."),
|
||||
PlatformCapability("clearnet", "android", "ready", "ready", "unsupported", "unsupported", "Use Android browser shell, WebView, or Custom Tabs."),
|
||||
PlatformCapability("clearnet", "ios", "ready", "ready", "unsupported", "unsupported", "Use iOS browser shell with platform web constraints."),
|
||||
PlatformCapability("tor", "desktop", "ready", "ready", "planned", "planned", "Adopt local Tor or prompt before managed desktop daemon setup."),
|
||||
PlatformCapability("tor", "android", "planned", "planned", "planned", "planned", "Use an app-owned foreground service or compatible installed Tor provider."),
|
||||
PlatformCapability("tor", "ios", "foreground-only", "planned", "foreground-only", "constrained", "Plan for in-app foreground sessions; do not assume always-on background service."),
|
||||
PlatformCapability("i2p", "desktop", "ready", "ready", "planned", "planned", "Adopt local I2P proxy or prompt before managed desktop daemon setup."),
|
||||
PlatformCapability("i2p", "android", "planned", "planned", "planned", "planned", "Use an app-owned foreground service or compatible installed I2P provider."),
|
||||
PlatformCapability("i2p", "ios", "constrained", "planned", "foreground-only", "constrained", "Treat iOS I2P support as foreground-only until native adapter constraints are proven."),
|
||||
PlatformCapability("gemini", "desktop", "ready", "ready", "ready", "unsupported", "Built-in fetch/render path can be shared by desktop shell."),
|
||||
PlatformCapability("gemini", "android", "ready", "ready", "ready", "unsupported", "Built-in fetch/render path can be shared by Android shell."),
|
||||
PlatformCapability("gemini", "ios", "ready", "ready", "ready", "unsupported", "Built-in fetch/render path can be shared by iOS shell."),
|
||||
PlatformCapability("reticulum", "desktop", "planned", "planned", "planned", "planned", "Adapter and local interface readiness checks are planned."),
|
||||
PlatformCapability("reticulum", "android", "planned", "planned", "planned", "planned", "Mobile adapter should use explicit foreground service consent."),
|
||||
PlatformCapability("reticulum", "ios", "constrained", "planned", "foreground-only", "constrained", "Do not assume background Reticulum operation on iOS."),
|
||||
PlatformCapability("unknown", "desktop", "unsupported", "unsupported", "unsupported", "unsupported", "No platform route is available."),
|
||||
PlatformCapability("unknown", "android", "unsupported", "unsupported", "unsupported", "unsupported", "No platform route is available."),
|
||||
PlatformCapability("unknown", "ios", "unsupported", "unsupported", "unsupported", "unsupported", "No platform route is available."),
|
||||
)
|
||||
|
||||
|
||||
def detect_platform() -> str:
|
||||
if hasattr(sys, "getandroidapilevel"):
|
||||
return "android"
|
||||
if sys.platform in {"ios", "watchos", "tvos"}:
|
||||
return "ios"
|
||||
return "desktop"
|
||||
|
||||
|
||||
def normalize_platform(platform: str | None) -> str:
|
||||
selected = platform or detect_platform()
|
||||
if selected not in PLATFORM_CHOICES:
|
||||
choices = ", ".join(PLATFORM_CHOICES)
|
||||
raise ValueError(f"platform must be one of: {choices}")
|
||||
return selected
|
||||
|
||||
|
||||
def capability_for(transport: str, platform: str | None = None) -> PlatformCapability:
|
||||
selected = normalize_platform(platform)
|
||||
for capability in PLATFORM_CAPABILITIES:
|
||||
if capability.transport == transport and capability.platform == selected:
|
||||
return capability
|
||||
for capability in PLATFORM_CAPABILITIES:
|
||||
if capability.transport == "unknown" and capability.platform == selected:
|
||||
return capability
|
||||
raise ValueError(f"no capability for transport={transport} platform={selected}")
|
||||
@@ -22,6 +22,7 @@ browser engine until those contracts are stable.
|
||||
- `ampbrowser.transports`: local readiness checks for Tor, I2P, Reticulum, and Gemini.
|
||||
- `ampbrowser.plan`: dry-run action planner for browser launches.
|
||||
- `ampbrowser.launch`: side-effect-free launch specs and first-use consent state.
|
||||
- `ampbrowser.platforms`: platform capability matrix for desktop, Android, and iOS.
|
||||
- `ampbrowser.docsgen`: generated public documentation from code-owned metadata.
|
||||
|
||||
## Browser Strategy
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated Platform Capabilities
|
||||
|
||||
> Status: generated | Updated by `python3 -m ampbrowser docs generate` | Applies to: AMPB
|
||||
|
||||
This file is generated from code. Do not edit it by hand.
|
||||
|
||||
| Transport | Platform | Browse | Adopt | Manage | Install | Note |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| `clearnet` | `desktop` | `ready` | `ready` | `unsupported` | `unsupported` | Use system browser or isolated desktop profile. |
|
||||
| `clearnet` | `android` | `ready` | `ready` | `unsupported` | `unsupported` | Use Android browser shell, WebView, or Custom Tabs. |
|
||||
| `clearnet` | `ios` | `ready` | `ready` | `unsupported` | `unsupported` | Use iOS browser shell with platform web constraints. |
|
||||
| `tor` | `desktop` | `ready` | `ready` | `planned` | `planned` | Adopt local Tor or prompt before managed desktop daemon setup. |
|
||||
| `tor` | `android` | `planned` | `planned` | `planned` | `planned` | Use an app-owned foreground service or compatible installed Tor provider. |
|
||||
| `tor` | `ios` | `foreground-only` | `planned` | `foreground-only` | `constrained` | Plan for in-app foreground sessions; do not assume always-on background service. |
|
||||
| `i2p` | `desktop` | `ready` | `ready` | `planned` | `planned` | Adopt local I2P proxy or prompt before managed desktop daemon setup. |
|
||||
| `i2p` | `android` | `planned` | `planned` | `planned` | `planned` | Use an app-owned foreground service or compatible installed I2P provider. |
|
||||
| `i2p` | `ios` | `constrained` | `planned` | `foreground-only` | `constrained` | Treat iOS I2P support as foreground-only until native adapter constraints are proven. |
|
||||
| `gemini` | `desktop` | `ready` | `ready` | `ready` | `unsupported` | Built-in fetch/render path can be shared by desktop shell. |
|
||||
| `gemini` | `android` | `ready` | `ready` | `ready` | `unsupported` | Built-in fetch/render path can be shared by Android shell. |
|
||||
| `gemini` | `ios` | `ready` | `ready` | `ready` | `unsupported` | Built-in fetch/render path can be shared by iOS shell. |
|
||||
| `reticulum` | `desktop` | `planned` | `planned` | `planned` | `planned` | Adapter and local interface readiness checks are planned. |
|
||||
| `reticulum` | `android` | `planned` | `planned` | `planned` | `planned` | Mobile adapter should use explicit foreground service consent. |
|
||||
| `reticulum` | `ios` | `constrained` | `planned` | `foreground-only` | `constrained` | Do not assume background Reticulum operation on iOS. |
|
||||
| `unknown` | `desktop` | `unsupported` | `unsupported` | `unsupported` | `unsupported` | No platform route is available. |
|
||||
| `unknown` | `android` | `unsupported` | `unsupported` | `unsupported` | `unsupported` | No platform route is available. |
|
||||
| `unknown` | `ios` | `unsupported` | `unsupported` | `unsupported` | `unsupported` | No platform route is available. |
|
||||
@@ -0,0 +1,25 @@
|
||||
# Mobile Browser
|
||||
|
||||
> Status: draft | Updated 2026-07-06 | Applies to: AMPB mobile shells
|
||||
|
||||
Mobile is a first-class AMPB target. The core router, policy model, consent flow, profile
|
||||
isolation, and fixture checks should stay shared across desktop, Android, and iOS. Native
|
||||
mobile shells should implement only the platform-specific browser and transport pieces.
|
||||
|
||||
## Android
|
||||
|
||||
Android is the primary mobile target for full AMPB browsing. Tor, I2P, and Reticulum
|
||||
adapters should run only after a URL selects that transport and the user approves setup.
|
||||
Managed adapters must use visible, stoppable foreground services and app-owned state.
|
||||
|
||||
## iOS
|
||||
|
||||
iOS is a supported design target, but transport management is constrained. AMPB should
|
||||
assume foreground-only sessions unless a reviewed platform capability proves otherwise.
|
||||
The iOS shell should still share routing, policy, consent, fixture, Gemini, and clearnet
|
||||
behavior with the rest of AMPB.
|
||||
|
||||
## Capability Matrix
|
||||
|
||||
Generated platform capability metadata lives in
|
||||
[generated platform capabilities](generated/platform-capabilities.md).
|
||||
@@ -27,4 +27,5 @@ python3 -m ampbrowser open https://wownero.org/
|
||||
python3 -m ampbrowser open http://example.b32.i2p/
|
||||
python3 -m ampbrowser open http://example.b32.i2p/ --yes
|
||||
python3 -m ampbrowser open http://example.b32.i2p/ --config examples/config.toml
|
||||
python3 -m ampbrowser open http://example.b32.i2p/ --platform android
|
||||
```
|
||||
|
||||
@@ -14,7 +14,11 @@ class DocsGenTest(unittest.TestCase):
|
||||
changed = generate_docs(root)
|
||||
|
||||
self.assertEqual(
|
||||
{Path("docs/generated/route-rules.md"), Path("docs/generated/transports.md")},
|
||||
{
|
||||
Path("docs/generated/platform-capabilities.md"),
|
||||
Path("docs/generated/route-rules.md"),
|
||||
Path("docs/generated/transports.md"),
|
||||
},
|
||||
set(changed),
|
||||
)
|
||||
self.assertEqual([], generate_docs(root, check=True))
|
||||
|
||||
@@ -62,6 +62,31 @@ class PrepareOpenTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(".local/ampb/profiles/clearnet", plan.profile_path)
|
||||
|
||||
def test_android_setup_steps_include_foreground_service(self) -> None:
|
||||
status = TransportStatus(
|
||||
transport="i2p",
|
||||
installed=False,
|
||||
running=False,
|
||||
endpoint="http://127.0.0.1:4444",
|
||||
adoptable=False,
|
||||
manage_supported=True,
|
||||
note="I2P HTTP proxy",
|
||||
)
|
||||
|
||||
with patch("ampbrowser.plan.inspect_transport", return_value=status):
|
||||
plan = prepare_open("http://example.b32.i2p/", platform="android")
|
||||
|
||||
self.assertEqual("android", plan.browse_plan.platform_capability.platform)
|
||||
self.assertEqual(
|
||||
(
|
||||
"install or enable Android i2p provider",
|
||||
"start visible Android foreground service",
|
||||
"start managed Android i2p transport",
|
||||
"wait for http://127.0.0.1:4444",
|
||||
),
|
||||
plan.setup_steps,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -89,6 +89,41 @@ class PlanUrlTest(unittest.TestCase):
|
||||
self.assertEqual("blocked by policy: tor mode is adopt", plan.action)
|
||||
self.assertFalse(plan.requires_consent)
|
||||
|
||||
def test_android_platform_is_reported_on_plan(self) -> None:
|
||||
status = TransportStatus(
|
||||
transport="tor",
|
||||
installed=False,
|
||||
running=False,
|
||||
endpoint="socks5://127.0.0.1:9050",
|
||||
adoptable=False,
|
||||
manage_supported=True,
|
||||
note="Tor SOCKS proxy",
|
||||
)
|
||||
|
||||
with patch("ampbrowser.plan.inspect_transport", return_value=status):
|
||||
plan = plan_url("http://example.onion/", platform="android")
|
||||
|
||||
self.assertEqual("android", plan.platform_capability.platform)
|
||||
self.assertEqual("planned", plan.platform_capability.manage)
|
||||
self.assertTrue(plan.requires_consent)
|
||||
|
||||
def test_ios_foreground_only_transport_prompt(self) -> None:
|
||||
status = TransportStatus(
|
||||
transport="tor",
|
||||
installed=False,
|
||||
running=False,
|
||||
endpoint="socks5://127.0.0.1:9050",
|
||||
adoptable=False,
|
||||
manage_supported=True,
|
||||
note="Tor SOCKS proxy",
|
||||
)
|
||||
|
||||
with patch("ampbrowser.plan.inspect_transport", return_value=status):
|
||||
plan = plan_url("http://example.onion/", platform="ios")
|
||||
|
||||
self.assertEqual("prompt to enable foreground-only transport session", plan.action)
|
||||
self.assertIn("foreground-only", plan.prompt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from ampbrowser.platforms import capability_for, normalize_platform
|
||||
|
||||
|
||||
class PlatformsTest(unittest.TestCase):
|
||||
def test_android_tor_is_managed_setup_capable(self) -> None:
|
||||
capability = capability_for("tor", "android")
|
||||
|
||||
self.assertEqual("android", capability.platform)
|
||||
self.assertEqual("planned", capability.manage)
|
||||
self.assertTrue(capability.can_manage_setup)
|
||||
|
||||
def test_ios_tor_is_foreground_only(self) -> None:
|
||||
capability = capability_for("tor", "ios")
|
||||
|
||||
self.assertEqual("foreground-only", capability.manage)
|
||||
self.assertTrue(capability.can_manage_setup)
|
||||
self.assertIn("foreground", capability.note)
|
||||
|
||||
def test_unknown_platform_is_rejected(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
normalize_platform("watch")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user