mirror of
https://github.com/Such-Software/adaptive-multi-protocol-browser.git
synced 2026-10-17 14:31:46 +02:00
Harden browser docs and transport recovery
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Adaptive Multi-Protocol Browser
|
||||
|
||||
> Status: draft | Updated 2026-07-07 | Applies to: AMPB contributors and operators
|
||||
> Status: draft | Updated 2026-07-11 | Applies to: AMPB contributors and operators
|
||||
|
||||
Adaptive Multi-Protocol Browser, or AMPB, is a transport-aware browser shell. It routes
|
||||
URLs to the right network, adopts transports that are already healthy, and manages missing
|
||||
@@ -11,8 +11,8 @@ app, Android targets bundled GeckoView/Fenix/Tor Browser Android-lineage code, a
|
||||
uses an AMPB-owned shell with platform browser-engine constraints.
|
||||
|
||||
The first fixture is Wownero across clearnet, Tor, I2P, Gemini, and Reticulum-oriented
|
||||
targets generated by AMPG. Interactive fixtures include `ai-gen-bot` and the Medusa
|
||||
multi-tenant platform.
|
||||
targets generated by AMPG. Interactive fixtures should be described publicly by the
|
||||
capabilities they exercise, while project-specific target notes stay in private docs.
|
||||
|
||||
## Goals
|
||||
|
||||
@@ -21,6 +21,8 @@ multi-tenant platform.
|
||||
- Prompt on first visit before installing or starting a missing transport.
|
||||
- Manage approved transports through isolated AMPB state.
|
||||
- Keep browser profiles isolated by transport.
|
||||
- Treat route-aware desktop launches as a convenience and QA mode until storage isolation
|
||||
is proven for mixed clearnet, Tor, and I2P navigation.
|
||||
- Present transport health in plain language.
|
||||
|
||||
## Non-goals
|
||||
|
||||
+9
-1
@@ -15,7 +15,7 @@ from .plan import plan_url
|
||||
from .platforms import PLATFORM_CHOICES
|
||||
from .route_helper import serve_route_helper
|
||||
from .routing import route_url
|
||||
from .transport_manager import ensure_transport_ready, stop_managed_transport, transport_status
|
||||
from .transport_manager import ensure_transport_ready, repair_managed_transport, stop_managed_transport, transport_status
|
||||
from .transports import inspect_transports
|
||||
|
||||
|
||||
@@ -80,6 +80,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
transport_stop_parser = transport_subcommands.add_parser("stop", help="Stop an AMPB-owned transport.")
|
||||
transport_stop_parser.add_argument("transport")
|
||||
transport_stop_parser.add_argument("--config", type=Path, help="Path to an AMPB config file.")
|
||||
transport_repair_parser = transport_subcommands.add_parser("repair", help="Reset AMPB-owned runtime state for a transport.")
|
||||
transport_repair_parser.add_argument("transport")
|
||||
transport_repair_parser.add_argument("--config", type=Path, help="Path to an AMPB config file.")
|
||||
|
||||
fixture_parser = subcommands.add_parser("fixture", help="Check AMPG fixture manifests.")
|
||||
fixture_subcommands = fixture_parser.add_subparsers(dest="fixture_command", required=True)
|
||||
@@ -286,9 +289,12 @@ def _cmd_transport(args) -> int:
|
||||
result = ensure_transport_ready(args.transport, config=config, root=Path.cwd())
|
||||
elif args.transport_command == "stop":
|
||||
result = stop_managed_transport(args.transport, config=config, root=Path.cwd())
|
||||
elif args.transport_command == "repair":
|
||||
result = repair_managed_transport(args.transport, config=config, root=Path.cwd())
|
||||
else:
|
||||
return 1
|
||||
command = shlex.join(result.command) if result.command else "-"
|
||||
install_command = shlex.join(result.install_command) if result.install_command else "-"
|
||||
print(
|
||||
"AMPBROWSER_TRANSPORT "
|
||||
f"transport={result.transport} "
|
||||
@@ -301,6 +307,8 @@ def _cmd_transport(args) -> int:
|
||||
f"endpoint={result.endpoint} "
|
||||
f"state_dir={result.state_dir} "
|
||||
f"command=\"{_safe(command)}\" "
|
||||
f"install_command=\"{_safe(install_command)}\" "
|
||||
f"setup_hint=\"{_safe(result.setup_hint)}\" "
|
||||
f"message=\"{_safe(result.message)}\""
|
||||
)
|
||||
return 0
|
||||
|
||||
+22
-2
@@ -549,8 +549,9 @@ def _route_helper_background_js() -> str:
|
||||
" }",
|
||||
" if (status.ready) return;",
|
||||
" pendingByTab.set(details.tabId, details.url);",
|
||||
" const installCommand = Array.isArray(status.install_command) ? status.install_command.join(' ') : '';",
|
||||
" const setupUrl = browser.runtime.getURL(",
|
||||
" `setup.html?transport=${encodeURIComponent(transport)}&url=${encodeURIComponent(details.url)}&message=${encodeURIComponent(status.message || '')}`",
|
||||
" `setup.html?transport=${encodeURIComponent(transport)}&url=${encodeURIComponent(details.url)}&message=${encodeURIComponent(status.message || '')}&hint=${encodeURIComponent(status.setup_hint || '')}&install=${encodeURIComponent(installCommand)}`",
|
||||
" );",
|
||||
" await browser.tabs.update(details.tabId, { url: setupUrl });",
|
||||
"}",
|
||||
@@ -584,6 +585,7 @@ def _route_helper_setup_html() -> str:
|
||||
' <p id="body">AMPB needs to start a local transport before opening this route.</p>',
|
||||
' <pre id="url"></pre>',
|
||||
' <p id="message" class="message"></p>',
|
||||
' <pre id="install-command" class="install-command" hidden></pre>',
|
||||
' <button id="approve" type="button">Start and open</button>',
|
||||
' <p id="status" class="status"></p>',
|
||||
" </main>",
|
||||
@@ -604,13 +606,27 @@ def _route_helper_setup_js() -> str:
|
||||
"const transport = params.get('transport') || '';",
|
||||
"const url = params.get('url') || '';",
|
||||
"const message = params.get('message') || '';",
|
||||
"const hint = params.get('hint') || '';",
|
||||
"const install = params.get('install') || '';",
|
||||
"const label = transport === 'tor' ? 'Tor' : transport === 'i2p' ? 'I2P' : transport;",
|
||||
"",
|
||||
"function showInstallCommand(value) {",
|
||||
" const installCommand = document.getElementById('install-command');",
|
||||
" if (!value) {",
|
||||
" installCommand.hidden = true;",
|
||||
" installCommand.textContent = '';",
|
||||
" return;",
|
||||
" }",
|
||||
" installCommand.hidden = false;",
|
||||
" installCommand.textContent = value;",
|
||||
"}",
|
||||
"",
|
||||
"document.getElementById('title').textContent = `Set up ${label}?`;",
|
||||
"document.getElementById('body').textContent = `AMPB can start a managed ${label} session and keep its state isolated.`;",
|
||||
"document.getElementById('url').textContent = url;",
|
||||
"document.getElementById('message').textContent = message;",
|
||||
"document.getElementById('message').textContent = hint || message;",
|
||||
"document.getElementById('approve').textContent = `Start ${label} and open`;",
|
||||
"showInstallCommand(install);",
|
||||
"",
|
||||
"document.getElementById('approve').addEventListener('click', async () => {",
|
||||
" const status = document.getElementById('status');",
|
||||
@@ -621,6 +637,9 @@ def _route_helper_setup_js() -> str:
|
||||
" window.location.href = url;",
|
||||
" return;",
|
||||
" }",
|
||||
" if (result && Array.isArray(result.install_command)) {",
|
||||
" showInstallCommand(result.install_command.join(' '));",
|
||||
" }",
|
||||
" status.textContent = (result && result.message) || `${label} did not become ready.`;",
|
||||
"});",
|
||||
"",
|
||||
@@ -637,6 +656,7 @@ def _route_helper_setup_css() -> str:
|
||||
".eyebrow { color: #36d1c4; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }",
|
||||
"h1 { font-size: 2rem; margin: 0 0 12px; }",
|
||||
"pre { white-space: pre-wrap; overflow-wrap: anywhere; padding: 12px; border: 1px solid #444; }",
|
||||
".install-command { background: #1d1d1d; border-color: #36d1c4; color: #f7f1e8; }",
|
||||
"button { font: inherit; font-weight: 700; padding: 12px 16px; border: 0; background: #36d1c4; color: #111; }",
|
||||
".message, .status { color: #cfc7bb; }",
|
||||
"",
|
||||
|
||||
@@ -126,6 +126,8 @@ def _result_response(result: ManagedTransportResult) -> dict[str, Any]:
|
||||
"state_dir": result.state_dir,
|
||||
"message": result.message,
|
||||
"command": list(result.command),
|
||||
"setup_hint": result.setup_hint,
|
||||
"install_command": list(result.install_command),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import signal
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -34,6 +35,12 @@ class I2PProvider:
|
||||
source: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SetupHint:
|
||||
message: str
|
||||
command: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ManagedTransportResult:
|
||||
transport: str
|
||||
@@ -46,6 +53,8 @@ class ManagedTransportResult:
|
||||
command: tuple[str, ...]
|
||||
message: str
|
||||
provider_source: str = "-"
|
||||
setup_hint: str = "-"
|
||||
install_command: tuple[str, ...] = ()
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
@@ -140,6 +149,8 @@ def transport_status(
|
||||
_managed_state_dir(root, config, transport),
|
||||
(),
|
||||
f"{transport} transport is not running",
|
||||
setup_hint=_stopped_setup_hint(transport, config),
|
||||
install_command=_stopped_install_command(transport, config),
|
||||
)
|
||||
|
||||
|
||||
@@ -244,6 +255,66 @@ def stop_managed_transport(
|
||||
)
|
||||
|
||||
|
||||
def repair_managed_transport(
|
||||
transport: str,
|
||||
*,
|
||||
config: AppConfig,
|
||||
root: Path,
|
||||
wait_seconds: float = 5.0,
|
||||
) -> ManagedTransportResult:
|
||||
adapter = adapter_for(transport)
|
||||
if adapter is None:
|
||||
return ManagedTransportResult(transport, "-", "unsupported", "-", False, 0, "-", (), "transport adapter not found")
|
||||
|
||||
state_dir = _managed_state_dir(root, config, transport)
|
||||
state_path = Path(state_dir)
|
||||
stop_result = stop_managed_transport(transport, config=config, root=root, wait_seconds=wait_seconds)
|
||||
if stop_result.status in {"stop-failed", "stop-timeout"}:
|
||||
return ManagedTransportResult(
|
||||
transport,
|
||||
stop_result.provider,
|
||||
"repair-blocked",
|
||||
stop_result.endpoint,
|
||||
stop_result.owned,
|
||||
stop_result.pid,
|
||||
state_dir,
|
||||
stop_result.command,
|
||||
f"could not repair AMPB-owned {transport}: {stop_result.message}",
|
||||
provider_source=stop_result.provider_source,
|
||||
)
|
||||
|
||||
try:
|
||||
shutil.rmtree(state_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as exc:
|
||||
return ManagedTransportResult(
|
||||
transport,
|
||||
stop_result.provider,
|
||||
"repair-failed",
|
||||
stop_result.endpoint,
|
||||
stop_result.owned,
|
||||
stop_result.pid,
|
||||
state_dir,
|
||||
stop_result.command,
|
||||
f"could not remove AMPB-owned {transport} runtime state: {exc}",
|
||||
provider_source=stop_result.provider_source,
|
||||
)
|
||||
|
||||
return ManagedTransportResult(
|
||||
transport,
|
||||
stop_result.provider,
|
||||
"repaired",
|
||||
stop_result.endpoint,
|
||||
stop_result.owned,
|
||||
stop_result.pid,
|
||||
state_dir,
|
||||
stop_result.command,
|
||||
f"removed AMPB-owned {transport} runtime state; run `ampbrowser transport start {transport}` to rebuild it",
|
||||
provider_source=stop_result.provider_source,
|
||||
)
|
||||
|
||||
|
||||
def _start_tor(
|
||||
*,
|
||||
config: AppConfig,
|
||||
@@ -418,6 +489,7 @@ def _start_i2p(
|
||||
provider = _i2p_provider(config)
|
||||
state_dir = _managed_state_dir(root, config, "i2p")
|
||||
if not provider:
|
||||
setup = _i2p_setup_hint()
|
||||
return ManagedTransportResult(
|
||||
"i2p",
|
||||
"-",
|
||||
@@ -427,8 +499,9 @@ def _start_i2p(
|
||||
0,
|
||||
state_dir,
|
||||
(),
|
||||
"I2P provider not found; on macOS run: brew install i2pd. "
|
||||
"Alternatively set AMPB_I2PD_BIN or transports.i2p.binary_path.",
|
||||
"I2P provider not found. " + setup.message,
|
||||
setup_hint=setup.message,
|
||||
install_command=setup.command,
|
||||
)
|
||||
|
||||
state_path = Path(state_dir)
|
||||
@@ -507,6 +580,33 @@ def _i2p_provider(config: AppConfig) -> I2PProvider | None:
|
||||
return None
|
||||
|
||||
|
||||
def _stopped_setup_hint(transport: str, config: AppConfig) -> str:
|
||||
if transport == "i2p" and _i2p_provider(config) is None:
|
||||
return _i2p_setup_hint().message
|
||||
return "-"
|
||||
|
||||
|
||||
def _stopped_install_command(transport: str, config: AppConfig) -> tuple[str, ...]:
|
||||
if transport == "i2p" and _i2p_provider(config) is None:
|
||||
return _i2p_setup_hint().command
|
||||
return ()
|
||||
|
||||
|
||||
def _i2p_setup_hint() -> SetupHint:
|
||||
override = "Or set AMPB_I2PD_BIN or transports.i2p.binary_path to an i2pd binary."
|
||||
if sys.platform == "darwin":
|
||||
return SetupHint("Install i2pd with Homebrew: brew install i2pd. " + override, ("brew", "install", "i2pd"))
|
||||
if shutil.which("pkg"):
|
||||
return SetupHint("Install i2pd with pkg: pkg install i2pd. " + override, ("pkg", "install", "i2pd"))
|
||||
if shutil.which("apt"):
|
||||
return SetupHint("Install i2pd with apt: sudo apt install i2pd. " + override, ("sudo", "apt", "install", "i2pd"))
|
||||
if shutil.which("dnf"):
|
||||
return SetupHint("Install i2pd with dnf: sudo dnf install i2pd. " + override, ("sudo", "dnf", "install", "i2pd"))
|
||||
if shutil.which("pacman"):
|
||||
return SetupHint("Install i2pd with pacman: sudo pacman -S i2pd. " + override, ("sudo", "pacman", "-S", "i2pd"))
|
||||
return SetupHint("Install i2pd with your package manager. " + override)
|
||||
|
||||
|
||||
def _homebrew_i2pd_path() -> str:
|
||||
brew = shutil.which("brew")
|
||||
if not brew:
|
||||
|
||||
+5
-1
@@ -1,6 +1,6 @@
|
||||
# Autodocs
|
||||
|
||||
> Status: draft | Updated 2026-07-07 | Applies to: AMPB maintainers
|
||||
> Status: draft | Updated 2026-07-11 | Applies to: AMPB maintainers
|
||||
|
||||
Public docs should stay concise and current. Code-owned behavior is documented from code,
|
||||
while manual docs explain stable concepts and operator-facing choices.
|
||||
@@ -41,3 +41,7 @@ Run:
|
||||
```sh
|
||||
python3 tools/docs_check.py
|
||||
```
|
||||
|
||||
The public docs gate checks status headers, internal links, repo-contained local links,
|
||||
private fixture target names, obvious private-key blocks, common API tokens, and assigned
|
||||
secret values. Private notes belong under ignored `docs/private/`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Browser Builds
|
||||
|
||||
> Status: draft | Updated 2026-07-07 | Applies to: AMPB desktop and mobile builders
|
||||
> Status: draft | Updated 2026-07-11 | Applies to: AMPB desktop and mobile builders
|
||||
|
||||
AMPB browser builds must keep source checkouts, object directories, toolchain caches, and
|
||||
temporary artifacts outside this repository. The default local workspace is
|
||||
@@ -99,8 +99,9 @@ system `tor` binary if one is present.
|
||||
Managed I2P startup currently targets `i2pd`. AMPB adopts an existing HTTP proxy on
|
||||
`127.0.0.1:4444` when available; otherwise `ampbrowser open <i2p-url> --yes --launch`
|
||||
uses `AMPB_I2PD_BIN`, configured `transports.i2p.binary_path`, or a system `i2pd` binary.
|
||||
The AMPB-owned process writes config, router data, logs, and ownership metadata under
|
||||
`.ampb/transports/i2p`.
|
||||
If no provider is found, AMPB returns a structured setup hint and install command for the
|
||||
CLI and route-helper page. The AMPB-owned process writes config, router data, logs, and
|
||||
ownership metadata under `.ampb/transports/i2p`.
|
||||
|
||||
## Source Plan
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Desktop Vertical
|
||||
|
||||
> Status: draft | Updated 2026-07-07 | Applies to: AMPB desktop builders
|
||||
> Status: draft | Updated 2026-07-11 | Applies to: AMPB desktop builders
|
||||
|
||||
The first AMPB browser proof is desktop-first with mobile constraints kept in view. The
|
||||
vertical is a bundled Gecko desktop runtime, AMPB-owned profile state, and a managed Tor
|
||||
@@ -26,10 +26,10 @@ artifacts are available for the selected checkout.
|
||||
|
||||
## Transport Path
|
||||
|
||||
The first transport target is Tor through an AMPB-owned Arti/Tor runtime. The desktop
|
||||
vertical is not complete until AMPB can start the runtime, verify the SOCKS endpoint, and
|
||||
launch the bundled browser profile without clearnet fallback. I2P and Reticulum use the
|
||||
same route, consent, state, and health-check model after Tor works.
|
||||
The first transport target is Tor through an AMPB-owned Arti/Tor runtime. The current
|
||||
desktop vertical can launch a bundled Gecko runtime, start a route-aware helper, and keep
|
||||
the helper tied to the browser process. I2P uses the same route, consent, state, and
|
||||
health-check model. Reticulum follows after the resilient-network adapter is concrete.
|
||||
|
||||
## Desktop Shell
|
||||
|
||||
|
||||
+8
-7
@@ -1,6 +1,6 @@
|
||||
# Open Flow
|
||||
|
||||
> Status: draft | Updated 2026-07-08 | Applies to: AMPB users and implementers
|
||||
> Status: draft | Updated 2026-07-11 | Applies to: AMPB users and implementers
|
||||
|
||||
`ampbrowser open` prepares a safe launch plan for one URL. It routes the URL, checks only
|
||||
the selected transport, and reports whether the browser can open immediately or needs a
|
||||
@@ -34,12 +34,13 @@ adopted daemon from an AMPB-owned managed process.
|
||||
through a desktop dialog when possible, falls back to a terminal prompt when needed, and
|
||||
launches through the same transport runner after approval.
|
||||
|
||||
`ampbrowser open <url> --route-aware --launch` creates one desktop profile for clearnet,
|
||||
Tor, and I2P. The profile uses a PAC file for `.onion` and `.i2p` routing, installs the
|
||||
AMPB route-helper extension, and starts a token-gated loopback helper tied to the browser
|
||||
process. If a user clicks an alternate-network link before its local transport is ready,
|
||||
the extension shows a setup page and asks the helper to start the AMPB-managed transport
|
||||
after approval.
|
||||
`ampbrowser open <url> --route-aware --launch` creates a desktop convenience profile for
|
||||
clearnet, Tor, and I2P QA. The profile uses a PAC file for `.onion` and `.i2p` routing,
|
||||
installs the AMPB route-helper extension, and starts a token-gated loopback helper tied
|
||||
to the browser process. If a user clicks an alternate-network link before its local
|
||||
transport is ready, the extension shows a setup page and asks the helper to start the
|
||||
AMPB-managed transport after approval. This mode proves routing and first-use setup; it
|
||||
is not the final per-transport storage isolation model.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Test Fixtures
|
||||
|
||||
> Status: draft | Updated 2026-07-05 | Applies to: AMPB and AMPG compatibility testing
|
||||
> Status: draft | Updated 2026-07-11 | Applies to: AMPB and AMPG compatibility testing
|
||||
|
||||
Fixtures should cover static sites, protocol-specific publishing, and interactive web
|
||||
apps without exposing private deployment notes.
|
||||
@@ -13,9 +13,8 @@ HTML improvements that improve alternate renderers.
|
||||
|
||||
## Interactive Fixtures
|
||||
|
||||
`ai-gen-bot` and `medusa-multi-tenant-platform` are stretch fixtures for dynamic behavior:
|
||||
sessions, forms, asset policy, script policy, tenant routing, and transport-specific
|
||||
degradation.
|
||||
Stretch fixtures cover dynamic behavior: sessions, forms, asset policy, script policy,
|
||||
tenant routing, payments, job-style workflows, and transport-specific degradation.
|
||||
|
||||
## Fixture Requirements
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Transport Management
|
||||
|
||||
> Status: draft | Updated 2026-07-08 | Applies to: AMPB transport adapters
|
||||
> Status: draft | Updated 2026-07-11 | Applies to: AMPB transport adapters
|
||||
|
||||
Transport management follows one rule: use what is already healthy, and manage missing
|
||||
transports only after the user opens a URL that needs them and approves setup.
|
||||
@@ -13,9 +13,14 @@ With `--launch --yes`, AMPB can start configured Tor and I2P providers with AMPB
|
||||
state before launching. Transport policy comes from `.ampb/config.toml` or an explicit
|
||||
`--config` path.
|
||||
|
||||
`ampbrowser transport start|status|stop <name>` exposes the same ownership model without
|
||||
launching a browser. It records only AMPB-owned processes and refuses to stop adopted
|
||||
user or system daemons.
|
||||
`ampbrowser transport start|status|stop|repair <name>` exposes the same ownership model
|
||||
without launching a browser. It records only AMPB-owned processes and refuses to stop
|
||||
adopted user or system daemons.
|
||||
|
||||
Transport results include `setup_hint` and `install_command` when AMPB knows how the
|
||||
user can install a missing provider. I2P currently reports Homebrew, apt, dnf, pacman,
|
||||
or pkg guidance where applicable, plus the `AMPB_I2PD_BIN` and
|
||||
`transports.i2p.binary_path` override path.
|
||||
|
||||
Route-aware desktop launches add a local helper process for in-browser setup prompts. The
|
||||
helper accepts token-gated loopback requests from the AMPB profile extension, reports
|
||||
@@ -43,12 +48,24 @@ Managed transports store runtime state under `.ampb/transports/<name>` by defaul
|
||||
leases, tunnels, and daemon logs in that tree are local state and must not be committed.
|
||||
AMPB records owned process metadata in `.ampb/transports/<name>/ampb-owned.json`.
|
||||
|
||||
If a managed transport reports ready but a route is flaky because of stale cache, bad
|
||||
guard selection, or corrupt local daemon state, reset only that transport with:
|
||||
|
||||
```sh
|
||||
python3 -m ampbrowser transport repair tor
|
||||
python3 -m ampbrowser transport start tor
|
||||
```
|
||||
|
||||
`repair` stops an AMPB-owned process when one is recorded, removes
|
||||
`.ampb/transports/<name>`, and leaves `.ampb/profiles/<name>` untouched. It refuses to
|
||||
erase runtime state if AMPB cannot stop a recorded live owned process.
|
||||
|
||||
## Current Adapters
|
||||
|
||||
- Tor: detect SOCKS on `127.0.0.1:9050`; managed start supports Arti, configured `tor`,
|
||||
and system `tor` providers with AMPB-owned state.
|
||||
- I2P: detect HTTP proxy on `127.0.0.1:4444`; managed start supports configured or system
|
||||
`i2pd` providers with AMPB-owned state.
|
||||
- I2P: detect HTTP proxy on `127.0.0.1:4444`; managed start supports configured, bundled,
|
||||
Homebrew, or system `i2pd` providers with AMPB-owned state.
|
||||
- IPFS: detect HTTP gateway on `127.0.0.1:8080`; managed gateway start planned.
|
||||
- Gemini: built-in route and render path; no daemon required for static browsing.
|
||||
- Reticulum: resilient/private route contract exists; adapter and readiness model are planned.
|
||||
|
||||
@@ -126,6 +126,55 @@ class CliOpenTest(unittest.TestCase):
|
||||
helper.assert_called_once()
|
||||
self.assertEqual(9876, helper.call_args.kwargs["watch_pid"])
|
||||
|
||||
def test_transport_command_prints_setup_hint(self) -> None:
|
||||
result = ManagedTransportResult(
|
||||
transport="i2p",
|
||||
provider="-",
|
||||
status="missing-provider",
|
||||
endpoint="http://127.0.0.1:4444",
|
||||
owned=False,
|
||||
pid=0,
|
||||
state_dir=".ampb/transports/i2p",
|
||||
command=(),
|
||||
message="I2P provider not found.",
|
||||
setup_hint="Install i2pd with Homebrew: brew install i2pd.",
|
||||
install_command=("brew", "install", "i2pd"),
|
||||
)
|
||||
|
||||
with patch("ampbrowser.cli.transport_status", return_value=result):
|
||||
with patch.object(sys, "stdout", new_callable=io.StringIO) as stdout:
|
||||
code = main(["transport", "status", "i2p"])
|
||||
|
||||
self.assertEqual(0, code)
|
||||
output = stdout.getvalue()
|
||||
self.assertIn('install_command="brew install i2pd"', output)
|
||||
self.assertIn('setup_hint="Install i2pd with Homebrew: brew install i2pd."', output)
|
||||
|
||||
def test_transport_repair_command_prints_result(self) -> None:
|
||||
result = ManagedTransportResult(
|
||||
transport="tor",
|
||||
provider="arti",
|
||||
status="repaired",
|
||||
endpoint="socks5://127.0.0.1:9050",
|
||||
owned=True,
|
||||
pid=1234,
|
||||
state_dir=".ampb/transports/tor",
|
||||
command=("/tmp/arti", "proxy"),
|
||||
message="removed AMPB-owned tor runtime state",
|
||||
provider_source="bundled-sidecar",
|
||||
)
|
||||
|
||||
with patch("ampbrowser.cli.repair_managed_transport", return_value=result) as repair:
|
||||
with patch.object(sys, "stdout", new_callable=io.StringIO) as stdout:
|
||||
code = main(["transport", "repair", "tor"])
|
||||
|
||||
self.assertEqual(0, code)
|
||||
repair.assert_called_once()
|
||||
output = stdout.getvalue()
|
||||
self.assertIn("status=repaired", output)
|
||||
self.assertIn("provider=arti", output)
|
||||
self.assertIn('message="removed AMPB-owned tor runtime state"', output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -180,10 +180,12 @@ class PrepareOpenTest(unittest.TestCase):
|
||||
background = (extension / "background.js").read_text(encoding="utf-8")
|
||||
setup = (extension / "setup.html").read_text(encoding="utf-8")
|
||||
self.assertIn("webNavigation", manifest)
|
||||
self.assertIn("install_command", background)
|
||||
self.assertIn("http://127.0.0.1:", background)
|
||||
self.assertNotIn("__AMPB_ROUTE_HELPER_URL__", background)
|
||||
self.assertNotIn("__AMPB_ROUTE_HELPER_TOKEN__", background)
|
||||
self.assertIn("Set Up Transport", setup)
|
||||
self.assertIn("install-command", setup)
|
||||
|
||||
def test_execute_open_blocks_when_runtime_is_missing(self) -> None:
|
||||
config = AppConfig(runtime_path="/missing/ampb/firefox", transport_modes={})
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PRIVATE_TARGET_TERMS = (
|
||||
"wowrace",
|
||||
"wowngeon",
|
||||
"smirk-monorepo",
|
||||
"medusa-multi-tenant-platform",
|
||||
"ai-gen-bot",
|
||||
)
|
||||
|
||||
|
||||
class PublicPrivateSplitTest(unittest.TestCase):
|
||||
def test_private_target_names_do_not_appear_in_public_docs(self) -> None:
|
||||
failures = []
|
||||
for path in sorted(ROOT.rglob("*.md")):
|
||||
rel = path.relative_to(ROOT)
|
||||
if rel.parts[0] in {".git", "docs"} and "private" in rel.parts:
|
||||
continue
|
||||
if "docs" in rel.parts and "generated" in rel.parts:
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8").lower()
|
||||
for term in PRIVATE_TARGET_TERMS:
|
||||
if term in text:
|
||||
failures.append(f"{rel}: contains private target term {term!r}")
|
||||
self.assertEqual([], failures)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -51,6 +51,8 @@ class RouteHelperTest(unittest.TestCase):
|
||||
command=("/opt/i2pd",),
|
||||
message="started managed i2pd transport",
|
||||
provider_source="system-package",
|
||||
setup_hint="Install i2pd with Homebrew: brew install i2pd.",
|
||||
install_command=("brew", "install", "i2pd"),
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -64,6 +66,8 @@ class RouteHelperTest(unittest.TestCase):
|
||||
self.assertTrue(response["ok"])
|
||||
self.assertTrue(response["ready"])
|
||||
self.assertEqual("started", response["status"])
|
||||
self.assertEqual("Install i2pd with Homebrew: brew install i2pd.", response["setup_hint"])
|
||||
self.assertEqual(["brew", "install", "i2pd"], response["install_command"])
|
||||
ensure.assert_called_once()
|
||||
|
||||
def test_blocks_unsupported_routes(self) -> None:
|
||||
|
||||
@@ -6,7 +6,7 @@ import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from ampbrowser.config import AppConfig
|
||||
from ampbrowser.transport_manager import ensure_transport_ready, stop_managed_transport, transport_status
|
||||
from ampbrowser.transport_manager import ensure_transport_ready, repair_managed_transport, stop_managed_transport, transport_status
|
||||
from ampbrowser.transports import TransportStatus
|
||||
|
||||
|
||||
@@ -181,6 +181,51 @@ class TransportManagerTest(unittest.TestCase):
|
||||
self.assertEqual("arti", result.provider)
|
||||
self.assertFalse(state_path.exists())
|
||||
|
||||
def test_repair_managed_transport_removes_runtime_state(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
state_dir = root / ".ampb/transports/tor"
|
||||
profile_dir = root / ".ampb/profiles/tor"
|
||||
state_dir.mkdir(parents=True)
|
||||
profile_dir.mkdir(parents=True)
|
||||
state_path = state_dir / "ampb-owned.json"
|
||||
state_path.write_text('{"pid": 1234, "provider": "arti", "endpoint": "socks5://127.0.0.1:9050"}\n', encoding="utf-8")
|
||||
(state_dir / "arti.log").write_text("warn\n", encoding="utf-8")
|
||||
(profile_dir / "user.js").write_text("prefs\n", encoding="utf-8")
|
||||
|
||||
live = [True]
|
||||
|
||||
def fake_alive(pid: int) -> bool:
|
||||
return live[0]
|
||||
|
||||
def fake_kill(pid: int, sig: int) -> None:
|
||||
live[0] = False
|
||||
|
||||
with patch("ampbrowser.transport_manager._pid_alive", side_effect=fake_alive):
|
||||
with patch("ampbrowser.transport_manager.os.kill", side_effect=fake_kill):
|
||||
result = repair_managed_transport("tor", config=AppConfig(transport_modes={}), root=root)
|
||||
|
||||
self.assertEqual("repaired", result.status)
|
||||
self.assertFalse(state_dir.exists())
|
||||
self.assertTrue((profile_dir / "user.js").exists())
|
||||
self.assertIn("runtime state", result.message)
|
||||
|
||||
def test_repair_managed_transport_blocks_when_stop_fails(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
state_dir = root / ".ampb/transports/tor"
|
||||
state_dir.mkdir(parents=True)
|
||||
state_path = state_dir / "ampb-owned.json"
|
||||
state_path.write_text('{"pid": 1234, "provider": "arti", "endpoint": "socks5://127.0.0.1:9050"}\n', encoding="utf-8")
|
||||
|
||||
with patch("ampbrowser.transport_manager._pid_alive", return_value=True):
|
||||
with patch("ampbrowser.transport_manager.os.kill", side_effect=OSError("nope")):
|
||||
result = repair_managed_transport("tor", config=AppConfig(transport_modes={}), root=root)
|
||||
|
||||
self.assertEqual("repair-blocked", result.status)
|
||||
self.assertTrue(state_path.exists())
|
||||
self.assertIn("could not repair", result.message)
|
||||
|
||||
def test_starts_configured_classic_tor_provider_with_ampb_state(self) -> None:
|
||||
status = TransportStatus(
|
||||
transport="tor",
|
||||
@@ -270,14 +315,16 @@ 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)
|
||||
with patch("ampbrowser.transport_manager.sys.platform", "darwin"):
|
||||
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)
|
||||
self.assertFalse(result.ready)
|
||||
self.assertIn("I2P provider not found", result.message)
|
||||
self.assertIn("brew install i2pd", result.message)
|
||||
self.assertIn("brew install i2pd", result.setup_hint)
|
||||
self.assertEqual(("brew", "install", "i2pd"), result.install_command)
|
||||
|
||||
def test_finds_homebrew_i2pd_provider_when_not_on_path(self) -> None:
|
||||
status = TransportStatus(
|
||||
|
||||
@@ -8,6 +8,26 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
IGNORED_PARTS = {".git", "__pycache__", "generated", "private"}
|
||||
LINK_RE = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
|
||||
FORBIDDEN_PUBLIC_TERMS = (
|
||||
"wowrace",
|
||||
"wowngeon",
|
||||
"smirk-monorepo",
|
||||
"medusa-multi-tenant-platform",
|
||||
"ai-gen-bot",
|
||||
)
|
||||
SECRET_PATTERNS = (
|
||||
("private key block", re.compile(r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----")),
|
||||
("open ssh private key block", re.compile(r"-----BEGIN OPENSSH PRIVATE KEY-----")),
|
||||
("aws access key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
|
||||
("github token", re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{20,}\b")),
|
||||
("slack token", re.compile(r"\bxox[baprs]-[A-Za-z0-9-]{10,}\b")),
|
||||
(
|
||||
"assigned secret",
|
||||
re.compile(
|
||||
r"(?i)\b(api[_-]?key|secret|password|token)\s*=\s*['\"][^'\"]{8,}['\"]"
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -17,6 +37,13 @@ def main() -> int:
|
||||
rel = path.relative_to(ROOT)
|
||||
if not _has_status_header(text):
|
||||
failures.append(f"{rel}: missing status header in first six lines")
|
||||
lower_text = text.lower()
|
||||
for term in FORBIDDEN_PUBLIC_TERMS:
|
||||
if term in lower_text:
|
||||
failures.append(f"{rel}: public docs contain private target term {term!r}")
|
||||
for label, pattern in SECRET_PATTERNS:
|
||||
if pattern.search(text):
|
||||
failures.append(f"{rel}: public docs may contain {label}")
|
||||
failures.extend(_broken_links(path, text))
|
||||
if failures:
|
||||
for failure in failures:
|
||||
|
||||
Reference in New Issue
Block a user