Add transport adapters and candidates

This commit is contained in:
jwinterm
2026-07-05 21:00:00 -04:00
parent 6c209b6557
commit a8cb056225
15 changed files with 393 additions and 109 deletions
+137
View File
@@ -0,0 +1,137 @@
from __future__ import annotations
from dataclasses import dataclass
import shutil
import socket
@dataclass(frozen=True)
class TransportStatus:
transport: str
installed: bool
running: bool
endpoint: str
adoptable: bool
manage_supported: bool
note: str
@dataclass(frozen=True)
class TransportAdapter:
name: str
profile: str
endpoint: str
adopt_check: str
managed_state: str
install_strategy: str
start_strategy: str
stop_policy: str
note: str
commands: tuple[str, ...] = ()
connect_host: str = ""
connect_port: int = 0
builtin_running: bool = False
manage_supported: bool = True
def inspect(self) -> TransportStatus:
installed = self.builtin_running or any(shutil.which(command) for command in self.commands)
running = self.builtin_running or (
bool(self.connect_host) and bool(self.connect_port) and _can_connect(self.connect_host, self.connect_port)
)
return TransportStatus(
transport=self.name,
installed=installed,
running=running,
endpoint=self.endpoint,
adoptable=running,
manage_supported=self.manage_supported,
note=self.note,
)
def setup_steps(self, *, installed: bool, platform: str) -> tuple[str, ...]:
steps: list[str] = []
if not installed and self.install_strategy != "unsupported":
steps.append(self.install_strategy)
if platform == "android":
steps.append(f"start visible Android foreground service for {self.name}")
steps.append(f"start managed Android {self.name} transport")
steps.append(f"wait for {self.endpoint}")
return tuple(steps)
elif platform == "ios":
steps.append(f"start foreground-only iOS session for {self.name}")
steps.append(f"start foreground-only iOS {self.name} transport")
steps.append(f"wait for {self.endpoint}")
return tuple(steps)
if self.start_strategy != "unsupported":
steps.append(self.start_strategy)
steps.append(f"wait for {self.endpoint}")
return tuple(steps)
ADAPTERS: dict[str, TransportAdapter] = {
"tor": TransportAdapter(
name="tor",
profile="tor",
endpoint="socks5://127.0.0.1:9050",
adopt_check="SOCKS on 127.0.0.1:9050",
managed_state=".ampb/transports/tor",
install_strategy="install Tor provider",
start_strategy="start managed Tor daemon",
stop_policy="stop only AMPB-owned Tor daemon",
note="Tor SOCKS proxy",
commands=("tor",),
connect_host="127.0.0.1",
connect_port=9050,
),
"i2p": TransportAdapter(
name="i2p",
profile="i2p",
endpoint="http://127.0.0.1:4444",
adopt_check="HTTP proxy on 127.0.0.1:4444",
managed_state=".ampb/transports/i2p",
install_strategy="install I2P provider",
start_strategy="start managed I2P router",
stop_policy="stop only AMPB-owned I2P router",
note="I2P HTTP proxy",
commands=("i2pd", "i2prouter"),
connect_host="127.0.0.1",
connect_port=4444,
),
"reticulum": TransportAdapter(
name="reticulum",
profile="reticulum",
endpoint="rns://local",
adopt_check="RNS tools and configured interfaces",
managed_state=".ampb/transports/reticulum",
install_strategy="install Reticulum provider",
start_strategy="start configured Reticulum interface",
stop_policy="stop only AMPB-owned Reticulum process",
note="Reticulum adapter is planned; interface readiness needs explicit config",
commands=("rnsd", "rnstatus", "rnodeconf"),
manage_supported=False,
),
"gemini": TransportAdapter(
name="gemini",
profile="gemini",
endpoint="builtin://gemtext-renderer",
adopt_check="built-in renderer available",
managed_state=".ampb/transports/gemini",
install_strategy="unsupported",
start_strategy="use built-in Gemtext fetch/render path",
stop_policy="no daemon to stop",
note="Built-in Gemtext fetch/render path",
builtin_running=True,
),
}
def adapter_for(name: str) -> TransportAdapter | None:
return ADAPTERS.get(name)
def _can_connect(host: str, port: int) -> bool:
try:
with socket.create_connection((host, port), timeout=0.2):
return True
except OSError:
return False
+97
View File
@@ -0,0 +1,97 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class CandidateTransport:
name: str
role: str
status: str
route_examples: tuple[str, ...]
browser_fit: str
publisher_fit: str
mobile_fit: str
source: str
note: str
CANDIDATE_TRANSPORTS = (
CandidateTransport(
name="ipfs",
role="content-addressed web",
status="next-evaluate",
route_examples=("ipfs://<cid>", "/ipfs/<cid>", "ipns://<name>"),
browser_fit="native fetch or local gateway",
publisher_fit="static site export and pinning",
mobile_fit="gateway first; embedded node later",
source="https://docs.ipfs.tech/concepts/what-is-ipfs/",
note="Strong fit for AMPG static output; not an anonymity layer by itself.",
),
CandidateTransport(
name="yggdrasil",
role="encrypted IPv6 overlay",
status="next-evaluate",
route_examples=("http://[0200::]/", "https://<yggdrasil-host>/"),
browser_fit="ordinary HTTP over overlay IPv6",
publisher_fit="serve normal HTTP/Gemini over overlay address",
mobile_fit="cross-platform, but mobile lifecycle needs adapter proof",
source="https://yggdrasil-network.github.io/",
note="Good fit for old-phone nodes and local/community mesh scenarios.",
),
CandidateTransport(
name="lokinet",
role="low-latency onion-routed overlay",
status="evaluate",
route_examples=("http://<name>.loki/", "https://<name>.loki/"),
browser_fit="ordinary IP traffic over overlay",
publisher_fit="SNApp-style hidden service publishing",
mobile_fit="Android likely first; iOS constrained",
source="https://docs.oxen.io/oxen-docs/products-built-on-oxen/lokinet",
note="Interesting Tor/I2P-adjacent candidate; depends on project health and packaging.",
),
CandidateTransport(
name="hyphanet",
role="privacy-preserving publishing datastore",
status="explore",
route_examples=("USK@...", "CHK@...", "SSK@..."),
browser_fit="local proxy/freesite viewer",
publisher_fit="censorship-resistant site publishing",
mobile_fit="possible on Android; iOS unlikely for full node",
source="https://www.hyphanet.org/",
note="Strong ideological fit; UX and key/address model need careful design.",
),
CandidateTransport(
name="gnunet",
role="privacy-preserving network stack",
status="explore",
route_examples=("gnunet://...", "gns://..."),
browser_fit="adapter required",
publisher_fit="future decentralized naming and publishing",
mobile_fit="research first",
source="https://www.gnunet.org/en/about.html",
note="Powerful but early-alpha per upstream; keep as research track.",
),
CandidateTransport(
name="arweave",
role="permanent web archive",
status="publisher-target",
route_examples=("ar://<txid>", "https://arweave.net/<txid>"),
browser_fit="gateway or HTTP API fetch",
publisher_fit="permanent static snapshots",
mobile_fit="publisher/export flow, not daemon-first",
source="https://docs.arweave.org/developers/arweave-node-server/http-api",
note="Great archival target; permanence has moderation and privacy implications.",
),
CandidateTransport(
name="nostr",
role="signed event relay network",
status="discovery-layer",
route_examples=("nostr:<event>", "nprofile...", "nevent..."),
browser_fit="discovery, comments, mirrors, address announcements",
publisher_fit="announce transport addresses and signed updates",
mobile_fit="good mobile fit; relay selection and spam controls matter",
source="https://github.com/nostr-protocol/nips/blob/master/01.md",
note="Not a web-page transport; useful as signed metadata and social distribution.",
),
)
+32
View File
@@ -2,6 +2,8 @@ from __future__ import annotations
from pathlib import Path
from .adapters import ADAPTERS
from .candidates import CANDIDATE_TRANSPORTS
from .metadata import ROUTE_RULES, TRANSPORT_DEFINITIONS
from .platforms import PLATFORM_CAPABILITIES
@@ -13,6 +15,8 @@ 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 / "adapters.md": _adapters_doc(),
GENERATED_DIR / "candidate-transports.md": _candidate_transports_doc(),
GENERATED_DIR / "platform-capabilities.md": _platform_capabilities_doc(),
}
changed: list[Path] = []
@@ -72,3 +76,31 @@ def _platform_capabilities_doc() -> str:
f"`{capability.adopt}` | `{capability.manage}` | `{capability.install}` | {capability.note} |"
)
return _header("Generated Platform Capabilities") + "\n".join(rows) + "\n"
def _adapters_doc() -> str:
rows = [
"| Adapter | Endpoint | Adopt Check | Install Strategy | Start Strategy | Stop Policy | Note |",
"| --- | --- | --- | --- | --- | --- | --- |",
]
for adapter in ADAPTERS.values():
rows.append(
f"| `{adapter.name}` | `{adapter.endpoint}` | {adapter.adopt_check} | "
f"{adapter.install_strategy} | {adapter.start_strategy} | {adapter.stop_policy} | {adapter.note} |"
)
return _header("Generated Transport Adapters") + "\n".join(rows) + "\n"
def _candidate_transports_doc() -> str:
rows = [
"| Candidate | Role | Status | Routes | Browser Fit | Publisher Fit | Mobile Fit | Source | Note |",
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
]
for candidate in CANDIDATE_TRANSPORTS:
routes = ", ".join(f"`{route}`" for route in candidate.route_examples)
rows.append(
f"| `{candidate.name}` | {candidate.role} | `{candidate.status}` | {routes} | "
f"{candidate.browser_fit} | {candidate.publisher_fit} | {candidate.mobile_fit} | "
f"[source]({candidate.source}) | {candidate.note} |"
)
return _header("Generated Candidate Transports") + "\n".join(rows) + "\n"
+11 -24
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass
from .adapters import adapter_for
from .config import AppConfig, default_config
from .plan import BrowsePlan, plan_url
@@ -88,33 +89,19 @@ def _proxy_for(browse_plan: BrowsePlan) -> str:
def _setup_steps(browse_plan: BrowsePlan) -> tuple[str, ...]:
if not browse_plan.status:
return ()
adapter = adapter_for(browse_plan.route.transport)
if not adapter:
return ()
steps: list[str] = []
platform = browse_plan.platform_capability.platform
if not browse_plan.status.installed:
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}",
]
)
if platform == "android" and not browse_plan.status.installed:
steps.append(f"install or enable Android {browse_plan.route.transport} provider")
steps.extend(adapter.setup_steps(installed=True, platform=platform))
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}",
]
)
if platform == "ios" and not browse_plan.status.installed:
steps.append(f"enable foreground iOS {browse_plan.route.transport} session")
steps.extend(adapter.setup_steps(installed=True, platform=platform))
return tuple(steps)
steps.extend([f"start managed {browse_plan.route.transport}", f"wait for {browse_plan.status.endpoint}"])
steps.extend(adapter.setup_steps(installed=browse_plan.status.installed, platform=platform))
return tuple(steps)
+3 -82
View File
@@ -1,31 +1,13 @@
from __future__ import annotations
from dataclasses import dataclass
import shutil
import socket
from .adapters import ADAPTERS, TransportStatus
@dataclass(frozen=True)
class TransportStatus:
transport: str
installed: bool
running: bool
endpoint: str
adoptable: bool
manage_supported: bool
note: str
TRANSPORTS = ("tor", "i2p", "reticulum", "gemini")
TRANSPORTS = tuple(ADAPTERS)
def inspect_transports() -> list[TransportStatus]:
return [
_inspect_tor(),
_inspect_i2p(),
_inspect_reticulum(),
_inspect_gemini(),
]
return [adapter.inspect() for adapter in ADAPTERS.values()]
def inspect_transport(name: str) -> TransportStatus | None:
@@ -33,64 +15,3 @@ def inspect_transport(name: str) -> TransportStatus | None:
if status.transport == name:
return status
return None
def _inspect_tor() -> TransportStatus:
installed = shutil.which("tor") is not None
running = _can_connect("127.0.0.1", 9050)
return TransportStatus(
transport="tor",
installed=installed,
running=running,
endpoint="socks5://127.0.0.1:9050",
adoptable=running,
manage_supported=True,
note="Tor SOCKS proxy",
)
def _inspect_i2p() -> TransportStatus:
installed = shutil.which("i2pd") is not None or shutil.which("i2prouter") is not None
running = _can_connect("127.0.0.1", 4444)
return TransportStatus(
transport="i2p",
installed=installed,
running=running,
endpoint="http://127.0.0.1:4444",
adoptable=running,
manage_supported=True,
note="I2P HTTP proxy",
)
def _inspect_reticulum() -> TransportStatus:
installed = any(shutil.which(command) for command in ("rnsd", "rnstatus", "rnodeconf"))
return TransportStatus(
transport="reticulum",
installed=installed,
running=False,
endpoint="rns://local",
adoptable=False,
manage_supported=False,
note="Reticulum adapter is planned; interface readiness needs explicit config",
)
def _inspect_gemini() -> TransportStatus:
return TransportStatus(
transport="gemini",
installed=True,
running=True,
endpoint="builtin://gemtext-renderer",
adoptable=True,
manage_supported=True,
note="Built-in Gemtext fetch/render path",
)
def _can_connect(host: str, port: int) -> bool:
try:
with socket.create_connection((host, port), timeout=0.2):
return True
except OSError:
return False