Keep transports in one browser window

This commit is contained in:
jwinterm
2026-07-11 10:20:54 -04:00
parent 2c68c072ba
commit ce874fec3c
20 changed files with 346 additions and 423 deletions
-41
View File
@@ -1,41 +0,0 @@
from __future__ import annotations
from pathlib import Path
from .config import AppConfig
from .launch import OpenPlan, prepare_open
from .open_runner import launch_open_plan
from .routing import route_url
BROKER_TRANSPORTS = {"tor", "i2p"}
class BrokerRouteError(ValueError):
pass
def open_brokered_url(
raw_url: str,
*,
expected_transport: str,
consent: bool,
config: AppConfig,
root: Path,
) -> OpenPlan:
route = route_url(raw_url)
if route.transport != expected_transport:
raise BrokerRouteError(
f"route transport {route.transport} does not match requested transport {expected_transport}"
)
if route.transport not in BROKER_TRANSPORTS:
raise BrokerRouteError(f"{route.transport} is not supported by the desktop broker yet")
open_plan = prepare_open(
route.normalized,
consent=consent,
dry_run=False,
config=config,
platform="desktop",
)
return launch_open_plan(open_plan, config=config, root=root)
+4 -4
View File
@@ -45,7 +45,7 @@ def main(argv: list[str] | None = None) -> int:
"--route-aware",
dest="broker",
action="store_true",
help="Launch a clearnet broker that hands Tor and I2P routes to isolated profiles.",
help="Launch one window with isolated clearnet, Tor, and I2P container tabs.",
)
open_mode = open_parser.add_mutually_exclusive_group()
open_mode.add_argument(
@@ -57,7 +57,7 @@ def main(argv: list[str] | None = None) -> int:
open_mode.add_argument(
"--launch",
action="store_true",
help="Create the isolated profile and launch the bundled browser when the route is ready.",
help="Create AMPB-owned browser state and launch the bundled browser when the route is ready.",
)
shell_parser = subcommands.add_parser("shell", help="Launch the desktop shell flow.")
@@ -354,8 +354,8 @@ def _cmd_fixture(args) -> int:
f"url={check.url} "
f"expected_transport={check.expected_transport} "
f"actual_transport={check.actual_transport} "
f"expected_profile={check.expected_profile} "
f"actual_profile={check.actual_profile} "
f"expected_context={check.expected_context} "
f"actual_context={check.actual_context} "
f"expected_isolation={check.expected_isolation} "
f"actual_isolation={check.actual_isolation} "
f"tier={check.tier} "
+14 -10
View File
@@ -9,7 +9,7 @@ from .interaction_policy import interaction_failures
from .routing import route_url
SUPPORTED_SCHEMAS = {"ampg.fixture-manifest.v1"}
SUPPORTED_SCHEMAS = {"ampg.fixture-manifest.v1", "ampg.fixture-manifest.v2"}
@dataclass(frozen=True)
@@ -21,8 +21,8 @@ class FixtureCheck:
url: str
expected_transport: str
actual_transport: str
expected_profile: str
actual_profile: str
expected_context: str
actual_context: str
expected_isolation: str
actual_isolation: str
tier: str
@@ -79,18 +79,22 @@ def _check_fixture(site_id: str, fixture: Any, path: Path) -> FixtureCheck:
if not isinstance(checks, dict):
raise ValueError(f"{path}: {protocol}: missing checks table")
expected_transport = _string(checks, "transport")
expected_profile = _string(checks, "profile")
expected_isolation = _optional_string(checks, "isolation", "transport-profile")
expected_context = (
_string(checks, "context")
if "context" in checks
else _string(checks, "profile")
)
expected_isolation = _optional_string(checks, "isolation", "transport-context")
interaction = _interaction_policy(fixture)
route_metadata = _route_metadata(fixture)
route = route_url(url)
actual_isolation = "transport-profile" if route.profile == route.transport else "shared-profile"
actual_isolation = "transport-context"
failures: list[str] = []
if route.transport != expected_transport:
failures.append(f"transport expected {expected_transport} got {route.transport}")
if route.profile != expected_profile:
failures.append(f"profile expected {expected_profile} got {route.profile}")
if route.profile != expected_context:
failures.append(f"context expected {expected_context} got {route.profile}")
if actual_isolation != expected_isolation:
failures.append(f"isolation expected {expected_isolation} got {actual_isolation}")
failures.extend(
@@ -112,8 +116,8 @@ def _check_fixture(site_id: str, fixture: Any, path: Path) -> FixtureCheck:
url=route.normalized,
expected_transport=expected_transport,
actual_transport=route.transport,
expected_profile=expected_profile,
actual_profile=route.profile,
expected_context=expected_context,
actual_context=route.profile,
expected_isolation=expected_isolation,
actual_isolation=actual_isolation,
tier=interaction["tier"],
+222 -147
View File
@@ -107,7 +107,7 @@ def prepare_open(
profile_path=config.profile_path(BROKER_PROFILE),
proxy="-",
setup_steps=(),
message="broker entry URLs must use clearnet; alternate routes open in their isolated profile",
message="broker entry URLs must use clearnet; alternate routes continue in isolated container tabs",
broker=True,
)
profile_name = (
@@ -178,7 +178,7 @@ def prepare_open(
profile_path=profile_path,
proxy=proxy,
setup_steps=(),
message="ready to open with isolated profile",
message="ready to open in the one-window broker" if broker else "ready to open with isolated profile",
broker=broker,
launch_spec=launch_spec,
)
@@ -415,6 +415,14 @@ def _prefs_for(browse_plan: BrowsePlan, *, broker: bool = False) -> tuple[str, .
prefs.extend(
[
("network.proxy.type", 0),
("network.http.speculative-parallel-limit", 0),
("network.prefetch-next", False),
("network.dns.disablePrefetchFromHTTPS", True),
("browser.urlbar.speculativeConnect.enabled", False),
("media.peerconnection.enabled", False),
("network.http.http3.enable", False),
("network.trr.mode", 5),
("privacy.userContext.enabled", True),
("extensions.autoDisableScopes", 0),
("extensions.enabledScopes", 5),
("xpinstall.signatures.required", False),
@@ -461,8 +469,6 @@ def _route_helper_extension_assets(extension_path: str) -> tuple[ProfileAsset, .
ProfileAsset(str(base / "setup.html"), _route_helper_setup_html()),
ProfileAsset(str(base / "setup.js"), _route_helper_setup_js()),
ProfileAsset(str(base / "setup.css"), _route_helper_setup_css()),
ProfileAsset(str(base / "handoff.html"), _route_helper_handoff_html()),
ProfileAsset(str(base / "handoff.js"), _route_helper_handoff_js()),
)
@@ -470,22 +476,20 @@ def _route_helper_manifest() -> str:
return json.dumps(
{
"manifest_version": 2,
"name": "AMPB Route Helper",
"name": "AMPB Transport Contexts",
"version": "0.1.0",
"description": "Hands alternate-network links to isolated AMPB transport profiles.",
"applications": {"gecko": {"id": ROUTE_HELPER_EXTENSION_ID}},
"description": "Keeps AMPB transports in isolated, fail-closed container tabs.",
"browser_specific_settings": {
"gecko": {"id": ROUTE_HELPER_EXTENSION_ID, "strict_min_version": "128.0"}
},
"permissions": [
"tabs",
"cookies",
"contextualIdentities",
"proxy",
"webRequest",
"webRequestBlocking",
"http://*.onion/*",
"https://*.onion/*",
"ws://*.onion/*",
"wss://*.onion/*",
"http://*.i2p/*",
"https://*.i2p/*",
"ws://*.i2p/*",
"wss://*.i2p/*",
"<all_urls>",
"http://127.0.0.1/*",
],
"background": {"scripts": ["background.js"]},
@@ -496,86 +500,197 @@ def _route_helper_manifest() -> str:
def _route_helper_background_js() -> str:
return "\n".join(
(
'"use strict";',
"",
f'const HELPER_URL = "{HELPER_URL_PLACEHOLDER}";',
f'const HELPER_TOKEN = "{HELPER_TOKEN_PLACEHOLDER}";',
"const pendingByTab = new Map();",
"",
"function transportForUrl(rawUrl) {",
" try {",
" const parsed = new URL(rawUrl);",
" const host = parsed.hostname.toLowerCase().replace(/\\.$/, '');",
" if (host.endsWith('.onion')) return 'tor';",
" if (host.endsWith('.i2p')) return 'i2p';",
" } catch (_error) {}",
" return '';",
"}",
"",
"async function helper(action, transport, url, consent = false) {",
" const response = await fetch(HELPER_URL, {",
" method: 'POST',",
" headers: {",
" 'Content-Type': 'application/json',",
" 'X-AMPB-Token': HELPER_TOKEN,",
" },",
" body: JSON.stringify({ action, transport, url, consent }),",
" });",
" return response.json();",
"}",
"",
"function internalUrl(page, transport, url, result) {",
" const installCommand = Array.isArray(result.install_command) ? result.install_command.join(' ') : '';",
" const params = new URLSearchParams({",
" transport,",
" url,",
" message: result.message || '',",
" hint: result.setup_hint || '',",
" install: installCommand,",
" });",
" return browser.runtime.getURL(`${page}.html?${params.toString()}`);",
"}",
"",
"async function handoff(details, transport) {",
" let result;",
" try {",
" result = await helper('open', transport, details.url, false);",
" } catch (error) {",
" result = { ok: false, launched: false, message: String(error) };",
" }",
" const page = result.launched ? 'handoff' : 'setup';",
" try {",
" await browser.tabs.update(details.tabId, { url: internalUrl(page, transport, details.url, result) });",
" } finally {",
" pendingByTab.delete(details.tabId);",
" }",
"}",
"",
"function interceptRequest(details) {",
" const transport = transportForUrl(details.url);",
" if (!transport) return {};",
" if (details.type !== 'main_frame' || details.tabId < 0) return { cancel: true };",
" if (pendingByTab.get(details.tabId) === details.url) return { cancel: true };",
" pendingByTab.set(details.tabId, details.url);",
" void handoff(details, transport);",
" return { cancel: true };",
"}",
"",
"browser.webRequest.onBeforeRequest.addListener(",
" interceptRequest,",
" { urls: ['http://*.onion/*', 'https://*.onion/*', 'ws://*.onion/*', 'wss://*.onion/*', 'http://*.i2p/*', 'https://*.i2p/*', 'ws://*.i2p/*', 'wss://*.i2p/*'] },",
" ['blocking']",
");",
"",
"browser.runtime.onMessage.addListener((message) => {",
" if (!message || message.type !== 'ampb-open') return false;",
" return helper('open', message.transport, message.url, message.consent === true);",
"});",
"",
)
tor_adapter = adapter_for("tor")
i2p_adapter = adapter_for("i2p")
tor_host, tor_port = _endpoint_host_port(
tor_adapter.endpoint if tor_adapter else "socks5://127.0.0.1:9050"
)
i2p_host, i2p_port = _endpoint_host_port(
i2p_adapter.endpoint if i2p_adapter else "http://127.0.0.1:4444"
)
lines = (
'"use strict";',
"",
f'const HELPER_URL = "{HELPER_URL_PLACEHOLDER}";',
f'const HELPER_TOKEN = "{HELPER_TOKEN_PLACEHOLDER}";',
f'const TOR_PROXY = {{ host: "{tor_host}", port: {tor_port} }};',
f'const I2P_PROXY = {{ host: "{i2p_host}", port: {i2p_port} }};',
"const CONTAINERS = {",
" clearnet: { name: 'AMPB Clearnet', color: 'blue', icon: 'circle' },",
" tor: { name: 'AMPB Tor', color: 'purple', icon: 'fingerprint' },",
" i2p: { name: 'AMPB I2P', color: 'orange', icon: 'circle' },",
"};",
"const storesByTransport = new Map();",
"const transportsByStore = new Map();",
"const pendingByTab = new Map();",
"const containersReady = initializeContainers();",
"",
"function transportForUrl(rawUrl) {",
" try {",
" const parsed = new URL(rawUrl);",
" const host = parsed.hostname.toLowerCase().replace(/\\.$/, '');",
" if (host.endsWith('.onion')) return 'tor';",
" if (host.endsWith('.i2p')) return 'i2p';",
" if (['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) return 'clearnet';",
" } catch (_error) {}",
" return '';",
"}",
"",
"function hostname(rawUrl) {",
" try { return new URL(rawUrl).hostname.toLowerCase().replace(/\\.$/, ''); } catch (_error) { return ''; }",
"}",
"",
"function transportForStore(cookieStoreId) {",
" return transportsByStore.get(cookieStoreId) || '';",
"}",
"",
"function isHelperRequest(details) {",
" const origin = details.originUrl || details.documentUrl || '';",
" return details.url.startsWith(HELPER_URL) && details.tabId < 0 && origin.startsWith(browser.runtime.getURL(''));",
"}",
"",
"async function initializeContainers() {",
" const identities = await browser.contextualIdentities.query({});",
" for (const [transport, metadata] of Object.entries(CONTAINERS)) {",
" let identity = identities.find((candidate) => candidate.name === metadata.name);",
" if (!identity) identity = await browser.contextualIdentities.create(metadata);",
" storesByTransport.set(transport, identity.cookieStoreId);",
" transportsByStore.set(identity.cookieStoreId, transport);",
" }",
"}",
"",
"async function helper(action, transport, url) {",
" const response = await fetch(HELPER_URL, {",
" method: 'POST',",
" headers: { 'Content-Type': 'application/json', 'X-AMPB-Token': HELPER_TOKEN },",
" body: JSON.stringify({ action, transport, url }),",
" });",
" return response.json();",
"}",
"",
"function setupUrl(mode, transport, url, result) {",
" const installCommand = Array.isArray(result.install_command) ? result.install_command.join(' ') : '';",
" const params = new URLSearchParams({",
" mode, transport, url, message: result.message || '', hint: result.setup_hint || '', install: installCommand,",
" });",
" return browser.runtime.getURL(`setup.html?${params.toString()}`);",
"}",
"",
"function isolationToken(details) {",
" const store = details.cookieStoreId || 'unknown';",
" const firstParty = hostname(details.documentUrl || details.originUrl || details.url) || 'unknown';",
" return `ampb:${store}:${firstParty}`.slice(0, 220);",
"}",
"",
"function torProxy(details) {",
" const token = isolationToken(details);",
" return {",
" type: 'socks', host: TOR_PROXY.host, port: TOR_PROXY.port, proxyDNS: true,",
" username: '<torS0X>0', password: token, connectionIsolationKey: token, failoverTimeout: 1,",
" };",
"}",
"",
"function i2pProxy() {",
" return { type: 'http', host: I2P_PROXY.host, port: I2P_PROXY.port, failoverTimeout: 1 };",
"}",
"",
"function blockedProxy() {",
" return { type: 'http', host: '127.0.0.1', port: 9, failoverTimeout: 1 };",
"}",
"",
"function proxyForRequest(details) {",
" if (isHelperRequest(details)) return { type: 'direct' };",
" if (details.url.startsWith(HELPER_URL)) return blockedProxy();",
" const target = transportForUrl(details.url);",
" const current = transportForStore(details.cookieStoreId);",
" if (target === 'tor' && current !== 'tor') return torProxy(details);",
" if (target === 'i2p' && current !== 'i2p') return i2pProxy();",
" if (current === 'tor') return torProxy(details);",
" if (current === 'i2p') return target === 'i2p' ? i2pProxy() : blockedProxy();",
" return { type: 'direct' };",
"}",
"",
"async function replaceTab(tabId, transport, url) {",
" await containersReady;",
" const oldTab = await browser.tabs.get(tabId);",
" const cookieStoreId = storesByTransport.get(transport);",
" if (!cookieStoreId) throw new Error(`No container for ${transport}`);",
" const newTab = await browser.tabs.create({",
" windowId: oldTab.windowId, index: oldTab.index, active: oldTab.active,",
" pinned: oldTab.pinned, cookieStoreId, url,",
" });",
" if (oldTab.mutedInfo && oldTab.mutedInfo.muted) await browser.tabs.update(newTab.id, { muted: true });",
" await browser.tabs.remove(tabId);",
"}",
"",
"async function routeTopLevel(details, transport, current) {",
" try {",
" await containersReady;",
" if (transport === 'clearnet') {",
" if (!current) await replaceTab(details.tabId, transport, details.url);",
" else await browser.tabs.update(details.tabId, {",
" url: setupUrl('switch', transport, details.url, { message: 'Switching to clearnet can reveal your network address.' }),",
" });",
" return;",
" }",
" const result = await helper('status', transport, details.url);",
" if (result.ready) await replaceTab(details.tabId, transport, details.url);",
" else await browser.tabs.update(details.tabId, { url: setupUrl('setup', transport, details.url, result) });",
" } catch (error) {",
" await browser.tabs.update(details.tabId, {",
" url: setupUrl('error', transport, details.url, { message: String(error) }),",
" });",
" } finally {",
" pendingByTab.delete(details.tabId);",
" }",
"}",
"",
"function interceptRequest(details) {",
" if (isHelperRequest(details)) return {};",
" if (details.url.startsWith(HELPER_URL)) return { cancel: true };",
" const target = transportForUrl(details.url);",
" if (!target) return {};",
" const current = transportForStore(details.cookieStoreId);",
" if (current === target) return {};",
" if (current === 'tor' && target === 'clearnet') return {};",
" if (details.type !== 'main_frame' || details.tabId < 0) return { cancel: true };",
" if (pendingByTab.get(details.tabId) === details.url) return { cancel: true };",
" pendingByTab.set(details.tabId, details.url);",
" void routeTopLevel(details, target, current);",
" return { cancel: true };",
"}",
"",
"browser.proxy.onRequest.addListener(proxyForRequest, { urls: ['<all_urls>'] });",
"browser.webRequest.onBeforeRequest.addListener(",
" interceptRequest, { urls: ['<all_urls>'] }, ['blocking']",
");",
"",
"browser.runtime.onMessage.addListener(async (message, sender) => {",
" if (!message || !sender.tab || sender.tab.id < 0) return false;",
" if (message.type === 'ampb-confirm-switch') {",
" setTimeout(() => void replaceTab(sender.tab.id, message.transport, message.url), 0);",
" return { ok: true, switching: true };",
" }",
" if (message.type !== 'ampb-ensure-and-switch') return false;",
" const result = await helper('ensure', message.transport, message.url);",
" if (result.ready) setTimeout(() => void replaceTab(sender.tab.id, message.transport, message.url), 0);",
" return { ...result, switching: result.ready };",
"});",
"browser.tabs.onRemoved.addListener((tabId) => pendingByTab.delete(tabId));",
"",
"async function normalizeOpenTabs() {",
" await containersReady;",
" const tabs = await browser.tabs.query({});",
" for (const tab of tabs) {",
" if (tab.id < 0 || transportForStore(tab.cookieStoreId)) continue;",
" const transport = transportForUrl(tab.url || '');",
" if (transport === 'clearnet') await replaceTab(tab.id, transport, tab.url);",
" }",
"}",
"void normalizeOpenTabs();",
"",
)
return "\n".join(lines)
def _route_helper_setup_html() -> str:
@@ -591,7 +706,7 @@ def _route_helper_setup_html() -> str:
"</head>",
"<body>",
' <main class="panel">',
' <p class="eyebrow">AMPB Route Helper</p>',
' <p class="eyebrow">AMPB</p>',
' <h1 id="title">Set Up Transport</h1>',
' <p id="body">AMPB needs to start a local transport before opening this route.</p>',
' <pre id="url"></pre>',
@@ -614,6 +729,7 @@ def _route_helper_setup_js() -> str:
'"use strict";',
"",
"const params = new URLSearchParams(window.location.search);",
"const mode = params.get('mode') || 'setup';",
"const transport = params.get('transport') || '';",
"const url = params.get('url') || '';",
"const message = params.get('message') || '';",
@@ -632,21 +748,23 @@ def _route_helper_setup_js() -> str:
" 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.`;",
"const switching = mode === 'switch';",
"document.getElementById('title').textContent = switching ? `Switch to ${label}?` : `Set up ${label}?`;",
"document.getElementById('body').textContent = switching",
" ? `This tab will continue in the isolated ${label} container.`",
" : `AMPB can start a managed ${label} session and continue in an isolated ${label} container tab.`;",
"document.getElementById('url').textContent = url;",
"document.getElementById('message').textContent = hint || message;",
"document.getElementById('approve').textContent = `Start ${label} and open`;",
"document.getElementById('approve').textContent = switching ? `Switch to ${label}` : `Start ${label} and continue`;",
"showInstallCommand(install);",
"",
"document.getElementById('approve').addEventListener('click', async () => {",
" const status = document.getElementById('status');",
" status.textContent = `Starting ${label}...`;",
" const result = await browser.runtime.sendMessage({ type: 'ampb-open', transport, url, consent: true });",
" if (result && result.launched) {",
" status.textContent = `${label} opened in its isolated browser profile.`;",
" const params = new URLSearchParams({ transport, url });",
" window.location.href = browser.runtime.getURL(`handoff.html?${params.toString()}`);",
" status.textContent = switching ? `Switching to ${label}...` : `Starting ${label}...`;",
" const type = switching ? 'ampb-confirm-switch' : 'ampb-ensure-and-switch';",
" const result = await browser.runtime.sendMessage({ type, transport, url });",
" if (result && result.switching) {",
" status.textContent = `Continuing in the ${label} tab...`;",
" return;",
" }",
" if (result && Array.isArray(result.install_command)) {",
@@ -676,49 +794,6 @@ def _route_helper_setup_css() -> str:
)
def _route_helper_handoff_html() -> str:
return "\n".join(
(
"<!doctype html>",
'<html lang="en">',
"<head>",
' <meta charset="utf-8">',
' <meta name="viewport" content="width=device-width, initial-scale=1">',
" <title>Route Opened</title>",
' <link rel="stylesheet" href="setup.css">',
"</head>",
"<body>",
' <main class="panel">',
' <p class="eyebrow">AMPB Transport Broker</p>',
' <h1 id="title">Route opened</h1>',
' <p id="body">The destination is open in its isolated transport profile.</p>',
' <pre id="url"></pre>',
" </main>",
' <script src="handoff.js"></script>',
"</body>",
"</html>",
"",
)
)
def _route_helper_handoff_js() -> str:
return "\n".join(
(
'"use strict";',
"",
"const params = new URLSearchParams(window.location.search);",
"const transport = params.get('transport') || '';",
"const url = params.get('url') || '';",
"const label = transport === 'tor' ? 'Tor' : transport === 'i2p' ? 'I2P' : transport;",
"document.getElementById('title').textContent = `${label} route opened`;",
"document.getElementById('body').textContent = `This destination opened in the isolated ${label} profile. This clearnet profile did not load it.`;",
"document.getElementById('url').textContent = url;",
"",
)
)
def _endpoint_host_port(endpoint: str) -> tuple[str, int]:
parsed = urlparse(endpoint)
return parsed.hostname or "127.0.0.1", parsed.port or 0
+2 -2
View File
@@ -103,7 +103,7 @@ BROWSER_BACKENDS = (
"primary desktop runtime",
"planned",
"desktop",
"bundled Firefox/Gecko-lineage app with AMPB-owned profiles and transports",
"bundled Firefox/Gecko-lineage app with one window and AMPB-owned transport contexts",
"no system browser dependency; track Firefox ESR, Tor Browser, and Mullvad hardening choices",
"Default desktop runtime; users should not need Firefox installed.",
),
@@ -113,7 +113,7 @@ BROWSER_BACKENDS = (
"planned",
"android",
"bundled GeckoView/Fenix/Tor Browser Android-lineage app with foreground transports",
"no system browser dependency; app owns profile, proxy policy, and transport lifecycle",
"no system browser dependency; app owns context, proxy policy, and transport lifecycle",
"Default Android runtime for real mobile AMPB.",
),
BrowserBackend(
+2 -2
View File
@@ -24,8 +24,8 @@ class PlatformCapability:
PLATFORM_CAPABILITIES = (
PlatformCapability("clearnet", "desktop", "ready", "ready", "unsupported", "unsupported", "Use bundled AMPB Gecko desktop runtime with isolated profile."),
PlatformCapability("clearnet", "android", "ready", "ready", "unsupported", "unsupported", "Use bundled AMPB GeckoView Android runtime with isolated profile."),
PlatformCapability("clearnet", "desktop", "ready", "ready", "unsupported", "unsupported", "Use one bundled AMPB Gecko window with isolated transport contexts."),
PlatformCapability("clearnet", "android", "ready", "ready", "unsupported", "unsupported", "Use one bundled GeckoView app with isolated transport sessions."),
PlatformCapability("clearnet", "ios", "ready", "ready", "unsupported", "unsupported", "Use bundled AMPB iOS shell with platform WebKit constraints."),
PlatformCapability("tor", "desktop", "ready", "ready", "planned", "planned", "Adopt local Tor or prompt before managed Arti SOCKS setup."),
PlatformCapability("tor", "android", "planned", "planned", "planned", "planned", "Use an app-owned foreground service; prefer Arti/Tor runtime integration where available."),
-46
View File
@@ -8,9 +8,7 @@ import threading
import time
from typing import Any
from .broker import BrokerRouteError, open_brokered_url
from .config import AppConfig, load_config
from .launch import OpenPlan
from .routing import route_url
from .transport_manager import ManagedTransportResult, ensure_transport_ready, transport_status
@@ -35,24 +33,6 @@ def handle_helper_message(
return _result_response(transport_status(transport, config=config, root=root))
if action == "ensure":
return _result_response(ensure_transport_ready(transport, config=config, root=root))
if action == "open":
raw_url = _string(message.get("url"))
if not raw_url:
return _error_response("missing-url", "broker open requires a URL")
try:
open_plan = open_brokered_url(
raw_url,
expected_transport=transport,
consent=message.get("consent") is True,
config=config,
root=root,
)
except BrokerRouteError as exc:
return _error_response("route-mismatch", str(exc))
transport_result = None
if open_plan.status not in {"launched", "opened-existing"}:
transport_result = transport_status(transport, config=config, root=root)
return _open_response(open_plan, transport_result)
return _error_response("unsupported-action", f"unsupported helper action: {action or '-'}")
@@ -151,32 +131,6 @@ def _result_response(result: ManagedTransportResult) -> dict[str, Any]:
}
def _open_response(
open_plan: OpenPlan,
transport_result: ManagedTransportResult | None,
) -> dict[str, Any]:
route = open_plan.browse_plan.route
opened = open_plan.status in {"launched", "opened-existing"}
return {
"ok": True,
"transport": route.transport,
"profile": route.profile,
"status": open_plan.status,
"ready": opened,
"launched": opened,
"requires_consent": open_plan.status == "consent-required",
"consent_granted": open_plan.consent_granted,
"browser_pid": open_plan.browser_pid,
"profile_path": open_plan.profile_path,
"message": open_plan.message,
"setup_prompt_title": open_plan.setup_prompt_title,
"setup_prompt_body": open_plan.setup_prompt_body,
"setup_prompt_approve_label": open_plan.setup_prompt_approve_label,
"setup_hint": transport_result.setup_hint if transport_result else "-",
"install_command": list(transport_result.install_command) if transport_result else [],
}
def _error_response(status: str, message: str) -> dict[str, Any]:
return {
"ok": False,