Model browser transport provider sources

This commit is contained in:
jwinterm
2026-07-08 10:13:15 -04:00
parent 42131a5ea0
commit e255093203
11 changed files with 264 additions and 16 deletions
+2
View File
@@ -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()} "
+27
View File
@@ -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)
+1
View File
@@ -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 = "-"
+107
View File
@@ -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.",
),
)
+1
View File
@@ -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,
+54 -10
View File
@@ -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,
)