diff --git a/README.md b/README.md index bb5b8cd..a269ca9 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ multi-tenant platform. python3 -m ampbrowser route https://wownero.org/ python3 -m ampbrowser route http://example.b32.i2p/ python3 -m ampbrowser route gemini://wownero.org/ +python3 -m ampbrowser route ipfs://bafyexample/ python3 -m ampbrowser inspect python3 -m ampbrowser plan gemini://wownero.org/ python3 -m ampbrowser open https://wownero.org/ diff --git a/ampbrowser/adapters.py b/ampbrowser/adapters.py index 32904b4..f75215a 100644 --- a/ampbrowser/adapters.py +++ b/ampbrowser/adapters.py @@ -106,10 +106,24 @@ ADAPTERS: dict[str, TransportAdapter] = { 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", + note="Reticulum adapter is planned for resilient/private routing; it is not an anonymity layer", commands=("rnsd", "rnstatus", "rnodeconf"), manage_supported=False, ), + "ipfs": TransportAdapter( + name="ipfs", + profile="ipfs", + endpoint="http://127.0.0.1:8080", + adopt_check="HTTP gateway on 127.0.0.1:8080", + managed_state=".ampb/transports/ipfs", + install_strategy="install IPFS/Kubo provider", + start_strategy="start managed IPFS gateway", + stop_policy="stop only AMPB-owned IPFS process", + note="IPFS local gateway for content-addressed distribution; not an anonymity layer", + commands=("ipfs",), + connect_host="127.0.0.1", + connect_port=8080, + ), "gemini": TransportAdapter( name="gemini", profile="gemini", diff --git a/ampbrowser/candidates.py b/ampbrowser/candidates.py index 896e4f9..e6da9ab 100644 --- a/ampbrowser/candidates.py +++ b/ampbrowser/candidates.py @@ -20,7 +20,7 @@ CANDIDATE_TRANSPORTS = ( CandidateTransport( name="ipfs", role="content-addressed web", - status="next-evaluate", + status="active-dry-run", route_examples=("ipfs://", "/ipfs/", "ipns://"), browser_fit="native fetch or local gateway", publisher_fit="static site export and pinning", diff --git a/ampbrowser/metadata.py b/ampbrowser/metadata.py index b4bea4d..924ae28 100644 --- a/ampbrowser/metadata.py +++ b/ampbrowser/metadata.py @@ -23,8 +23,9 @@ class TransportDefinition: ROUTE_RULES = ( RouteRule("*.onion", "tor", "tor", "route through Tor SOCKS"), RouteRule("*.i2p", "i2p", "i2p", "route through I2P HTTP proxy"), + RouteRule("ipfs://*, ipns://*, /ipfs/*, /ipns/*", "ipfs", "ipfs", "route through local IPFS gateway"), RouteRule("gemini://*", "gemini", "gemini", "fetch and render Gemtext"), - RouteRule("rns://*, lxmf://*, nomad://*", "reticulum", "reticulum", "route through Reticulum adapter"), + RouteRule("rns://*, lxmf://*, nomad://*", "reticulum", "reticulum", "route to Reticulum adapter"), RouteRule("http://*, https://*", "clearnet", "clearnet", "ordinary web profile"), ) @@ -43,6 +44,13 @@ TRANSPORT_DEFINITIONS = ( "i2p", "Adopt existing i2pd/I2P router when healthy; otherwise prompt before managed setup.", ), + TransportDefinition( + "ipfs", + "HTTP gateway on 127.0.0.1:8080", + ".ampb/transports/ipfs", + "ipfs", + "Adopt existing IPFS gateway when healthy; otherwise prompt before managed setup.", + ), TransportDefinition( "gemini", "built-in renderer available", @@ -55,6 +63,6 @@ TRANSPORT_DEFINITIONS = ( "RNS tools and configured interfaces", ".ampb/transports/reticulum", "reticulum", - "Adapter planned; physical/link-layer setup may require operator config.", + "Adapter planned for resilient/private routing; physical/link-layer setup may require operator config.", ), ) diff --git a/ampbrowser/platforms.py b/ampbrowser/platforms.py index 61dca7a..6ff32d3 100644 --- a/ampbrowser/platforms.py +++ b/ampbrowser/platforms.py @@ -33,11 +33,14 @@ PLATFORM_CAPABILITIES = ( PlatformCapability("i2p", "desktop", "ready", "ready", "planned", "planned", "Adopt local I2P proxy or prompt before managed desktop daemon setup."), PlatformCapability("i2p", "android", "planned", "planned", "planned", "planned", "Use an app-owned foreground service or compatible installed I2P provider."), PlatformCapability("i2p", "ios", "constrained", "planned", "foreground-only", "constrained", "Treat iOS I2P support as foreground-only until native adapter constraints are proven."), + PlatformCapability("ipfs", "desktop", "ready", "ready", "planned", "planned", "Adopt local IPFS gateway or prompt before managed gateway setup."), + PlatformCapability("ipfs", "android", "planned", "planned", "planned", "planned", "Use gateway-first IPFS browsing; embedded node support needs adapter proof."), + PlatformCapability("ipfs", "ios", "constrained", "planned", "foreground-only", "constrained", "Use gateway-first IPFS browsing; embedded node support is constrained."), PlatformCapability("gemini", "desktop", "ready", "ready", "ready", "unsupported", "Built-in fetch/render path can be shared by desktop shell."), PlatformCapability("gemini", "android", "ready", "ready", "ready", "unsupported", "Built-in fetch/render path can be shared by Android shell."), PlatformCapability("gemini", "ios", "ready", "ready", "ready", "unsupported", "Built-in fetch/render path can be shared by iOS shell."), - PlatformCapability("reticulum", "desktop", "planned", "planned", "planned", "planned", "Adapter and local interface readiness checks are planned."), - PlatformCapability("reticulum", "android", "planned", "planned", "planned", "planned", "Mobile adapter should use explicit foreground service consent."), + PlatformCapability("reticulum", "desktop", "planned", "planned", "planned", "planned", "Resilient/private networking adapter and interface checks are planned."), + PlatformCapability("reticulum", "android", "planned", "planned", "planned", "planned", "Mobile resilient-network adapter should use explicit foreground service consent."), PlatformCapability("reticulum", "ios", "constrained", "planned", "foreground-only", "constrained", "Do not assume background Reticulum operation on iOS."), PlatformCapability("unknown", "desktop", "unsupported", "unsupported", "unsupported", "unsupported", "No platform route is available."), PlatformCapability("unknown", "android", "unsupported", "unsupported", "unsupported", "unsupported", "No platform route is available."), diff --git a/ampbrowser/routing.py b/ampbrowser/routing.py index 6f28930..4730277 100644 --- a/ampbrowser/routing.py +++ b/ampbrowser/routing.py @@ -14,6 +14,10 @@ class Route: def route_url(raw_url: str) -> Route: + path_route = _route_path_address(raw_url) + if path_route: + return path_route + normalized = _normalize_url(raw_url) parsed = urlparse(normalized) scheme = parsed.scheme.lower() @@ -21,6 +25,8 @@ def route_url(raw_url: str) -> Route: if scheme == "gemini": return Route(raw_url, normalized, "gemini", "gemini", "gemini scheme") + if scheme in {"ipfs", "ipns"}: + return Route(raw_url, normalized, "ipfs", "ipfs", f"{scheme} scheme") if scheme in {"rns", "lxmf", "nomad"}: return Route(raw_url, normalized, "reticulum", "reticulum", f"{scheme} scheme") if host.endswith(".onion"): @@ -34,6 +40,19 @@ def route_url(raw_url: str) -> Route: def _normalize_url(raw_url: str) -> str: value = raw_url.strip() - if "://" not in value and not value.startswith(("rns:", "lxmf:")): + if "://" not in value and not value.startswith(("rns:", "lxmf:", "ipfs:", "ipns:")): value = "https://" + value return value + + +def _route_path_address(raw_url: str) -> Route | None: + value = raw_url.strip() + if value.startswith("/ipfs/"): + cid = value.removeprefix("/ipfs/").strip("/") + normalized = f"ipfs://{cid}" if cid else "ipfs://" + return Route(raw_url, normalized, "ipfs", "ipfs", "/ipfs path") + if value.startswith("/ipns/"): + name = value.removeprefix("/ipns/").strip("/") + normalized = f"ipns://{name}" if name else "ipns://" + return Route(raw_url, normalized, "ipfs", "ipfs", "/ipns path") + return None diff --git a/docs/architecture.md b/docs/architecture.md index 5c38548..cc9df82 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,9 +2,9 @@ > Status: draft | Updated 2026-07-05 | Applies to: AMPB contributors and integrators -AMPB is a browser orchestration layer for privacy and alternate-network transports. It -keeps the routing, transport management, and profile isolation decisions outside the -browser engine until those contracts are stable. +AMPB is a browser orchestration layer for privacy, resilient-network, content-addressed, +and alternate-web transports. It keeps the routing, transport management, and profile +isolation decisions outside the browser engine until those contracts are stable. ## Operating Model @@ -20,7 +20,7 @@ browser engine until those contracts are stable. - `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, IPFS, Reticulum, and Gemini. - `ampbrowser.plan`: dry-run action planner for browser launches. - `ampbrowser.launch`: side-effect-free launch specs and first-use consent state. - `ampbrowser.platforms`: platform capability matrix for desktop, Android, and iOS. @@ -31,7 +31,7 @@ browser engine until those contracts are stable. The first implementation should be a launcher and profile manager, not a browser-engine fork. Tor Browser, Mullvad Browser, Firefox profiles, Chromium profiles, terminal Gemini -clients, and future Reticulum-native viewers can all sit behind the same route and +clients, local IPFS gateways, and future Reticulum-native viewers can all sit behind the same route and transport contracts. The long-term browser can become more integrated once the orchestration surface is boring: diff --git a/docs/candidate-transports.md b/docs/candidate-transports.md index ced7f1b..536f0dd 100644 --- a/docs/candidate-transports.md +++ b/docs/candidate-transports.md @@ -2,15 +2,16 @@ > 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. +Candidate transports are tracked from evaluation through active dry-run support. A +candidate should not become an enabled-by-default route 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. +- Treat resilient overlays separately from anonymity networks. - Keep mobile constraints visible before a candidate becomes active. ## Current Matrix diff --git a/docs/configuration.md b/docs/configuration.md index 7d613f3..a5dc4dd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -14,8 +14,9 @@ user opens a URL that needs it and approves the first-use prompt. - Clearnet URLs use a clearnet profile. - `.onion` hosts use Tor through `127.0.0.1:9050` when available. - `.i2p` hosts use I2P through `127.0.0.1:4444` when available. +- `ipfs://`, `ipns://`, `/ipfs/`, and `/ipns/` addresses use a local IPFS gateway when available. - `gemini://` URLs use the Gemini profile. -- Reticulum-family URLs are routed to the Reticulum adapter once configured. +- Reticulum-family URLs use the Reticulum adapter for resilient/private routing once configured. ## Config File @@ -39,6 +40,11 @@ enabled = true mode = "adopt-or-prompt-manage" http_proxy = "127.0.0.1:4444" +[transports.ipfs] +enabled = true +mode = "adopt-or-prompt-manage" +gateway = "127.0.0.1:8080" + [transports.reticulum] enabled = false mode = "adopt" diff --git a/docs/generated/adapters.md b/docs/generated/adapters.md index cf56980..b320be3 100644 --- a/docs/generated/adapters.md +++ b/docs/generated/adapters.md @@ -8,5 +8,6 @@ This file is generated from code. Do not edit it by hand. | --- | --- | --- | --- | --- | --- | --- | | `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 | +| `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 for resilient/private routing; it is not an anonymity layer | +| `ipfs` | `http://127.0.0.1:8080` | HTTP gateway on 127.0.0.1:8080 | install IPFS/Kubo provider | start managed IPFS gateway | stop only AMPB-owned IPFS process | IPFS local gateway for content-addressed distribution; not an anonymity layer | | `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 | diff --git a/docs/generated/candidate-transports.md b/docs/generated/candidate-transports.md index 5e5b6b6..73976ed 100644 --- a/docs/generated/candidate-transports.md +++ b/docs/generated/candidate-transports.md @@ -6,7 +6,7 @@ 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://`, `/ipfs/`, `ipns://` | 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. | +| `ipfs` | content-addressed web | `active-dry-run` | `ipfs://`, `/ipfs/`, `ipns://` | 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:///` | 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://.loki/`, `https://.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. | diff --git a/docs/generated/platform-capabilities.md b/docs/generated/platform-capabilities.md index eea90e0..5ba57a5 100644 --- a/docs/generated/platform-capabilities.md +++ b/docs/generated/platform-capabilities.md @@ -15,11 +15,14 @@ This file is generated from code. Do not edit it by hand. | `i2p` | `desktop` | `ready` | `ready` | `planned` | `planned` | Adopt local I2P proxy or prompt before managed desktop daemon setup. | | `i2p` | `android` | `planned` | `planned` | `planned` | `planned` | Use an app-owned foreground service or compatible installed I2P provider. | | `i2p` | `ios` | `constrained` | `planned` | `foreground-only` | `constrained` | Treat iOS I2P support as foreground-only until native adapter constraints are proven. | +| `ipfs` | `desktop` | `ready` | `ready` | `planned` | `planned` | Adopt local IPFS gateway or prompt before managed gateway setup. | +| `ipfs` | `android` | `planned` | `planned` | `planned` | `planned` | Use gateway-first IPFS browsing; embedded node support needs adapter proof. | +| `ipfs` | `ios` | `constrained` | `planned` | `foreground-only` | `constrained` | Use gateway-first IPFS browsing; embedded node support is constrained. | | `gemini` | `desktop` | `ready` | `ready` | `ready` | `unsupported` | Built-in fetch/render path can be shared by desktop shell. | | `gemini` | `android` | `ready` | `ready` | `ready` | `unsupported` | Built-in fetch/render path can be shared by Android shell. | | `gemini` | `ios` | `ready` | `ready` | `ready` | `unsupported` | Built-in fetch/render path can be shared by iOS shell. | -| `reticulum` | `desktop` | `planned` | `planned` | `planned` | `planned` | Adapter and local interface readiness checks are planned. | -| `reticulum` | `android` | `planned` | `planned` | `planned` | `planned` | Mobile adapter should use explicit foreground service consent. | +| `reticulum` | `desktop` | `planned` | `planned` | `planned` | `planned` | Resilient/private networking adapter and interface checks are planned. | +| `reticulum` | `android` | `planned` | `planned` | `planned` | `planned` | Mobile resilient-network adapter should use explicit foreground service consent. | | `reticulum` | `ios` | `constrained` | `planned` | `foreground-only` | `constrained` | Do not assume background Reticulum operation on iOS. | | `unknown` | `desktop` | `unsupported` | `unsupported` | `unsupported` | `unsupported` | No platform route is available. | | `unknown` | `android` | `unsupported` | `unsupported` | `unsupported` | `unsupported` | No platform route is available. | diff --git a/docs/generated/route-rules.md b/docs/generated/route-rules.md index 92fac5a..58674f7 100644 --- a/docs/generated/route-rules.md +++ b/docs/generated/route-rules.md @@ -8,6 +8,7 @@ This file is generated from code. Do not edit it by hand. | --- | --- | --- | --- | | `*.onion` | `tor` | `tor` | route through Tor SOCKS | | `*.i2p` | `i2p` | `i2p` | route through I2P HTTP proxy | +| `ipfs://*, ipns://*, /ipfs/*, /ipns/*` | `ipfs` | `ipfs` | route through local IPFS gateway | | `gemini://*` | `gemini` | `gemini` | fetch and render Gemtext | -| `rns://*, lxmf://*, nomad://*` | `reticulum` | `reticulum` | route through Reticulum adapter | +| `rns://*, lxmf://*, nomad://*` | `reticulum` | `reticulum` | route to Reticulum adapter | | `http://*, https://*` | `clearnet` | `clearnet` | ordinary web profile | diff --git a/docs/generated/transports.md b/docs/generated/transports.md index ec9d805..64f283b 100644 --- a/docs/generated/transports.md +++ b/docs/generated/transports.md @@ -8,5 +8,6 @@ This file is generated from code. Do not edit it by hand. | --- | --- | --- | --- | --- | | `tor` | SOCKS on 127.0.0.1:9050 | `.ampb/transports/tor` | `tor` | Adopt existing Tor when healthy; otherwise prompt before managed setup. | | `i2p` | HTTP proxy on 127.0.0.1:4444 | `.ampb/transports/i2p` | `i2p` | Adopt existing i2pd/I2P router when healthy; otherwise prompt before managed setup. | +| `ipfs` | HTTP gateway on 127.0.0.1:8080 | `.ampb/transports/ipfs` | `ipfs` | Adopt existing IPFS gateway when healthy; otherwise prompt before managed setup. | | `gemini` | built-in renderer available | `.ampb/transports/gemini` | `gemini` | No daemon required for static Gemtext browsing. | -| `reticulum` | RNS tools and configured interfaces | `.ampb/transports/reticulum` | `reticulum` | Adapter planned; physical/link-layer setup may require operator config. | +| `reticulum` | RNS tools and configured interfaces | `.ampb/transports/reticulum` | `reticulum` | Adapter planned for resilient/private routing; physical/link-layer setup may require operator config. | diff --git a/docs/mobile-browser.md b/docs/mobile-browser.md index 18da052..90897ca 100644 --- a/docs/mobile-browser.md +++ b/docs/mobile-browser.md @@ -8,16 +8,17 @@ mobile shells should implement only the platform-specific browser and transport ## Android -Android is the primary mobile target for full AMPB browsing. Tor, I2P, and Reticulum -adapters should run only after a URL selects that transport and the user approves setup. -Managed adapters must use visible, stoppable foreground services and app-owned state. +Android is the primary mobile target for full AMPB browsing. Tor, I2P, IPFS, and +Reticulum adapters should run only after a URL selects that transport and the user +approves setup. Managed adapters must use visible, stoppable foreground services and +app-owned state. ## iOS iOS is a supported design target, but transport management is constrained. AMPB should -assume foreground-only sessions unless a reviewed platform capability proves otherwise. -The iOS shell should still share routing, policy, consent, fixture, Gemini, and clearnet -behavior with the rest of AMPB. +assume foreground-only sessions for managed transports unless a reviewed platform +capability proves otherwise. The iOS shell should still share routing, policy, consent, +fixture, Gemini, IPFS gateway, and clearnet behavior with the rest of AMPB. ## Capability Matrix diff --git a/docs/open-flow.md b/docs/open-flow.md index 84bfd7d..9f12b52 100644 --- a/docs/open-flow.md +++ b/docs/open-flow.md @@ -8,7 +8,7 @@ first-use setup prompt. ## Consent Model -AMPB does not install or start Tor, I2P, Reticulum, or other optional transports when the +AMPB does not install or start Tor, I2P, IPFS, Reticulum, or other optional transports when the application starts. Setup is lazy: 1. The user opens a URL. diff --git a/docs/transport-management.md b/docs/transport-management.md index 845d064..5b8b583 100644 --- a/docs/transport-management.md +++ b/docs/transport-management.md @@ -32,8 +32,9 @@ leases, tunnels, and daemon logs in that tree are local state and must not be co - Tor: detect SOCKS on `127.0.0.1:9050`; managed start planned. - I2P: detect HTTP proxy on `127.0.0.1:4444`; managed start planned. +- 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: route contract exists; adapter and readiness model are planned. +- Reticulum: resilient/private route contract exists; adapter and readiness model are planned. Generated transport metadata lives in [generated transports](generated/transports.md). Adapter ownership and setup contracts live in [generated adapters](generated/adapters.md). diff --git a/examples/config.toml b/examples/config.toml index f69e684..b6864bc 100644 --- a/examples/config.toml +++ b/examples/config.toml @@ -10,6 +10,10 @@ mode = "adopt-or-prompt-manage" enabled = true mode = "adopt-or-prompt-manage" +[transports.ipfs] +enabled = true +mode = "adopt-or-prompt-manage" + [transports.reticulum] enabled = false diff --git a/tests/test_adapters.py b/tests/test_adapters.py index 5b234ba..afa31f4 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -8,7 +8,7 @@ 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)) + self.assertEqual({"tor", "i2p", "ipfs", "reticulum", "gemini"}, set(ADAPTERS)) def test_gemini_adapter_is_builtin_ready(self) -> None: status = inspect_transport("gemini") @@ -23,7 +23,17 @@ class AdaptersTest(unittest.TestCase): 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")) + self.assertEqual( + ("install Tor provider", "start managed Tor daemon", "wait for socks5://127.0.0.1:9050"), + adapter.setup_steps(installed=False, platform="desktop"), + ) + + def test_ipfs_adapter_declares_gateway_not_anonymity(self) -> None: + adapter = adapter_for("ipfs") + + self.assertIsNotNone(adapter) + self.assertEqual("http://127.0.0.1:8080", adapter.endpoint) + self.assertIn("not an anonymity layer", adapter.note) if __name__ == "__main__": diff --git a/tests/test_candidates.py b/tests/test_candidates.py index d6110db..81e65ba 100644 --- a/tests/test_candidates.py +++ b/tests/test_candidates.py @@ -11,7 +11,7 @@ class CandidateTransportsTest(unittest.TestCase): self.assertIn("ipfs", by_name) self.assertIn("yggdrasil", by_name) - self.assertEqual("next-evaluate", by_name["ipfs"].status) + self.assertEqual("active-dry-run", by_name["ipfs"].status) self.assertEqual("next-evaluate", by_name["yggdrasil"].status) def test_nostr_is_not_classified_as_page_transport(self) -> None: diff --git a/tests/test_launch.py b/tests/test_launch.py index d13b56f..d97a081 100644 --- a/tests/test_launch.py +++ b/tests/test_launch.py @@ -90,6 +90,26 @@ class PrepareOpenTest(unittest.TestCase): plan.setup_steps, ) + def test_ipfs_desktop_setup_uses_gateway_adapter(self) -> None: + status = TransportStatus( + transport="ipfs", + installed=False, + running=False, + endpoint="http://127.0.0.1:8080", + adoptable=False, + manage_supported=True, + note="IPFS local gateway", + ) + + with patch("ampbrowser.plan.inspect_transport", return_value=status): + plan = prepare_open("ipfs://bafyexample", consent=True) + + self.assertEqual("setup-approved", plan.status) + self.assertEqual( + ("install IPFS/Kubo provider", "start managed IPFS gateway", "wait for http://127.0.0.1:8080"), + plan.setup_steps, + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_plan.py b/tests/test_plan.py index 22edd7e..b7a191f 100644 --- a/tests/test_plan.py +++ b/tests/test_plan.py @@ -107,6 +107,24 @@ class PlanUrlTest(unittest.TestCase): self.assertEqual("planned", plan.platform_capability.manage) self.assertTrue(plan.requires_consent) + def test_ipfs_missing_gateway_prompts_before_setup(self) -> None: + status = TransportStatus( + transport="ipfs", + installed=False, + running=False, + endpoint="http://127.0.0.1:8080", + adoptable=False, + manage_supported=True, + note="IPFS local gateway", + ) + + with patch("ampbrowser.plan.inspect_transport", return_value=status): + plan = plan_url("ipfs://bafyexample") + + self.assertEqual("ipfs", plan.route.transport) + self.assertEqual("prompt to install and start managed transport", plan.action) + self.assertTrue(plan.requires_consent) + def test_ios_foreground_only_transport_prompt(self) -> None: status = TransportStatus( transport="tor", diff --git a/tests/test_platforms.py b/tests/test_platforms.py index 85a1034..75884dd 100644 --- a/tests/test_platforms.py +++ b/tests/test_platforms.py @@ -20,6 +20,12 @@ class PlatformsTest(unittest.TestCase): self.assertTrue(capability.can_manage_setup) self.assertIn("foreground", capability.note) + def test_ipfs_android_is_gateway_first_planned(self) -> None: + capability = capability_for("ipfs", "android") + + self.assertEqual("planned", capability.manage) + self.assertIn("gateway-first", capability.note) + def test_unknown_platform_is_rejected(self) -> None: with self.assertRaises(ValueError): normalize_platform("watch") diff --git a/tests/test_routing.py b/tests/test_routing.py index 2f102c1..efcf77e 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -31,6 +31,24 @@ class RouteUrlTest(unittest.TestCase): self.assertEqual("gemini", route.transport) self.assertEqual("gemini", route.profile) + def test_routes_ipfs_scheme(self) -> None: + route = route_url("ipfs://bafyexample") + + self.assertEqual("ipfs", route.transport) + self.assertEqual("ipfs", route.profile) + + def test_routes_ipns_scheme(self) -> None: + route = route_url("ipns://wownero.example") + + self.assertEqual("ipfs", route.transport) + self.assertEqual("ipfs", route.profile) + + def test_routes_ipfs_gateway_path(self) -> None: + route = route_url("/ipfs/bafyexample") + + self.assertEqual("ipfs://bafyexample", route.normalized) + self.assertEqual("ipfs", route.transport) + def test_routes_reticulum_family_scheme(self) -> None: route = route_url("rns://wownero")