mirror of
https://github.com/Such-Software/adaptive-multi-protocol-browser.git
synced 2026-10-17 14:31:46 +02:00
Add transport adapters and candidates
This commit is contained in:
@@ -46,6 +46,7 @@ python3 -m unittest discover -s tests
|
|||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [Architecture](docs/architecture.md)
|
- [Architecture](docs/architecture.md)
|
||||||
|
- [Candidate transports](docs/candidate-transports.md)
|
||||||
- [Configuration](docs/configuration.md)
|
- [Configuration](docs/configuration.md)
|
||||||
- [Mobile browser](docs/mobile-browser.md)
|
- [Mobile browser](docs/mobile-browser.md)
|
||||||
- [Open flow](docs/open-flow.md)
|
- [Open flow](docs/open-flow.md)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.",
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .adapters import ADAPTERS
|
||||||
|
from .candidates import CANDIDATE_TRANSPORTS
|
||||||
from .metadata import ROUTE_RULES, TRANSPORT_DEFINITIONS
|
from .metadata import ROUTE_RULES, TRANSPORT_DEFINITIONS
|
||||||
from .platforms import PLATFORM_CAPABILITIES
|
from .platforms import PLATFORM_CAPABILITIES
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ def generate_docs(root: Path, *, check: bool = False) -> list[Path]:
|
|||||||
docs = {
|
docs = {
|
||||||
GENERATED_DIR / "route-rules.md": _route_rules_doc(),
|
GENERATED_DIR / "route-rules.md": _route_rules_doc(),
|
||||||
GENERATED_DIR / "transports.md": _transports_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(),
|
GENERATED_DIR / "platform-capabilities.md": _platform_capabilities_doc(),
|
||||||
}
|
}
|
||||||
changed: list[Path] = []
|
changed: list[Path] = []
|
||||||
@@ -72,3 +76,31 @@ def _platform_capabilities_doc() -> str:
|
|||||||
f"`{capability.adopt}` | `{capability.manage}` | `{capability.install}` | {capability.note} |"
|
f"`{capability.adopt}` | `{capability.manage}` | `{capability.install}` | {capability.note} |"
|
||||||
)
|
)
|
||||||
return _header("Generated Platform Capabilities") + "\n".join(rows) + "\n"
|
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
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .adapters import adapter_for
|
||||||
from .config import AppConfig, default_config
|
from .config import AppConfig, default_config
|
||||||
from .plan import BrowsePlan, plan_url
|
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, ...]:
|
def _setup_steps(browse_plan: BrowsePlan) -> tuple[str, ...]:
|
||||||
if not browse_plan.status:
|
if not browse_plan.status:
|
||||||
return ()
|
return ()
|
||||||
|
adapter = adapter_for(browse_plan.route.transport)
|
||||||
|
if not adapter:
|
||||||
|
return ()
|
||||||
|
|
||||||
steps: list[str] = []
|
steps: list[str] = []
|
||||||
platform = browse_plan.platform_capability.platform
|
platform = browse_plan.platform_capability.platform
|
||||||
if not browse_plan.status.installed:
|
if platform == "android" and not browse_plan.status.installed:
|
||||||
if platform == "android":
|
steps.append(f"install or enable Android {browse_plan.route.transport} provider")
|
||||||
steps.append(f"install or enable Android {browse_plan.route.transport} provider")
|
steps.extend(adapter.setup_steps(installed=True, platform=platform))
|
||||||
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)
|
return tuple(steps)
|
||||||
elif platform == "ios":
|
if platform == "ios" and not browse_plan.status.installed:
|
||||||
steps.append("start foreground-only iOS session")
|
steps.append(f"enable foreground iOS {browse_plan.route.transport} session")
|
||||||
steps.extend(
|
steps.extend(adapter.setup_steps(installed=True, platform=platform))
|
||||||
[
|
|
||||||
f"start foreground-only iOS {browse_plan.route.transport} transport",
|
|
||||||
f"wait for {browse_plan.status.endpoint}",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return tuple(steps)
|
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)
|
return tuple(steps)
|
||||||
|
|||||||
@@ -1,31 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from .adapters import ADAPTERS, TransportStatus
|
||||||
import shutil
|
|
||||||
import socket
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
TRANSPORTS = tuple(ADAPTERS)
|
||||||
class TransportStatus:
|
|
||||||
transport: str
|
|
||||||
installed: bool
|
|
||||||
running: bool
|
|
||||||
endpoint: str
|
|
||||||
adoptable: bool
|
|
||||||
manage_supported: bool
|
|
||||||
note: str
|
|
||||||
|
|
||||||
|
|
||||||
TRANSPORTS = ("tor", "i2p", "reticulum", "gemini")
|
|
||||||
|
|
||||||
|
|
||||||
def inspect_transports() -> list[TransportStatus]:
|
def inspect_transports() -> list[TransportStatus]:
|
||||||
return [
|
return [adapter.inspect() for adapter in ADAPTERS.values()]
|
||||||
_inspect_tor(),
|
|
||||||
_inspect_i2p(),
|
|
||||||
_inspect_reticulum(),
|
|
||||||
_inspect_gemini(),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def inspect_transport(name: str) -> TransportStatus | None:
|
def inspect_transport(name: str) -> TransportStatus | None:
|
||||||
@@ -33,64 +15,3 @@ def inspect_transport(name: str) -> TransportStatus | None:
|
|||||||
if status.transport == name:
|
if status.transport == name:
|
||||||
return status
|
return status
|
||||||
return None
|
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
|
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ browser engine until those contracts are stable.
|
|||||||
## Main Components
|
## Main Components
|
||||||
|
|
||||||
- `ampbrowser.routing`: deterministic URL-to-transport rules.
|
- `ampbrowser.routing`: deterministic URL-to-transport rules.
|
||||||
|
- `ampbrowser.adapters`: transport ownership, setup, inspection, and health contracts.
|
||||||
- `ampbrowser.transports`: local readiness checks for Tor, I2P, Reticulum, and Gemini.
|
- `ampbrowser.transports`: local readiness checks for Tor, I2P, Reticulum, and Gemini.
|
||||||
- `ampbrowser.plan`: dry-run action planner for browser launches.
|
- `ampbrowser.plan`: dry-run action planner for browser launches.
|
||||||
- `ampbrowser.launch`: side-effect-free launch specs and first-use consent state.
|
- `ampbrowser.launch`: side-effect-free launch specs and first-use consent state.
|
||||||
- `ampbrowser.platforms`: platform capability matrix for desktop, Android, and iOS.
|
- `ampbrowser.platforms`: platform capability matrix for desktop, Android, and iOS.
|
||||||
|
- `ampbrowser.candidates`: evaluated transport candidates before active adapter support.
|
||||||
- `ampbrowser.docsgen`: generated public documentation from code-owned metadata.
|
- `ampbrowser.docsgen`: generated public documentation from code-owned metadata.
|
||||||
|
|
||||||
## Browser Strategy
|
## Browser Strategy
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Candidate Transports
|
||||||
|
|
||||||
|
> Status: draft | Updated 2026-07-06 | Applies to: AMPB and AMPG transport planning
|
||||||
|
|
||||||
|
Candidate transports are tracked separately from active adapters. A candidate should not
|
||||||
|
be routed by default until AMPB has an adapter contract, fixture behavior, mobile posture,
|
||||||
|
and publishing story for it.
|
||||||
|
|
||||||
|
## Selection Rules
|
||||||
|
|
||||||
|
- Prefer transports that can open AMPG static output or serve normal web/Gemini content.
|
||||||
|
- Treat discovery/social protocols as metadata layers, not page transports.
|
||||||
|
- Treat permanent archives as publishing targets with explicit user warnings.
|
||||||
|
- Keep mobile constraints visible before a candidate becomes active.
|
||||||
|
|
||||||
|
## Current Matrix
|
||||||
|
|
||||||
|
Generated candidate metadata lives in
|
||||||
|
[generated candidate transports](generated/candidate-transports.md).
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Generated Transport Adapters
|
||||||
|
|
||||||
|
> Status: generated | Updated by `python3 -m ampbrowser docs generate` | Applies to: AMPB
|
||||||
|
|
||||||
|
This file is generated from code. Do not edit it by hand.
|
||||||
|
|
||||||
|
| Adapter | Endpoint | Adopt Check | Install Strategy | Start Strategy | Stop Policy | Note |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `tor` | `socks5://127.0.0.1:9050` | SOCKS on 127.0.0.1:9050 | install Tor provider | start managed Tor daemon | stop only AMPB-owned Tor daemon | Tor SOCKS proxy |
|
||||||
|
| `i2p` | `http://127.0.0.1:4444` | HTTP proxy on 127.0.0.1:4444 | install I2P provider | start managed I2P router | stop only AMPB-owned I2P router | I2P HTTP proxy |
|
||||||
|
| `reticulum` | `rns://local` | RNS tools and configured interfaces | install Reticulum provider | start configured Reticulum interface | stop only AMPB-owned Reticulum process | Reticulum adapter is planned; interface readiness needs explicit config |
|
||||||
|
| `gemini` | `builtin://gemtext-renderer` | built-in renderer available | unsupported | use built-in Gemtext fetch/render path | no daemon to stop | Built-in Gemtext fetch/render path |
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Generated Candidate Transports
|
||||||
|
|
||||||
|
> Status: generated | Updated by `python3 -m ampbrowser docs generate` | Applies to: AMPB
|
||||||
|
|
||||||
|
This file is generated from code. Do not edit it by hand.
|
||||||
|
|
||||||
|
| Candidate | Role | Status | Routes | Browser Fit | Publisher Fit | Mobile Fit | Source | Note |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `ipfs` | content-addressed web | `next-evaluate` | `ipfs://<cid>`, `/ipfs/<cid>`, `ipns://<name>` | native fetch or local gateway | static site export and pinning | gateway first; embedded node later | [source](https://docs.ipfs.tech/concepts/what-is-ipfs/) | Strong fit for AMPG static output; not an anonymity layer by itself. |
|
||||||
|
| `yggdrasil` | encrypted IPv6 overlay | `next-evaluate` | `http://[0200::]/`, `https://<yggdrasil-host>/` | ordinary HTTP over overlay IPv6 | serve normal HTTP/Gemini over overlay address | cross-platform, but mobile lifecycle needs adapter proof | [source](https://yggdrasil-network.github.io/) | Good fit for old-phone nodes and local/community mesh scenarios. |
|
||||||
|
| `lokinet` | low-latency onion-routed overlay | `evaluate` | `http://<name>.loki/`, `https://<name>.loki/` | ordinary IP traffic over overlay | SNApp-style hidden service publishing | Android likely first; iOS constrained | [source](https://docs.oxen.io/oxen-docs/products-built-on-oxen/lokinet) | Interesting Tor/I2P-adjacent candidate; depends on project health and packaging. |
|
||||||
|
| `hyphanet` | privacy-preserving publishing datastore | `explore` | `USK@...`, `CHK@...`, `SSK@...` | local proxy/freesite viewer | censorship-resistant site publishing | possible on Android; iOS unlikely for full node | [source](https://www.hyphanet.org/) | Strong ideological fit; UX and key/address model need careful design. |
|
||||||
|
| `gnunet` | privacy-preserving network stack | `explore` | `gnunet://...`, `gns://...` | adapter required | future decentralized naming and publishing | research first | [source](https://www.gnunet.org/en/about.html) | Powerful but early-alpha per upstream; keep as research track. |
|
||||||
|
| `arweave` | permanent web archive | `publisher-target` | `ar://<txid>`, `https://arweave.net/<txid>` | gateway or HTTP API fetch | permanent static snapshots | publisher/export flow, not daemon-first | [source](https://docs.arweave.org/developers/arweave-node-server/http-api) | Great archival target; permanence has moderation and privacy implications. |
|
||||||
|
| `nostr` | signed event relay network | `discovery-layer` | `nostr:<event>`, `nprofile...`, `nevent...` | discovery, comments, mirrors, address announcements | announce transport addresses and signed updates | good mobile fit; relay selection and spam controls matter | [source](https://github.com/nostr-protocol/nips/blob/master/01.md) | Not a web-page transport; useful as signed metadata and social distribution. |
|
||||||
@@ -36,3 +36,4 @@ leases, tunnels, and daemon logs in that tree are local state and must not be co
|
|||||||
- Reticulum: route contract exists; adapter and readiness model are planned.
|
- Reticulum: route contract exists; adapter and readiness model are planned.
|
||||||
|
|
||||||
Generated transport metadata lives in [generated transports](generated/transports.md).
|
Generated transport metadata lives in [generated transports](generated/transports.md).
|
||||||
|
Adapter ownership and setup contracts live in [generated adapters](generated/adapters.md).
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from ampbrowser.adapters import ADAPTERS, adapter_for
|
||||||
|
from ampbrowser.transports import inspect_transport
|
||||||
|
|
||||||
|
|
||||||
|
class AdaptersTest(unittest.TestCase):
|
||||||
|
def test_known_adapters_are_registered(self) -> None:
|
||||||
|
self.assertEqual({"tor", "i2p", "reticulum", "gemini"}, set(ADAPTERS))
|
||||||
|
|
||||||
|
def test_gemini_adapter_is_builtin_ready(self) -> None:
|
||||||
|
status = inspect_transport("gemini")
|
||||||
|
|
||||||
|
self.assertIsNotNone(status)
|
||||||
|
self.assertTrue(status.installed)
|
||||||
|
self.assertTrue(status.running)
|
||||||
|
self.assertTrue(status.adoptable)
|
||||||
|
|
||||||
|
def test_tor_adapter_setup_steps_respect_ownership_language(self) -> None:
|
||||||
|
adapter = adapter_for("tor")
|
||||||
|
|
||||||
|
self.assertIsNotNone(adapter)
|
||||||
|
self.assertEqual("stop only AMPB-owned Tor daemon", adapter.stop_policy)
|
||||||
|
self.assertEqual(("install Tor provider", "start managed Tor daemon", "wait for socks5://127.0.0.1:9050"), adapter.setup_steps(installed=False, platform="desktop"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from ampbrowser.candidates import CANDIDATE_TRANSPORTS
|
||||||
|
|
||||||
|
|
||||||
|
class CandidateTransportsTest(unittest.TestCase):
|
||||||
|
def test_candidate_matrix_contains_next_evaluation_targets(self) -> None:
|
||||||
|
by_name = {candidate.name: candidate for candidate in CANDIDATE_TRANSPORTS}
|
||||||
|
|
||||||
|
self.assertIn("ipfs", by_name)
|
||||||
|
self.assertIn("yggdrasil", by_name)
|
||||||
|
self.assertEqual("next-evaluate", by_name["ipfs"].status)
|
||||||
|
self.assertEqual("next-evaluate", by_name["yggdrasil"].status)
|
||||||
|
|
||||||
|
def test_nostr_is_not_classified_as_page_transport(self) -> None:
|
||||||
|
by_name = {candidate.name: candidate for candidate in CANDIDATE_TRANSPORTS}
|
||||||
|
|
||||||
|
self.assertEqual("discovery-layer", by_name["nostr"].status)
|
||||||
|
self.assertIn("Not a web-page transport", by_name["nostr"].note)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -15,6 +15,8 @@ class DocsGenTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{
|
{
|
||||||
|
Path("docs/generated/adapters.md"),
|
||||||
|
Path("docs/generated/candidate-transports.md"),
|
||||||
Path("docs/generated/platform-capabilities.md"),
|
Path("docs/generated/platform-capabilities.md"),
|
||||||
Path("docs/generated/route-rules.md"),
|
Path("docs/generated/route-rules.md"),
|
||||||
Path("docs/generated/transports.md"),
|
Path("docs/generated/transports.md"),
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ class PrepareOpenTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual("consent-required", plan.status)
|
self.assertEqual("consent-required", plan.status)
|
||||||
self.assertFalse(plan.consent_granted)
|
self.assertFalse(plan.consent_granted)
|
||||||
self.assertEqual(("install i2p", "start managed i2p", "wait for http://127.0.0.1:4444"), plan.setup_steps)
|
self.assertEqual(
|
||||||
|
("install I2P provider", "start managed I2P router", "wait for http://127.0.0.1:4444"),
|
||||||
|
plan.setup_steps,
|
||||||
|
)
|
||||||
self.assertIn("install and start", plan.message)
|
self.assertIn("install and start", plan.message)
|
||||||
|
|
||||||
def test_yes_approves_setup_without_executing_it(self) -> None:
|
def test_yes_approves_setup_without_executing_it(self) -> None:
|
||||||
@@ -53,7 +56,7 @@ class PrepareOpenTest(unittest.TestCase):
|
|||||||
self.assertEqual("setup-approved", plan.status)
|
self.assertEqual("setup-approved", plan.status)
|
||||||
self.assertTrue(plan.dry_run)
|
self.assertTrue(plan.dry_run)
|
||||||
self.assertTrue(plan.consent_granted)
|
self.assertTrue(plan.consent_granted)
|
||||||
self.assertEqual(("start managed tor", "wait for socks5://127.0.0.1:9050"), plan.setup_steps)
|
self.assertEqual(("start managed Tor daemon", "wait for socks5://127.0.0.1:9050"), plan.setup_steps)
|
||||||
|
|
||||||
def test_custom_state_dir_changes_profile_path(self) -> None:
|
def test_custom_state_dir_changes_profile_path(self) -> None:
|
||||||
config = AppConfig(state_dir=".local/ampb", transport_modes={})
|
config = AppConfig(state_dir=".local/ampb", transport_modes={})
|
||||||
@@ -80,7 +83,7 @@ class PrepareOpenTest(unittest.TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
(
|
(
|
||||||
"install or enable Android i2p provider",
|
"install or enable Android i2p provider",
|
||||||
"start visible Android foreground service",
|
"start visible Android foreground service for i2p",
|
||||||
"start managed Android i2p transport",
|
"start managed Android i2p transport",
|
||||||
"wait for http://127.0.0.1:4444",
|
"wait for http://127.0.0.1:4444",
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user