mirror of
https://github.com/Such-Software/adaptive-multi-protocol-browser.git
synced 2026-10-17 06:21:26 +02:00
Model browser transport provider sources
This commit is contained in:
@@ -218,6 +218,7 @@ def _print_open_plan(prefix: str, open_plan, extra_fields: tuple[str, ...] = ())
|
||||
f"setup_prompt_approval_command=\"{_safe(open_plan.setup_prompt_approval_command)}\" "
|
||||
f"transport_setup_status={open_plan.transport_setup_status} "
|
||||
f"transport_setup_provider={open_plan.transport_setup_provider} "
|
||||
f"transport_setup_provider_source={open_plan.transport_setup_provider_source} "
|
||||
f"transport_setup_owned={str(open_plan.transport_setup_owned).lower()} "
|
||||
f"transport_setup_pid={open_plan.transport_setup_pid} "
|
||||
f"transport_setup_endpoint={open_plan.transport_setup_endpoint} "
|
||||
@@ -259,6 +260,7 @@ def _cmd_transport(args) -> int:
|
||||
"AMPBROWSER_TRANSPORT "
|
||||
f"transport={result.transport} "
|
||||
f"provider={result.provider} "
|
||||
f"provider_source={result.provider_source} "
|
||||
f"status={result.status} "
|
||||
f"ready={str(result.ready).lower()} "
|
||||
f"owned={str(result.owned).lower()} "
|
||||
|
||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
||||
from .adapters import ADAPTERS
|
||||
from .candidates import CANDIDATE_TRANSPORTS
|
||||
from .metadata import BROWSER_BACKENDS, ROUTE_RULES, TRANSPORT_DEFINITIONS
|
||||
from .metadata import PROVIDER_SOURCE_DEFINITIONS, TRANSPORT_PROVIDER_DEFINITIONS
|
||||
from .platforms import PLATFORM_CAPABILITIES
|
||||
|
||||
|
||||
@@ -19,6 +20,7 @@ def generate_docs(root: Path, *, check: bool = False) -> list[Path]:
|
||||
GENERATED_DIR / "candidate-transports.md": _candidate_transports_doc(),
|
||||
GENERATED_DIR / "platform-capabilities.md": _platform_capabilities_doc(),
|
||||
GENERATED_DIR / "browser-strategy.md": _browser_strategy_doc(),
|
||||
GENERATED_DIR / "provider-sources.md": _provider_sources_doc(),
|
||||
}
|
||||
changed: list[Path] = []
|
||||
for rel_path, content in docs.items():
|
||||
@@ -118,3 +120,28 @@ def _browser_strategy_doc() -> str:
|
||||
f"{backend.launch_mode} | {backend.privacy_posture} | {backend.note} |"
|
||||
)
|
||||
return _header("Generated Browser Strategy") + "\n".join(rows) + "\n"
|
||||
|
||||
|
||||
def _provider_sources_doc() -> str:
|
||||
sections = [_header("Generated Provider Sources")]
|
||||
sections.append("## Source Types\n")
|
||||
sections.append("| Source | Discovery | Lifecycle | Platforms | Note |")
|
||||
sections.append("| --- | --- | --- | --- | --- |")
|
||||
for source in PROVIDER_SOURCE_DEFINITIONS:
|
||||
sections.append(
|
||||
f"| `{source.source}` | {source.discovery} | {source.lifecycle} | "
|
||||
f"{source.platforms} | {source.note} |"
|
||||
)
|
||||
sections.append("")
|
||||
|
||||
sections.append("## Transport Providers\n")
|
||||
sections.append("| Transport | Provider | Sources | Endpoint | Status | Note |")
|
||||
sections.append("| --- | --- | --- | --- | --- | --- |")
|
||||
for provider in TRANSPORT_PROVIDER_DEFINITIONS:
|
||||
sources = ", ".join(f"`{source}`" for source in provider.sources)
|
||||
sections.append(
|
||||
f"| `{provider.transport}` | `{provider.provider}` | {sources} | "
|
||||
f"`{provider.endpoint}` | `{provider.status}` | {provider.note} |"
|
||||
)
|
||||
sections.append("")
|
||||
return "\n".join(sections)
|
||||
|
||||
@@ -41,6 +41,7 @@ class OpenPlan:
|
||||
setup_prompt_approval_command: str = "-"
|
||||
transport_setup_status: str = "-"
|
||||
transport_setup_provider: str = "-"
|
||||
transport_setup_provider_source: str = "-"
|
||||
transport_setup_owned: bool = False
|
||||
transport_setup_pid: int = 0
|
||||
transport_setup_endpoint: str = "-"
|
||||
|
||||
@@ -31,6 +31,25 @@ class BrowserBackend:
|
||||
note: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderSourceDefinition:
|
||||
source: str
|
||||
discovery: str
|
||||
lifecycle: str
|
||||
platforms: str
|
||||
note: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TransportProviderDefinition:
|
||||
transport: str
|
||||
provider: str
|
||||
sources: tuple[str, ...]
|
||||
endpoint: str
|
||||
status: str
|
||||
note: str
|
||||
|
||||
|
||||
ROUTE_RULES = (
|
||||
RouteRule("*.onion", "tor", "tor", "route through Tor SOCKS"),
|
||||
RouteRule("*.i2p", "i2p", "i2p", "route through I2P HTTP proxy"),
|
||||
@@ -125,3 +144,91 @@ BROWSER_BACKENDS = (
|
||||
"Used for Gemini and similar lightweight document transports.",
|
||||
),
|
||||
)
|
||||
|
||||
PROVIDER_SOURCE_DEFINITIONS = (
|
||||
ProviderSourceDefinition(
|
||||
"configured",
|
||||
"explicit config or environment variable",
|
||||
"operator override; AMPB owns profile policy but not the binary provenance",
|
||||
"desktop, android, ios",
|
||||
"Used for development, custom bundles, and reviewed advanced installs.",
|
||||
),
|
||||
ProviderSourceDefinition(
|
||||
"bundled-sidecar",
|
||||
"provider binary shipped with AMPB or the build workspace",
|
||||
"AMPB-owned process and isolated state",
|
||||
"desktop, android",
|
||||
"Default desktop target for Tor, I2P, IPFS, Reticulum, and similar daemon-style transports.",
|
||||
),
|
||||
ProviderSourceDefinition(
|
||||
"embedded-library",
|
||||
"transport library linked into the app process",
|
||||
"AMPB-owned foreground session and app-private state",
|
||||
"android, ios",
|
||||
"Preferred mobile shape when platform policy makes sidecar daemons awkward.",
|
||||
),
|
||||
ProviderSourceDefinition(
|
||||
"system-adopted",
|
||||
"healthy local proxy or service already running",
|
||||
"operator-owned service; AMPB only routes browser traffic to it",
|
||||
"desktop, android",
|
||||
"Keeps power users and existing deployments working without duplicate daemons.",
|
||||
),
|
||||
ProviderSourceDefinition(
|
||||
"system-package",
|
||||
"known package manager install such as brew, apt, pkg, or platform provider",
|
||||
"installed only after user consent, then run with AMPB-owned state",
|
||||
"desktop, android",
|
||||
"Fallback when the app did not ship that provider yet.",
|
||||
),
|
||||
ProviderSourceDefinition(
|
||||
"builtin-renderer",
|
||||
"browser-native renderer or fetch path",
|
||||
"AMPB-owned code path with no external daemon",
|
||||
"desktop, android, ios",
|
||||
"Used for Gemini and other lightweight document transports.",
|
||||
),
|
||||
)
|
||||
|
||||
TRANSPORT_PROVIDER_DEFINITIONS = (
|
||||
TransportProviderDefinition(
|
||||
"tor",
|
||||
"arti/tor",
|
||||
("configured", "bundled-sidecar", "embedded-library", "system-adopted", "system-package"),
|
||||
"socks5://127.0.0.1:9050",
|
||||
"active",
|
||||
"Desktop uses managed Arti/Tor sidecars today; mobile should prefer embedded or app-owned foreground providers.",
|
||||
),
|
||||
TransportProviderDefinition(
|
||||
"i2p",
|
||||
"i2pd/i2p-router",
|
||||
("configured", "bundled-sidecar", "embedded-library", "system-adopted", "system-package"),
|
||||
"http://127.0.0.1:4444",
|
||||
"active",
|
||||
"Desktop can adopt or manage i2pd; mobile provider packaging is planned behind the same contract.",
|
||||
),
|
||||
TransportProviderDefinition(
|
||||
"gemini",
|
||||
"gemini-native-viewer",
|
||||
("builtin-renderer",),
|
||||
"builtin://gemtext-renderer",
|
||||
"planned",
|
||||
"No daemon is required once the built-in fetch/render path exists.",
|
||||
),
|
||||
TransportProviderDefinition(
|
||||
"ipfs",
|
||||
"kubo/ipfs",
|
||||
("configured", "bundled-sidecar", "system-adopted", "system-package"),
|
||||
"http://127.0.0.1:8080",
|
||||
"planned",
|
||||
"Content-addressed browsing uses the same provider lifecycle without treating IPFS as anonymity.",
|
||||
),
|
||||
TransportProviderDefinition(
|
||||
"reticulum",
|
||||
"rnsd/rnstatus",
|
||||
("configured", "bundled-sidecar", "system-adopted", "system-package"),
|
||||
"rns://local",
|
||||
"planned",
|
||||
"Reticulum browsing may still need operator-owned physical or link-layer interface setup.",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ def launch_open_plan(open_plan: OpenPlan, *, config: AppConfig, root: Path) -> O
|
||||
message=transport_result.message,
|
||||
transport_setup_status=transport_result.status,
|
||||
transport_setup_provider=transport_result.provider,
|
||||
transport_setup_provider_source=transport_result.provider_source,
|
||||
transport_setup_owned=transport_result.owned,
|
||||
transport_setup_pid=transport_result.pid,
|
||||
transport_setup_endpoint=transport_result.endpoint,
|
||||
|
||||
@@ -16,6 +16,7 @@ from .config import AppConfig
|
||||
|
||||
|
||||
ARTI_RELATIVE_PATH = Path("providers/arti/bin/arti")
|
||||
I2PD_RELATIVE_PATH = Path("providers/i2pd/bin/i2pd")
|
||||
OWNED_STATE_FILE = "ampb-owned.json"
|
||||
|
||||
|
||||
@@ -23,12 +24,14 @@ OWNED_STATE_FILE = "ampb-owned.json"
|
||||
class TorProvider:
|
||||
kind: str
|
||||
binary: str
|
||||
source: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class I2PProvider:
|
||||
kind: str
|
||||
binary: str
|
||||
source: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -42,6 +45,7 @@ class ManagedTransportResult:
|
||||
state_dir: str
|
||||
command: tuple[str, ...]
|
||||
message: str
|
||||
provider_source: str = "-"
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
@@ -76,6 +80,7 @@ def ensure_transport_ready(
|
||||
_managed_state_dir(root, config, transport),
|
||||
(),
|
||||
f"adopted existing {transport} transport",
|
||||
provider_source="system-adopted",
|
||||
)
|
||||
|
||||
if transport == "tor":
|
||||
@@ -122,6 +127,7 @@ def transport_status(
|
||||
_managed_state_dir(root, config, transport),
|
||||
(),
|
||||
f"{transport} transport is running but not AMPB-owned",
|
||||
provider_source="system-adopted",
|
||||
)
|
||||
|
||||
return ManagedTransportResult(
|
||||
@@ -149,6 +155,7 @@ def stop_managed_transport(
|
||||
endpoint = str(state.get("endpoint") or _endpoint_for(transport))
|
||||
state_dir = _managed_state_dir(root, config, transport)
|
||||
provider = str(state.get("provider") or "-")
|
||||
provider_source = str(state.get("provider_source") or "-")
|
||||
command = tuple(str(part) for part in state.get("command", ()))
|
||||
pid = _state_pid(state)
|
||||
|
||||
@@ -163,6 +170,7 @@ def stop_managed_transport(
|
||||
state_dir,
|
||||
(),
|
||||
f"no AMPB-owned {transport} process is recorded",
|
||||
provider_source=provider_source,
|
||||
)
|
||||
|
||||
if not _pid_alive(pid):
|
||||
@@ -181,6 +189,7 @@ def stop_managed_transport(
|
||||
if cleaned
|
||||
else f"could not remove stale AMPB-owned {transport} state"
|
||||
),
|
||||
provider_source=provider_source,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -196,6 +205,7 @@ def stop_managed_transport(
|
||||
state_dir,
|
||||
command,
|
||||
f"could not stop AMPB-owned {transport}: {exc}",
|
||||
provider_source=provider_source,
|
||||
)
|
||||
|
||||
deadline = time.monotonic() + wait_seconds
|
||||
@@ -216,6 +226,7 @@ def stop_managed_transport(
|
||||
if cleaned
|
||||
else f"stopped AMPB-owned {transport} but could not remove state"
|
||||
),
|
||||
provider_source=provider_source,
|
||||
)
|
||||
time.sleep(0.1)
|
||||
|
||||
@@ -229,6 +240,7 @@ def stop_managed_transport(
|
||||
state_dir,
|
||||
command,
|
||||
f"AMPB-owned {transport} did not stop before timeout",
|
||||
provider_source=provider_source,
|
||||
)
|
||||
|
||||
|
||||
@@ -273,6 +285,7 @@ def _start_tor(
|
||||
state_dir,
|
||||
command,
|
||||
f"managed {provider.kind} could not start: {exc}",
|
||||
provider_source=provider.source,
|
||||
)
|
||||
|
||||
if _wait_for_endpoint(endpoint, timeout_seconds=wait_seconds):
|
||||
@@ -286,6 +299,7 @@ def _start_tor(
|
||||
state_dir,
|
||||
command,
|
||||
f"started managed {provider.kind} transport",
|
||||
provider_source=provider.source,
|
||||
)
|
||||
_write_owned_state(root, config, result)
|
||||
return result
|
||||
@@ -301,31 +315,49 @@ def _start_tor(
|
||||
state_dir,
|
||||
command,
|
||||
f"managed {provider.kind} did not become ready at {endpoint}",
|
||||
provider_source=provider.source,
|
||||
)
|
||||
|
||||
|
||||
def _tor_provider(config: AppConfig) -> TorProvider | None:
|
||||
arti_path = os.environ.get("AMPB_ARTI_BIN") or _bundled_arti_path()
|
||||
arti_path = os.environ.get("AMPB_ARTI_BIN")
|
||||
if arti_path:
|
||||
return TorProvider("arti", arti_path)
|
||||
return TorProvider("arti", arti_path, "configured")
|
||||
|
||||
tor_path = os.environ.get("AMPB_TOR_BIN")
|
||||
if tor_path:
|
||||
return TorProvider("tor", tor_path)
|
||||
return TorProvider("tor", tor_path, "configured")
|
||||
|
||||
config_path = config.transport_binary("tor")
|
||||
if config_path:
|
||||
return TorProvider(_infer_provider_kind(config_path), config_path)
|
||||
return TorProvider(_infer_provider_kind(config_path), config_path, "configured")
|
||||
|
||||
bundled_arti = _bundled_arti_path()
|
||||
if bundled_arti:
|
||||
return TorProvider("arti", bundled_arti, "bundled-sidecar")
|
||||
|
||||
system_tor = shutil.which("tor")
|
||||
if system_tor:
|
||||
return TorProvider("tor", system_tor)
|
||||
return TorProvider("tor", system_tor, "system-package")
|
||||
return None
|
||||
|
||||
|
||||
def _bundled_arti_path() -> str:
|
||||
return _bundled_provider_path(ARTI_RELATIVE_PATH)
|
||||
|
||||
|
||||
def _bundled_i2pd_path() -> str:
|
||||
return _bundled_provider_path(I2PD_RELATIVE_PATH)
|
||||
|
||||
|
||||
def _bundled_provider_path(relative_path: Path) -> str:
|
||||
provider_root = os.environ.get("AMPB_PROVIDER_ROOT")
|
||||
if provider_root:
|
||||
candidate = Path(provider_root) / relative_path
|
||||
if candidate.exists() and candidate.is_file():
|
||||
return str(candidate)
|
||||
build_root = Path(os.environ.get("AMPB_BROWSER_BUILD_ROOT", "/tmp/ampb-browser-build"))
|
||||
candidate = build_root / ARTI_RELATIVE_PATH
|
||||
candidate = build_root / relative_path
|
||||
if candidate.exists() and candidate.is_file():
|
||||
return str(candidate)
|
||||
return ""
|
||||
@@ -418,6 +450,7 @@ def _start_i2p(
|
||||
state_dir,
|
||||
command,
|
||||
f"managed {provider.kind} could not start: {exc}",
|
||||
provider_source=provider.source,
|
||||
)
|
||||
|
||||
if _wait_for_endpoint(endpoint, timeout_seconds=wait_seconds):
|
||||
@@ -431,6 +464,7 @@ def _start_i2p(
|
||||
state_dir,
|
||||
command,
|
||||
f"started managed {provider.kind} transport",
|
||||
provider_source=provider.source,
|
||||
)
|
||||
_write_owned_state(root, config, result)
|
||||
return result
|
||||
@@ -446,25 +480,30 @@ def _start_i2p(
|
||||
state_dir,
|
||||
command,
|
||||
f"managed {provider.kind} did not become ready at {endpoint}",
|
||||
provider_source=provider.source,
|
||||
)
|
||||
|
||||
|
||||
def _i2p_provider(config: AppConfig) -> I2PProvider | None:
|
||||
env_path = os.environ.get("AMPB_I2PD_BIN")
|
||||
if env_path:
|
||||
return I2PProvider("i2pd", env_path)
|
||||
return I2PProvider("i2pd", env_path, "configured")
|
||||
|
||||
config_path = config.transport_binary("i2p")
|
||||
if config_path:
|
||||
return I2PProvider("i2pd", config_path)
|
||||
return I2PProvider("i2pd", config_path, "configured")
|
||||
|
||||
bundled_i2pd = _bundled_i2pd_path()
|
||||
if bundled_i2pd:
|
||||
return I2PProvider("i2pd", bundled_i2pd, "bundled-sidecar")
|
||||
|
||||
system_i2pd = shutil.which("i2pd")
|
||||
if system_i2pd:
|
||||
return I2PProvider("i2pd", system_i2pd)
|
||||
return I2PProvider("i2pd", system_i2pd, "system-package")
|
||||
|
||||
homebrew_i2pd = _homebrew_i2pd_path()
|
||||
if homebrew_i2pd:
|
||||
return I2PProvider("i2pd", homebrew_i2pd)
|
||||
return I2PProvider("i2pd", homebrew_i2pd, "system-package")
|
||||
return None
|
||||
|
||||
|
||||
@@ -575,6 +614,7 @@ def _write_owned_state(root: Path, config: AppConfig, result: ManagedTransportRe
|
||||
"pid": result.pid,
|
||||
"state_dir": result.state_dir,
|
||||
"command": list(result.command),
|
||||
"provider_source": result.provider_source,
|
||||
"started_at": int(time.time()),
|
||||
}
|
||||
state_path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
@@ -595,6 +635,7 @@ def _owned_transport_result(
|
||||
|
||||
pid = _state_pid(state)
|
||||
provider = str(state.get("provider") or "-")
|
||||
provider_source = str(state.get("provider_source") or "-")
|
||||
endpoint = str(state.get("endpoint") or _endpoint_for(transport))
|
||||
state_dir = str(state.get("state_dir") or _managed_state_dir(root, config, transport))
|
||||
command = tuple(str(part) for part in state.get("command", ()))
|
||||
@@ -615,6 +656,7 @@ def _owned_transport_result(
|
||||
if cleaned
|
||||
else f"could not remove stale AMPB-owned {transport} state"
|
||||
),
|
||||
provider_source=provider_source,
|
||||
)
|
||||
|
||||
if _wait_for_endpoint(endpoint, timeout_seconds=wait_seconds):
|
||||
@@ -628,6 +670,7 @@ def _owned_transport_result(
|
||||
state_dir,
|
||||
command,
|
||||
f"using running AMPB-owned {transport}",
|
||||
provider_source=provider_source,
|
||||
)
|
||||
|
||||
return ManagedTransportResult(
|
||||
@@ -640,6 +683,7 @@ def _owned_transport_result(
|
||||
state_dir,
|
||||
command,
|
||||
f"AMPB-owned {transport} process is running but endpoint is not ready",
|
||||
provider_source=provider_source,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -21,9 +21,9 @@ python3 -m ampbrowser docs generate --check
|
||||
|
||||
Generated files live under `docs/generated/` and must not be edited by hand. They cover
|
||||
route rules, active transports, browser backend strategy, adapter contracts, candidate
|
||||
transports, and platform capabilities. Browser source trees, object directories, caches,
|
||||
and artifacts belong under `/tmp/ampb-browser-build` or another explicit external build
|
||||
workspace, not this repo.
|
||||
transports, provider sources, and platform capabilities. Browser source trees, object
|
||||
directories, caches, and artifacts belong under `/tmp/ampb-browser-build` or another
|
||||
explicit external build workspace, not this repo.
|
||||
|
||||
## Manual Docs
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated Provider Sources
|
||||
|
||||
> Status: generated | Updated by `python3 -m ampbrowser docs generate` | Applies to: AMPB
|
||||
|
||||
This file is generated from code. Do not edit it by hand.
|
||||
|
||||
|
||||
## Source Types
|
||||
|
||||
| Source | Discovery | Lifecycle | Platforms | Note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `configured` | explicit config or environment variable | operator override; AMPB owns profile policy but not the binary provenance | desktop, android, ios | Used for development, custom bundles, and reviewed advanced installs. |
|
||||
| `bundled-sidecar` | provider binary shipped with AMPB or the build workspace | AMPB-owned process and isolated state | desktop, android | Default desktop target for Tor, I2P, IPFS, Reticulum, and similar daemon-style transports. |
|
||||
| `embedded-library` | transport library linked into the app process | AMPB-owned foreground session and app-private state | android, ios | Preferred mobile shape when platform policy makes sidecar daemons awkward. |
|
||||
| `system-adopted` | healthy local proxy or service already running | operator-owned service; AMPB only routes browser traffic to it | desktop, android | Keeps power users and existing deployments working without duplicate daemons. |
|
||||
| `system-package` | known package manager install such as brew, apt, pkg, or platform provider | installed only after user consent, then run with AMPB-owned state | desktop, android | Fallback when the app did not ship that provider yet. |
|
||||
| `builtin-renderer` | browser-native renderer or fetch path | AMPB-owned code path with no external daemon | desktop, android, ios | Used for Gemini and other lightweight document transports. |
|
||||
|
||||
## Transport Providers
|
||||
|
||||
| Transport | Provider | Sources | Endpoint | Status | Note |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `tor` | `arti/tor` | `configured`, `bundled-sidecar`, `embedded-library`, `system-adopted`, `system-package` | `socks5://127.0.0.1:9050` | `active` | Desktop uses managed Arti/Tor sidecars today; mobile should prefer embedded or app-owned foreground providers. |
|
||||
| `i2p` | `i2pd/i2p-router` | `configured`, `bundled-sidecar`, `embedded-library`, `system-adopted`, `system-package` | `http://127.0.0.1:4444` | `active` | Desktop can adopt or manage i2pd; mobile provider packaging is planned behind the same contract. |
|
||||
| `gemini` | `gemini-native-viewer` | `builtin-renderer` | `builtin://gemtext-renderer` | `planned` | No daemon is required once the built-in fetch/render path exists. |
|
||||
| `ipfs` | `kubo/ipfs` | `configured`, `bundled-sidecar`, `system-adopted`, `system-package` | `http://127.0.0.1:8080` | `planned` | Content-addressed browsing uses the same provider lifecycle without treating IPFS as anonymity. |
|
||||
| `reticulum` | `rnsd/rnstatus` | `configured`, `bundled-sidecar`, `system-adopted`, `system-package` | `rns://local` | `planned` | Reticulum browsing may still need operator-owned physical or link-layer interface setup. |
|
||||
@@ -60,6 +60,7 @@ class CliOpenTest(unittest.TestCase):
|
||||
state_dir=".ampb/transports/tor",
|
||||
command=("/tmp/arti", "proxy"),
|
||||
message="started managed arti transport",
|
||||
provider_source="bundled-sidecar",
|
||||
)
|
||||
|
||||
def fake_execute(open_plan, *, root: Path):
|
||||
@@ -85,6 +86,7 @@ class CliOpenTest(unittest.TestCase):
|
||||
self.assertIn("browser_pid=5678", output)
|
||||
self.assertIn("transport_setup_status=started", output)
|
||||
self.assertIn("transport_setup_provider=arti", output)
|
||||
self.assertIn("transport_setup_provider_source=bundled-sidecar", output)
|
||||
self.assertIn("transport_setup_owned=true", output)
|
||||
self.assertIn("transport_setup_pid=1234", output)
|
||||
self.assertIn("transport_setup_endpoint=socks5://127.0.0.1:9050", output)
|
||||
|
||||
@@ -19,6 +19,7 @@ class DocsGenTest(unittest.TestCase):
|
||||
Path("docs/generated/browser-strategy.md"),
|
||||
Path("docs/generated/candidate-transports.md"),
|
||||
Path("docs/generated/platform-capabilities.md"),
|
||||
Path("docs/generated/provider-sources.md"),
|
||||
Path("docs/generated/route-rules.md"),
|
||||
Path("docs/generated/transports.md"),
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@ class TransportManagerTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual("ready", result.status)
|
||||
self.assertEqual("-", result.provider)
|
||||
self.assertEqual("system-adopted", result.provider_source)
|
||||
self.assertFalse(result.owned)
|
||||
self.assertTrue(result.ready)
|
||||
self.assertEqual(0, result.pid)
|
||||
@@ -73,6 +74,7 @@ class TransportManagerTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual("started", result.status)
|
||||
self.assertEqual("arti", result.provider)
|
||||
self.assertEqual("bundled-sidecar", result.provider_source)
|
||||
self.assertTrue(result.owned)
|
||||
self.assertEqual(1234, result.pid)
|
||||
self.assertEqual(str(root / ".ampb/transports/tor"), result.state_dir)
|
||||
@@ -84,6 +86,7 @@ class TransportManagerTest(unittest.TestCase):
|
||||
state_path = root / ".ampb/transports/tor/ampb-owned.json"
|
||||
self.assertTrue(state_path.exists())
|
||||
self.assertIn('"provider": "arti"', state_path.read_text(encoding="utf-8"))
|
||||
self.assertIn('"provider_source": "bundled-sidecar"', state_path.read_text(encoding="utf-8"))
|
||||
popen.assert_called_once()
|
||||
|
||||
def test_reuses_recorded_ampb_owned_transport(self) -> None:
|
||||
@@ -122,6 +125,7 @@ class TransportManagerTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual("ready", result.status)
|
||||
self.assertEqual("arti", result.provider)
|
||||
self.assertEqual("-", result.provider_source)
|
||||
self.assertTrue(result.owned)
|
||||
self.assertEqual(1234, result.pid)
|
||||
popen.assert_not_called()
|
||||
@@ -199,6 +203,7 @@ class TransportManagerTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual("started", result.status)
|
||||
self.assertEqual("tor", result.provider)
|
||||
self.assertEqual("configured", result.provider_source)
|
||||
self.assertTrue(result.owned)
|
||||
self.assertEqual(1234, result.pid)
|
||||
self.assertEqual(str(root / ".ampb/transports/tor"), result.state_dir)
|
||||
@@ -226,6 +231,7 @@ class TransportManagerTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual("start-failed", result.status)
|
||||
self.assertEqual("tor", result.provider)
|
||||
self.assertEqual("configured", result.provider_source)
|
||||
self.assertFalse(result.ready)
|
||||
self.assertIn("could not start", result.message)
|
||||
|
||||
@@ -265,6 +271,7 @@ class TransportManagerTest(unittest.TestCase):
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with patch("ampbrowser.transport_manager.shutil.which", return_value=None):
|
||||
with patch("ampbrowser.transport_manager._bundled_i2pd_path", return_value=""):
|
||||
result = ensure_transport_ready("i2p", config=AppConfig(transport_modes={}), root=Path(tmp), status=status)
|
||||
|
||||
self.assertEqual("missing-provider", result.status)
|
||||
@@ -298,6 +305,7 @@ class TransportManagerTest(unittest.TestCase):
|
||||
return "/opt/homebrew/bin/brew" if name == "brew" else None
|
||||
|
||||
with patch("ampbrowser.transport_manager.shutil.which", side_effect=which):
|
||||
with patch("ampbrowser.transport_manager._bundled_i2pd_path", return_value=""):
|
||||
with patch("ampbrowser.transport_manager.subprocess.run", return_value=Result()):
|
||||
with patch("ampbrowser.transport_manager._wait_for_endpoint", return_value=True):
|
||||
with patch("ampbrowser.transport_manager.subprocess.Popen") as popen:
|
||||
@@ -305,8 +313,34 @@ class TransportManagerTest(unittest.TestCase):
|
||||
result = ensure_transport_ready("i2p", config=AppConfig(transport_modes={}), root=root, status=status)
|
||||
|
||||
self.assertEqual("started", result.status)
|
||||
self.assertEqual("system-package", result.provider_source)
|
||||
self.assertEqual(str(i2pd), result.command[0])
|
||||
|
||||
def test_starts_bundled_i2pd_provider_before_system_provider(self) -> None:
|
||||
status = TransportStatus(
|
||||
transport="i2p",
|
||||
installed=True,
|
||||
running=False,
|
||||
endpoint="http://127.0.0.1:4444",
|
||||
adoptable=False,
|
||||
manage_supported=True,
|
||||
note="I2P HTTP proxy",
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
bundled = root / "providers/i2pd/bin/i2pd"
|
||||
with patch("ampbrowser.transport_manager._bundled_i2pd_path", return_value=str(bundled)):
|
||||
with patch("ampbrowser.transport_manager.shutil.which", return_value="/usr/bin/i2pd"):
|
||||
with patch("ampbrowser.transport_manager._wait_for_endpoint", return_value=True):
|
||||
with patch("ampbrowser.transport_manager.subprocess.Popen") as popen:
|
||||
popen.return_value.pid = 1234
|
||||
result = ensure_transport_ready("i2p", config=AppConfig(transport_modes={}), root=root, status=status)
|
||||
|
||||
self.assertEqual("started", result.status)
|
||||
self.assertEqual("bundled-sidecar", result.provider_source)
|
||||
self.assertEqual(str(bundled), result.command[0])
|
||||
|
||||
def test_starts_configured_i2pd_provider_with_ampb_state(self) -> None:
|
||||
status = TransportStatus(
|
||||
transport="i2p",
|
||||
@@ -328,6 +362,7 @@ class TransportManagerTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual("started", result.status)
|
||||
self.assertEqual("i2pd", result.provider)
|
||||
self.assertEqual("configured", result.provider_source)
|
||||
self.assertTrue(result.owned)
|
||||
self.assertEqual(1234, result.pid)
|
||||
self.assertEqual(str(root / ".ampb/transports/i2p"), result.state_dir)
|
||||
@@ -346,6 +381,7 @@ class TransportManagerTest(unittest.TestCase):
|
||||
state_path = root / ".ampb/transports/i2p/ampb-owned.json"
|
||||
self.assertTrue(state_path.exists())
|
||||
self.assertIn('"provider": "i2pd"', state_path.read_text(encoding="utf-8"))
|
||||
self.assertIn('"provider_source": "configured"', state_path.read_text(encoding="utf-8"))
|
||||
popen.assert_called_once()
|
||||
|
||||
def test_terminates_owned_i2pd_process_on_timeout(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user