Mobile Agentic Browsers: Auto‑Agent AI on Android Chrome WebView and iOS WebKit, Device‑Aware Browser Agent Switcher, and “What Is My Browser Agent” Checks
Agentic browsing is rapidly moving from server-side headless automation to on-device assistants that can read, click, fill, and extract within a real mobile browser. On phones, the rendering engines are not optional: every iOS browser sits atop WebKit; on Android, your in-app browser is a Chromium WebView. That constraint is a gift—if you embrace the platform—and a trap if you try to spoof your identity or bypass the OS’s permission model.
This article is a deep, opinionated guide to building a phone-ready agentic browser that is reliable under bot checks, respectful of privacy, and maintainable in production:
- How to control a mobile browser with CDP/BiDi and native bridges
- How to implement background/permission guards so your auto-agent never clicks “Allow” while the app is backgrounded
- How to align your user agent string with Client Hints (UA-CH) on Android and with WebKit norms on iOS
- How to verify identity using “what is my browser agent” checks—yours and third parties
- How to reduce the security and fraud risk of a mobile browser agent, including content isolation and storage hygiene
The short version: resist the temptation to “be everything everywhere.” Align to the actual device and engine you’re running on, keep UA, UA‑CH, and capability signals coherent, test with multiple “what is my browser agent” sources, and harden your automation with explicit permission policies and app-bound domains.
1) Architecture: What a phone‑ready agentic browser looks like
A minimal but robust agentic browser stack, embedded in a mobile app, typically includes:
- Renderer: Android WebView (Chromium) or iOS WKWebView (WebKit)
- Agent brain: plans, observes, acts; issues navigation, DOM queries, and input events
- Control plane: in-process JS bridges, CDP (Chromium DevTools Protocol), and/or WebDriver BiDi where available
- Policy/guards: permission prompts, background state, domain allowlists, Safe Browsing
- Identity: device-aware UA string, coherent with Client Hints (Android), plus Accept-Language and viewport
- Observability: network and console logs, DOM snapshots, and a “what is my browser agent” self-check page
Opinion: use in-process bridges as your default control channel for privacy and latency; add CDP/BiDi where you need deep capabilities such as network interception, performance tracing, or low-level input synthesis.
2) Device‑aware browser agent switcher (without spoofing yourself into trouble)
A device-aware agent switcher’s job is to consistently set the browser’s identity to match the actual rendering engine and OS, while allowing per-domain tweaks that are honest and justified. Avoid wholesale cross-engine impersonation: it increases bot-detection risk and breaks features.
Key principles:
- Match the engine: On iOS, you are always WebKit. On Android, you are Chromium WebView; Chrome varies by device’s WebView package version.
- Keep UA and UA-CH consistent: On Android, Client Hints (Sec-CH-UA*) are authoritative for modern sites. Do not send fake CH; prefer defaults and let the engine send the correct hints after server opt-in.
- Avoid mixing personas per tab: Switching UAs within the same origin session is fingerprintable. If you must switch, create a new ephemeral WebView/WKWebView instance per persona.
- Adjust only what you can justify: Append an application name to UA on iOS, or a stable, minimal UA template on Android for specific compatibility reasons. Document each deviation.
A practical approach:
- Maintain UA templates by OS major version and engine version. Fill placeholders from native APIs (e.g., Build.VERSION on Android; UIDevice on iOS).
- Scope overrides per domain (or domain wildcard) and reason.
- Treat Accept-Language, viewport, and platform-revealing headers as part of the same identity set.
Pseudo-algorithm:
pseudoselectPersona(context): devInfo = readDeviceInfo() engine = (iOS ? 'WebKit' : 'Chromium') baseUA = defaultEngineUA(engine, devInfo) domain = topLevelDomain(context) if policy.hasOverride(domain): persona = policy.override(domain) // Only allow adjustments consistent with engine + OS ua = adjustUA(baseUA, persona.uaTweaks) else: ua = baseUA return { ua, acceptLanguage: policy.acceptLanguage(devInfo.locale), viewport: deviceViewport(devInfo), storageMode: policy.storage(domain), permissions: policy.permissions(domain), }
The outcome is a coherent identity, with per-domain policies that are explainable and testable.
3) Android: Building an agentic WebView with CDP and guardrails
3.1 WebView setup (Kotlin)
Use the latest androidx.webkit. Enable Safe Browsing. Lock down file/mixed content, and wire permission guards.
kotlinclass AgentWebView(context: Context) : WebView(ContextThemeWrapper(context, R.style.Theme_Material3_DayNight)) { init { settings.javaScriptEnabled = true settings.domStorageEnabled = true settings.databaseEnabled = true settings.mediaPlaybackRequiresUserGesture = false settings.loadWithOverviewMode = true settings.useWideViewPort = true // Identity hygiene: avoid ad-hoc UA spoofing; optionally append app name // settings.userAgentString = settings.userAgentString + " AgentApp/1.0" // Security hardening settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW settings.allowFileAccess = false settings.allowContentAccess = false settings.javaScriptCanOpenWindowsAutomatically = false // Safe Browsing WebViewCompat.setSafeBrowsingEnabled(this, true) // Debugging for dev; disable in production setWebContentsDebuggingEnabled(BuildConfig.DEBUG) webChromeClient = object : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest) { // Guard: deny if app is not in foreground or domain not allowlisted val isForeground = (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager) .appTasks.any { it.taskInfo.topActivity?.packageName == context.packageName } val origin = request.origin.toString() if (!isForeground || !Policy.isAllowed(origin)) { request.deny() return } // Allow granularly val resources = request.resources.filter { r -> when (r) { PermissionRequest.RESOURCE_AUDIO_CAPTURE -> Policy.allowMic(origin) PermissionRequest.RESOURCE_VIDEO_CAPTURE -> Policy.allowCamera(origin) PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID -> false PermissionRequest.RESOURCE_MIDI_SYSEX -> false else -> false } }.toTypedArray() if (resources.isNotEmpty()) request.grant(resources) else request.deny() } } webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { val url = request.url // Allowlist navigation targets return if (Policy.isAllowed(url.host.orEmpty())) false else true } override fun onPageFinished(view: WebView, url: String) { super.onPageFinished(view, url) // Optional: inject agent bridge after load addAgentBridge() } } // JS bridge for agent actions (mind security implications) addJavascriptInterface(AgentBridge(context, this), "AgentBridge") } private fun addAgentBridge() { evaluateJavascript( """ window.Agent = { click: (selector) => { const el = document.querySelector(selector); if (el) el.click(); }, type: (selector, text) => { const el = document.querySelector(selector); if (el) { el.focus(); el.value = text; el.dispatchEvent(new Event('input', {bubbles: true})); } }, getText: (selector) => { const el = document.querySelector(selector); return el ? el.innerText : null; } } """.trimIndent(), null ) } } class AgentBridge(private val context: Context, private val webView: WebView) { @JavascriptInterface fun log(msg: String) { Log.d("Agent", msg) } }
Notes:
- Do not leave WebView debugging enabled in production unless guarded by developer flags.
- If you need per-domain UA, set
settings.userAgentStringbefore first load of that domain in a fresh WebView instance to avoid confusing caches.
3.2 CDP control: connecting to WebView DevTools on-device
Android WebView exposes the Chromium DevTools Protocol (CDP) via a Unix domain socket named webview_devtools_remote_<pid>. You can attach with ADB port forwarding for development and testing.
Steps:
- Enable WebView debugging in your app:
WebView.setWebContentsDebuggingEnabled(true) - Find the WebView process PID (via
adb shell ps | grep your.package) - Forward the devtools socket to a TCP port:
bashadb forward tcp:9222 localabstract:webview_devtools_remote_<PID>
- List targets:
bashcurl http://127.0.0.1:9222/json
- Connect to a page’s
webSocketDebuggerUrland issue CDP commands.
Example Node.js snippet to evaluate JavaScript via CDP Runtime domain:
jsimport fetch from 'node-fetch'; import WebSocket from 'ws'; const targets = await (await fetch('http://127.0.0.1:9222/json')).json(); const page = targets.find(t => t.type === 'page'); const ws = new WebSocket(page.webSocketDebuggerUrl); ws.on('open', () => { let id = 1; const send = (method, params={}) => ws.send(JSON.stringify({ id: id++, method, params })); send('Runtime.enable'); send('Runtime.evaluate', { expression: 'navigator.userAgent' }); }); ws.on('message', (data) => { const msg = JSON.parse(data); if (msg.result?.result?.value) { console.log('UA:', msg.result.result.value); } });
Caveats:
- This technique is for development or controlled test rigs. Do not expose a CDP port in production builds. Instead, prefer in-process bridges and app-to-engine calls.
- WebView’s CDP support may lag behind desktop Chrome; check the WebView version on the device.
3.3 WebDriver BiDi on Android
Chrome’s WebDriver BiDi is maturing (Chrome 120+), and Appium 2+ offers pathways to BiDi for Chrome on Android, but direct BiDi for embedded WebView is still uneven. A pragmatic setup today:
- For Chrome (full browser) on Android devices: use Appium + Chromedriver; where supported, enable BiDi bridge to subscribe to console, network, and log events while running WebDriver actions.
- For in-app WebView: rely on CDP (as above) and/or
addJavascriptInterfacefor agent actions.
Example (Node.js) Appium session for Chrome with BiDi events:
jsimport { remote } from 'webdriverio'; const caps = { platformName: 'Android', 'appium:deviceName': 'Android Device', 'appium:automationName': 'UiAutomator2', browserName: 'Chrome', 'goog:chromeOptions': { args: ['--disable-fre'] } }; const driver = await remote({ hostname: 'localhost', port: 4723, path: '/', capabilities: caps }); // Subscribe to BiDi logs if supported by the driver try { await driver.subscribeToLogs(['browser']); } catch {} await driver.url('https://example.com');
3.4 UA + Client Hints alignment on Android
Chrome is freezing parts of the User-Agent string; modern sites should use UA-CH (User-Agent Client Hints) such as Sec-CH-UA, Sec-CH-UA-Platform, and high-entropy hints when requested. As an app embedding WebView:
- Prefer leaving UA unchanged; rely on WebView to present correct UA and UA-CH.
- Do not synthesize Sec-CH-UA headers yourself; servers can detect misalignment.
- If a target server requires hints, it should send
Accept-CHresponse headers; then the engine will include hints on subsequent requests.
To read UA-CH from page script (for debugging or internal checks):
jsasync function dumpUA() { const ua = navigator.userAgent; const ch = navigator.userAgentData; const high = ch ? await ch.getHighEntropyValues(['platformVersion','fullVersionList','model']) : null; return { ua, brands: ch?.brands, mobile: ch?.mobile, high }; }
Opinion: if you must adjust UA for a specific broken site, keep the change minimal (e.g., append your app identifier) and test that UA-CH remains coherent. Avoid cross-engine impersonation.
3.5 Background and permission guards
Agentic browsers must not grant sensitive permissions when the app is in the background or when the domain is not trusted. On Android, centralize this logic in your WebChromeClient#onPermissionRequest and WebViewClient.
Additional guards:
- Deny mixed content:
settings.mixedContentMode = NEVER_ALLOW - Deny file URL access:
settings.allowFileAccess = false - Restrict navigation to allowlisted domains in
shouldOverrideUrlLoading - Disable third-party cookies if you do not need them:
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, false) - Clear storage after each session if the agent is ephemeral:
WebStorage.getInstance().deleteAllData()andCookieManager.getInstance().removeAllCookies(null)
4) iOS: Building an agentic WKWebView with policy-first controls
4.1 WKWebView setup (Swift)
WKWebView is the only renderer available on iOS. Favor app-bound domains where possible (iOS 14+), and enforce permission decisions in delegates.
swiftimport WebKit import AVFoundation import CoreLocation final class AgentWebViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler { private var webView: WKWebView! private let locationManager = CLLocationManager() override func viewDidLoad() { super.viewDidLoad() let config = WKWebViewConfiguration() config.defaultWebpagePreferences.allowsContentJavaScript = true config.websiteDataStore = .nonPersistent() // ephemeral agent sessions // App-bound domains reduce attack surface (list your domains in Info.plist) if #available(iOS 14.0, *) { config.limitsNavigationsToAppBoundDomains = true } // JS message bridge for agent config.userContentController.add(self, name: "AgentBridge") webView = WKWebView(frame: .zero, configuration: config) webView.uiDelegate = self webView.navigationDelegate = self // Identity: keep UA engine honest; optionally append app name if #available(iOS 9.0, *) { webView.customUserAgent = (webView.value(forKey: "userAgent") as? String ?? "") + " AgentApp/1.0" } view = webView // Inject a simple agent API let source = """ window.Agent = { click: sel => { const el = document.querySelector(sel); if (el) el.click(); }, type: (sel, txt) => { const el = document.querySelector(sel); if (el) { el.focus(); el.value = txt; el.dispatchEvent(new Event('input', {bubbles:true})); } }, getText: sel => { const el = document.querySelector(sel); return el ? el.innerText : null; } }; """ let script = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true) webView.configuration.userContentController.addUserScript(script) // Load a start page if let url = URL(string: "https://example.com") { webView.load(URLRequest(url: url)) } } // JS -> Native bridge func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { if message.name == "AgentBridge", let body = message.body as? String { print("Agent: \(body)") } } // Permission guards for camera/mic (iOS 15+ public API) func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) { let foreground = UIApplication.shared.applicationState == .active let allowed = foreground && Policy.isAllowed(origin.host) decisionHandler(allowed ? .grant : .deny) } // Navigation allowlist func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if let host = navigationAction.request.url?.host, Policy.isAllowed(host) { decisionHandler(.allow) } else { decisionHandler(.cancel) } } }
Notes:
- For geolocation, ensure
NSLocationWhenInUseUsageDescriptionin Info.plist and gate via your ownCLLocationManagerif you need fine-grained control. - For camera/mic, include
NSCameraUsageDescriptionandNSMicrophoneUsageDescription. The delegate shown allows deny/allow with foreground and domain checks. - Use
nonPersistent()data store for ephemeral agents to avoid cross-session tracking.
4.2 Remote debugging and CDP on iOS
iOS uses WebKit’s Remote Inspector protocol. Tools like ios_webkit_debug_proxy translate it into a CDP-like interface so you can use DevTools-like tooling for development.
- Install:
brew install ios-webkit-debug-proxy libimobiledevice - Connect device via USB; run:
ios_webkit_debug_proxyd -dthenios_webkit_debug_proxy -f chrome-devtools://devtools/bundled/inspector.htmlor expose127.0.0.1:9221 - Discover pages:
curl http://127.0.0.1:9221/json
Caveats:
- This is for development; there is no public system API to embed a production-grade CDP server on iOS.
- The mapping is imperfect; not all CDP domains are available.
Opinion: Rely primarily on WKWebView’s in-process bridges and delegates on iOS, and use the remote inspector proxy only for debugging during development.
4.3 UA and Client Hints on iOS
As of iOS 17, Safari/WebKit has limited or experimental UA-CH support; many sites still rely on the classical UA. Do not attempt to inject fake Sec-CH-UA headers. Keep your UA string truthful to WebKit and iOS.
Recommended practice:
- Keep default UA; optionally append an app token via
customUserAgentorapplicationNameForUserAgent. - Maintain consistent Accept-Language and viewport that match the device.
- Validate via “what is my browser agent” checks to see how servers perceive your WKWebView.
5) “What is my browser agent” checks: build-in and cross-verify
An agent’s identity must be verifiable internally and externally. Add a built-in diagnostic page, and cross-check with third-party services.
5.1 Built-in diagnostic page
Host a simple endpoint in your app or a controlled server that echoes headers and JS-observable values.
HTML snippet:
html<!doctype html> <html> <meta charset="utf-8"> <title>Agent Diagnostic</title> <pre id="out"></pre> <script> (async () => { const ua = navigator.userAgent; const ch = navigator.userAgentData; let high = null; try { high = ch ? await ch.getHighEntropyValues(['platformVersion','fullVersionList','model']) : null; } catch {} const headers = await fetch('https://httpbin.org/anything', { method: 'GET' }) .then(r => r.json()) .then(j => j.headers) .catch(() => ({})); const payload = { ua, ch: { brands: ch?.brands, mobile: ch?.mobile, high }, headers }; document.getElementById('out').textContent = JSON.stringify(payload, null, 2); })(); </script> </html>
This produces a coherent snapshot: string UA, UA-CH data visible to JS, and the actual request headers seen by a server.
5.2 Third-party cross-checks
- https://httpbin.org/anything (echoes headers)
- https://deviceatlas.com/device-data/user-agent-tester (manual)
- https://www.whatismybrowser.com/detect/what-is-my-user-agent
- https://tools.keycdn.com/geo (shows headers including Accept-Language)
Best practice: test at least two services plus your internal diagnostic page. If UA says “iPhone; CPU iPhone OS 17” but the headers include Sec-CH-UA-Platform: "Windows", your identity is incoherent and will trigger bot heuristics.
6) Reducing mobile browser agent security risk
Agentic browsers amplify risk: they act quickly, click widely, and can be tricked. Harden them around three axes: identity, permissions, and containment.
6.1 Identity consistency
- Keep UA consistent with engine and OS.
- Do not forge UA-CH. Let the engine send them.
- Align Accept-Language to the device locale and keep it stable per session; avoid flapping locales.
- Keep viewport/device metrics consistent with actual screen DPR and size.
6.2 Permission and background guards
- Never auto-allow prompts when
applicationState != activeor when a domain is not allowlisted. - On Android, gate in
onPermissionRequest; on iOS, gate inrequestMediaCapturePermissionForand use CLLocation for location gating. - Deny or rate-limit
window.openand popups via delegates (javaScriptCanOpenWindowsAutomatically = false).
6.3 Containment
- App-bound domains (iOS): set
limitsNavigationsToAppBoundDomains = trueand enumerate allowed origins. - Domain allowlist (Android): cancel in
shouldOverrideUrlLoadingfor non-allowed hosts. - Ephemeral storage for agents: use
WKWebsiteDataStore.nonPersistent()on iOS; on Android, clear cookies, localStorage, and cache between sessions. - Safe Browsing (Android) and Fraudulent Website Warning (iOS Safari-equivalent isn’t exposed to WKWebView; compensate with your own allowlists and reputation checks).
- CSP and sandboxing: where you control content, inject strict CSP (no inline script, no mixed content) and sandbox if rendering untrusted HTML.
6.4 Network controls and observability
- On Android, use CDP Network domain to log and optionally intercept requests in development; in production, prefer a controlled proxy with explicit rules rather than live-traffic interception.
- Log console errors and important events; sample DOM snapshots when actions fail for replay/debugging.
- Throttle action rates; implement backoff and circuit-breakers when repeated blocks or captchas appear.
6.5 Respect TOS and robots
Agentic browsing in real-user browsers must respect site terms and robots guidance. Use declared endpoints, APIs, and avoid bypassing paywalls or authentication flows you do not own.
7) Putting it together: a minimal auto-agent loop
A typical control loop for an agent embedded in a WebView/WKWebView:
- Plan: given a goal, produce a sequence of steps (navigate, find, click, type)
- Observe: read DOM state, headers, errors; update plan
- Act: perform next step via JS bridge; if needed, use CDP for network tracing or screenshot
- Guard: ensure the app is foregrounded and domain allowed before any action with side effects
- Validate identity: on new sessions, run a silent “what is my browser agent” check and assert coherence
Pseudo-code:
pseudowhile goal not achieved: if not Foreground(): sleep(); continue step = planner.next() if step.type == NAVIGATE: loadUrl(step.url) if step.type == CLICK: js Agent.click(step.selector) if step.type == TYPE: js Agent.type(step.selector, step.text) if step.type == WAIT: await condition or timeout obs = js collectState() if incoherentIdentity(obs): resetSession() planner.update(obs)
8) Android UA templates and why small is beautiful
Android UA strings look like:
Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.185 Mobile Safari/537.36
Recommendations:
- If you must tweak, append your app token at the end:
... Mobile Safari/537.36 AgentApp/1.0. - Avoid changing the device model or Chrome version; those are cross-checked by CDP-exposed capabilities and UA-CH.
- Test that
navigator.userAgentData’sfullVersionListandbrandsreflect Chrome’s version consistent with UA.
9) iOS UA realities
Every WKWebView uses WebKit and reports an iOS UA of the form:
Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1
Adjustments you can safely make:
- Append an app name string using
customUserAgentorapplicationNameForUserAgent. - Avoid claiming to be Chrome; servers can detect engine behavior (e.g., feature availability, quirks) and flag inconsistencies.
10) Practical “what is my browser agent” workflow in CI and on-device
- Include a test that launches a fresh WebView/WKWebView, loads your diagnostic page, and asserts:
- UA contains expected OS and engine tokens
- If Android,
navigator.userAgentData.mobile === truefor phones - If your server sends
Accept-CH, the subsequent request carriesSec-CH-UA-Platformconsistent with Android - Accept-Language matches the device locale you expect
- Run the same on a farm of real devices (Firebase Test Lab, BrowserStack App Automate) to catch OEM-specific WebView variations.
Example minimal JS assertions:
jsconst diag = await dumpUA(); console.assert(/Android\s1[2-5]/.test(diag.ua) || /iPhone OS 1[5-9]/.test(diag.ua), 'Unexpected OS in UA'); if (diag.ch) { console.assert(diag.ch.mobile === true, 'Expected mobile true'); }
11) Troubleshooting incoherent identity
Symptoms and fixes:
- Symptom: Site shows desktop layout on Android phone.
- Check viewport meta; ensure
useWideViewPortand loadWithOverviewMode; ensure UA containsMobiletoken.
- Check viewport meta; ensure
- Symptom: Site blocks with “unsupported browser” on iOS.
- Ensure you did not strip
Safari/604.1token; revert to default UA and only append app name.
- Ensure you did not strip
- Symptom: Server receives
Sec-CH-UA-Platform: "Windows"for your Android app.- You are likely adding CH headers manually; remove them and let WebView handle CH after Accept-CH.
- Symptom: Permission prompts appear while app is backgrounded.
- Implement checks for app foreground state before granting; deny by default.
12) References and helpful specs
- Chromium DevTools Protocol: https://chromedevtools.github.io/devtools-protocol/
- WebDriver BiDi: https://w3c.github.io/webdriver-bidi/
- Android WebView docs: https://developer.android.com/reference/android/webkit/WebView
- AndroidX WebKit: https://developer.android.com/jetpack/androidx/releases/webkit
- WKWebView docs: https://developer.apple.com/documentation/webkit/wkwebview
- App-bound domains (iOS): https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/3654910-limitsnavigationstoappbounddomain
- User-Agent Client Hints: https://wicg.github.io/ua-client-hints/
- MDN: NavigatorUAData: https://developer.mozilla.org/docs/Web/API/NavigatorUAData
- ios_webkit_debug_proxy: https://github.com/google/ios-webkit-debug-proxy
- httpbin for header echo: https://httpbin.org/
Final opinionated takeaways
- Be honest about the engine: don’t pretend your WKWebView is Chrome or your WebView is Safari. The more you spoof, the more bot systems will notice.
- Treat UA, UA-CH, Accept-Language, and viewport as a single identity unit. Keep them coherent and stable per session.
- Prefer in-process JS bridges for action; use CDP/BiDi selectively for deep debugging or observability.
- Default-deny permissions, and never grant sensitive permissions when backgrounded. Build explicit allowlists.
- Make “what is my browser agent” a first-class diagnostic page and a CI test. If you can’t measure your identity, you can’t manage it.
Build your agent like you’d build a browser: with a clear identity, strict policies, and the humility to follow the platform. That is how you survive production reality—and bot defenses—on mobile.