ai-dev-browser 0.58.2__tar.gz → 0.58.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/.github/workflows/ci.yml +24 -4
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/PKG-INFO +11 -2
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/README.md +10 -1
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/_recorder.py +27 -13
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/_session.py +1 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/_transport.py +12 -9
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/browser.py +166 -21
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/connection.py +4 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/ext_bridge.py +6 -3
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/extension.py +21 -3
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/port.py +56 -15
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/extension/background.js +15 -4
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/extension/manifest.json +1 -1
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/page_view.js +2 -2
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser.egg-info/PKG-INFO +11 -2
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser.egg-info/SOURCES.txt +10 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser.egg-info/scm_file_list.json +10 -0
- ai_dev_browser-0.58.4/docs/BR-cdp-startup-target-20261024.md +106 -0
- ai_dev_browser-0.58.4/docs/BR-extension-runtime-20261023-resolution.md +117 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/docs/remote-views.md +22 -4
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/pyproject.toml +2 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/conftest.py +113 -7
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/extension_ownership_support.py +18 -0
- ai_dev_browser-0.58.4/tests/integration/extension_runtime_support.py +232 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/extension_window_support.py +60 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/live_cleanup.py +5 -2
- ai_dev_browser-0.58.4/tests/integration/live_resources.py +58 -0
- ai_dev_browser-0.58.4/tests/integration/navigation_startup_support.py +432 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/recording_demo_support.py +150 -5
- ai_dev_browser-0.58.4/tests/integration/scenarios_extension_runtime.json +22 -0
- ai_dev_browser-0.58.4/tests/integration/scenarios_extension_windowless.json +22 -0
- ai_dev_browser-0.58.4/tests/integration/scenarios_navigation_startup.json +78 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/scenarios_recording_demo.json +46 -2
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_browser_start_retry.py +28 -6
- ai_dev_browser-0.58.4/tests/integration/test_discovery_socket_pressure.py +108 -0
- ai_dev_browser-0.58.4/tests/integration/test_extension_runtime_upgrade.py +67 -0
- ai_dev_browser-0.58.4/tests/integration/test_extension_windowless.py +69 -0
- ai_dev_browser-0.58.4/tests/integration/test_navigation_startup.py +141 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_recording_demo.py +44 -2
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_remote_selection_live.py +243 -39
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_remote_view.py +3 -1
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/extension_ownership.test.cjs +65 -0
- ai_dev_browser-0.58.4/tests/unit/test_discovery_budget.py +47 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_lifecycle_handshake_errors.py +91 -5
- ai_dev_browser-0.58.4/tests/unit/test_live_cleanup.py +135 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_window_input_contract.py +8 -0
- ai_dev_browser-0.58.2/tests/integration/navigation_startup_support.py +0 -116
- ai_dev_browser-0.58.2/tests/integration/scenarios_navigation_startup.json +0 -50
- ai_dev_browser-0.58.2/tests/integration/test_navigation_startup.py +0 -81
- ai_dev_browser-0.58.2/tests/unit/test_live_cleanup.py +0 -65
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/.dockerignore +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/.gitattributes +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/.github/workflows/publish.yml +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/.gitignore +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/.gitmodules +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/.pre-commit-config.yaml +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/.secrets.baseline +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/LICENSE +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/__init__.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/_cli.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/_version.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/__init__.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/accessibility.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/animation.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/audits.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/autofill.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/background_service.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/bluetooth_emulation.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/browser.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/cache_storage.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/cast.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/console.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/crash_report_context.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/css.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/debugger.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/device_access.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/device_orientation.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/dom.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/dom_debugger.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/dom_snapshot.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/dom_storage.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/emulation.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/event_breakpoints.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/extensions.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/fed_cm.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/fetch.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/file_system.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/headless_experimental.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/heap_profiler.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/indexed_db.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/input_.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/inspector.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/io.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/layer_tree.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/log.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/media.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/memory.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/network.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/overlay.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/page.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/performance.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/performance_timeline.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/preload.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/profiler.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/pwa.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/py.typed +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/runtime.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/schema.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/security.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/service_worker.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/smart_card_emulation.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/storage.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/system_info.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/target.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/tethering.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/tracing.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/util.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/web_audio.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/web_authn.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/cdp/web_mcp.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/__init__.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/_browser_guidance.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/_case.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/_demo.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/_element.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/_extension_lifecycle.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/_image_cap.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/_js.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/_ref.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/_tab.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/ax.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/cdp.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/chrome.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/cleanup.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/config.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/cookies.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/cookies_import.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/dialog.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/download.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/elements.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/errors.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/human.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/identity.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/login.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/mouse.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/navigation.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/overlays.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/page.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/process.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/recording.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/registry.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/snapshot.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/storage.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/tabs.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/text_match.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/core/window.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/extension/README.md +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/extension/icon128.png +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/extension/icon16.png +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/extension/icon48.png +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/extension/managed-schema.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/pool/__init__.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/pool/job.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/pool/persistence.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/pool/pool.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/pool/worker.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/profile.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/py.typed +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/__init__.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/cdp.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/control.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/existing.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/files.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/mcp-tools.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/page.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/profile_lease.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/provision.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/session.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/vnc.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/window.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/remote/worker.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/__init__.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/_generate.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/browser_cleanup.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/browser_connect.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/browser_disconnect.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/browser_list.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/browser_reconnect.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/browser_restart.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/browser_start.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/browser_stop.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/cdp_send.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/click_by_html_id.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/click_by_ref.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/click_by_text.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/click_by_xpath.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/click_row_by_text.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/cookies_extract_live.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/cookies_extract_offline.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/cookies_import.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/cookies_load.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/cookies_save.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/dialog_respond.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/download.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/download_link.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/drag_by_ref.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/find_by_html_id.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/find_by_text.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/find_by_xpath.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/focus_by_ref.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/highlight_by_ref.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/hover_by_ref.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/html_by_ref.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/js_evaluate.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/login_interactive.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/mouse_click.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/mouse_drag.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/mouse_move.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_discover.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_emulate_focus.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_goto.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_html.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_info.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_pdf.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_record_start.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_record_stop.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_reload.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_screenshot.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_scroll.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_wait_element.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_wait_ready.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/page_wait_url.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/press_key.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/screenshot_by_ref.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/select_by_ref.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/select_text.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/storage_get.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/storage_set.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/tab_close.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/tab_list.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/tab_new.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/tab_switch.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/type_by_ref.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/type_by_text.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/upload_by_ref.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser/tools/window_set.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser.egg-info/dependency_links.txt +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser.egg-info/requires.txt +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/ai_dev_browser.egg-info/top_level.txt +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/deploy/managed-chrome/Dockerfile +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/deploy/managed-chrome/configure_extension.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/docs/BR-real-profile-hidden-input-resolution.md +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/docs/navigation-ownership.md +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/scripts/build_release_assets.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/scripts/live_browser_lifecycle_steering.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/scripts/live_grok_pool_persistence.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/scripts/live_recording_steering.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/scripts/sync_cdp.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/setup.cfg +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/__init__.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/conftest.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/__init__.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/browser_lifecycle_acceptance.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/browser_lifecycle_journeys.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/browser_lifecycle_support.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/browser_lifecycle_ui.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/cli_steering_acceptance.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/cli_steering_support.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/native_window_occluder.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/pdf_readiness_support.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/pool-persistence-acceptance.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/recording_demo_acceptance.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/run_managed_extension.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/scenarios_browser_lifecycle.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/scenarios_cli_extension.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/scenarios_cli_steering.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/scenarios_extension_ownership.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/scenarios_extension_ownership_native.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/scenarios_extension_window.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/scenarios_extension_window_previous.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/scenarios_pdf_readiness.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/scenarios_public_web.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/scenarios_recording.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/scenarios_workspace.json +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_background_processes.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_browser_control_live.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_browser_files_live.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_browser_inventory.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_browser_lifecycle.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_browser_session_live.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_browser_worker_live.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_cdp_send_warning.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_ci_integration.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_cli_extension_workflows.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_cli_steering.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_cli_unicode_help.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_click_robust.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_config_resolvers.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_cookies_extract_hermetic.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_cookies_extract_live.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_cookies_extract_windows.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_cookies_import.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_dialog_auto.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_dialog_respond.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_dom_discovery.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_download_link.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_download_link_frame.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_existing_worker_live.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_ext_bridge.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_extension_ownership.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_extension_ownership_native.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_extension_window_native.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_extension_window_previous.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_find_and_interact_workflows.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_image_cap.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_js_contract.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_js_evaluate_frame.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_locator_workflows.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_mouse_click.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_mouse_drag_move.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_multi_profile_pool.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_page_discover.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_page_goto.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_page_pdf.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_page_reload.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_page_scroll_verify.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_page_wait_element.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_pdf_readiness.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_pool_model_persistence.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_port_detection.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_press_key.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_proxy_identity.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_public_web_workflows.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_recording_workflows.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_remote_vnc.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_remote_window_live.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_screenshot_coords.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_scroll_embedded_target.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_select_text.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_startup_timeout.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_stealth_defaults.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_tab_close.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_tab_url_matching.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_text_locator.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_timeout_and_retry.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_trusted_click.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_type_verified_fill.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_viewport.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_worker_managed_status_live.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/integration/test_workspace_workflows.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_browser_control.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_build_info.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_case.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_cdp_coerce.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_cdp_session_scoped.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_cdp_unbound.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_cli_docstring_args.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_cli_error_envelope.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_cli_param_type.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_cli_steering_contract.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_demo_encoding.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_extension_ownership.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_find_hint.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_launch_window_size.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_no_acronym_rot.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_pdf_units.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_recording.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_release_version.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_remote_files.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_remote_navigation.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_session_failure_state.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_tool_registry.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/tests/unit/test_worker_lifecycle_status.py +0 -0
- {ai_dev_browser-0.58.2 → ai_dev_browser-0.58.4}/uv.lock +0 -0
|
@@ -167,13 +167,13 @@ jobs:
|
|
|
167
167
|
env:
|
|
168
168
|
AI_DEV_BROWSER_CHROME: ${{ steps.chrome.outputs.chrome-path }}
|
|
169
169
|
AI_DEV_BROWSER_TEST_EXTENSION_CHROME: ${{ steps.chrome.outputs.chrome-path }}
|
|
170
|
-
run: xvfb-run -a -s "-screen 0 1280x1000x24" python -m pytest -v --full-trace -o faulthandler_timeout=180 tests/integration/test_remote_view.py tests/integration/test_remote_selection_live.py tests/integration/test_remote_vnc.py tests/integration/test_browser_control_live.py tests/integration/test_browser_session_live.py tests/integration/test_browser_files_live.py tests/integration/test_browser_worker_live.py tests/integration/test_existing_worker_live.py tests/integration/test_navigation_startup.py --basetemp remote-results
|
|
170
|
+
run: xvfb-run -a -s "-screen 0 1280x1000x24" python -m pytest -v --full-trace -o faulthandler_timeout=180 tests/integration/test_remote_view.py tests/integration/test_discovery_socket_pressure.py tests/integration/test_remote_selection_live.py tests/integration/test_remote_vnc.py tests/integration/test_browser_control_live.py tests/integration/test_browser_session_live.py tests/integration/test_browser_files_live.py tests/integration/test_browser_worker_live.py tests/integration/test_existing_worker_live.py tests/integration/test_navigation_startup.py --basetemp remote-results
|
|
171
171
|
- name: Run page and extension workflows on Windows
|
|
172
172
|
if: runner.os == 'Windows'
|
|
173
173
|
env:
|
|
174
174
|
AI_DEV_BROWSER_CHROME: ${{ steps.chrome.outputs.chrome-path }}
|
|
175
175
|
AI_DEV_BROWSER_TEST_EXTENSION_CHROME: ${{ steps.chrome.outputs.chrome-path }}
|
|
176
|
-
run: python -m pytest -v --full-trace -o faulthandler_timeout=180 tests/integration/test_remote_view.py tests/integration/test_remote_selection_live.py tests/integration/test_browser_control_live.py tests/integration/test_browser_session_live.py tests/integration/test_browser_files_live.py tests/integration/test_browser_worker_live.py tests/integration/test_existing_worker_live.py tests/integration/test_navigation_startup.py --basetemp remote-results
|
|
176
|
+
run: python -m pytest -v --full-trace -o faulthandler_timeout=180 tests/integration/test_remote_view.py tests/integration/test_discovery_socket_pressure.py tests/integration/test_remote_selection_live.py tests/integration/test_browser_control_live.py tests/integration/test_browser_session_live.py tests/integration/test_browser_files_live.py tests/integration/test_browser_worker_live.py tests/integration/test_existing_worker_live.py tests/integration/test_navigation_startup.py --basetemp remote-results
|
|
177
177
|
- name: Retain real viewer evidence
|
|
178
178
|
if: always()
|
|
179
179
|
uses: actions/upload-artifact@v4
|
|
@@ -193,7 +193,7 @@ jobs:
|
|
|
193
193
|
run: docker build -f deploy/managed-chrome/Dockerfile --secret "id=extension_key,src=$RUNNER_TEMP/extension-fixture.pem" --build-arg "SOURCE_REVISION=$GITHUB_SHA" -t adb-managed-chrome:ci .
|
|
194
194
|
- name: Run real signed extension, handoff and viewer workflows
|
|
195
195
|
run: |
|
|
196
|
-
docker create --name "adb-managed-live-$GITHUB_RUN_ID" --memory=1536m --shm-size=256m --pids-limit=384 \
|
|
196
|
+
docker create --init --name "adb-managed-live-$GITHUB_RUN_ID" --memory=1536m --shm-size=256m --pids-limit=384 \
|
|
197
197
|
--mount "type=bind,source=$PWD/tests,target=/opt/browser/tests,readonly" \
|
|
198
198
|
adb-managed-chrome:ci /bin/sleep infinity
|
|
199
199
|
docker start "adb-managed-live-$GITHUB_RUN_ID"
|
|
@@ -250,7 +250,7 @@ jobs:
|
|
|
250
250
|
- name: Run the real extension on both WebSocket server implementations
|
|
251
251
|
env:
|
|
252
252
|
AI_DEV_BROWSER_TEST_EXTENSION_CHROME: ${{ steps.chrome.outputs.chrome-path }}
|
|
253
|
-
run: uv run --no-sync python -m pytest -v --full-trace -o faulthandler_timeout=180 tests/integration/test_cli_extension_workflows.py tests/integration/test_recording_workflows.py tests/integration/test_recording_demo.py tests/integration/test_browser_lifecycle.py tests/integration/test_remote_view.py tests/integration/test_extension_ownership.py tests/integration/test_cli_steering.py -k "real_extension or outbound_proxy" --basetemp extension-results
|
|
253
|
+
run: uv run --no-sync python -m pytest -v --full-trace -o faulthandler_timeout=180 tests/integration/test_cli_extension_workflows.py tests/integration/test_recording_workflows.py tests/integration/test_recording_demo.py tests/integration/test_browser_lifecycle.py tests/integration/test_remote_view.py tests/integration/test_extension_ownership.py tests/integration/test_cli_steering.py tests/integration/test_extension_windowless.py -k "real_extension or outbound_proxy" --basetemp extension-results
|
|
254
254
|
|
|
255
255
|
- name: Install the unchanged released extension fixture
|
|
256
256
|
run: uv pip install --target previous-extension-fixture --no-deps ai-dev-browser==0.57.7
|
|
@@ -261,6 +261,15 @@ jobs:
|
|
|
261
261
|
AI_DEV_BROWSER_TEST_PREVIOUS_EXTENSION: ${{ github.workspace }}/previous-extension-fixture/ai_dev_browser/extension
|
|
262
262
|
run: uv run --no-sync python -m pytest -v tests/integration/test_extension_window_previous.py --basetemp previous-extension-results
|
|
263
263
|
|
|
264
|
+
- name: Install the released extension without version reporting
|
|
265
|
+
run: uv pip install --target unknown-extension-fixture --no-deps ai-dev-browser==0.54.0
|
|
266
|
+
|
|
267
|
+
- name: Recover an unknown extension runtime through an ordinary reload
|
|
268
|
+
env:
|
|
269
|
+
AI_DEV_BROWSER_TEST_EXTENSION_CHROME: ${{ steps.chrome.outputs.chrome-path }}
|
|
270
|
+
AI_DEV_BROWSER_TEST_UNKNOWN_EXTENSION: ${{ github.workspace }}/unknown-extension-fixture/ai_dev_browser/extension
|
|
271
|
+
run: uv run --no-sync python -m pytest -v tests/integration/test_extension_runtime_upgrade.py --basetemp runtime-upgrade-results
|
|
272
|
+
|
|
264
273
|
- name: Honor genuine Windows window visibility before input
|
|
265
274
|
if: runner.os == 'Windows'
|
|
266
275
|
env:
|
|
@@ -300,13 +309,24 @@ jobs:
|
|
|
300
309
|
extension-results/**/extension-lifecycle.png
|
|
301
310
|
extension-results/**/installed-extension.png
|
|
302
311
|
extension-results/**/extension-startup-evidence.json
|
|
312
|
+
extension-results/**/windowless-startup-order.json
|
|
313
|
+
extension-results/**/windowless-extension-accepted.png
|
|
303
314
|
extension-results/**/extension-chrome.log
|
|
315
|
+
extension-results/**/host-resources-*.json
|
|
316
|
+
extension-results/**/navigation-readiness.json
|
|
304
317
|
extension-results/**/extension-cleanup-stacks.txt
|
|
305
318
|
native-ownership-results/**
|
|
306
319
|
native-window-results/**/native-window-evidence.json
|
|
307
320
|
native-window-results/**/native-window-accepted.png
|
|
308
321
|
native-window-results/**/extension-chrome.log
|
|
322
|
+
native-window-results/**/host-resources-*.json
|
|
323
|
+
native-window-results/**/extension-cleanup-evidence.json
|
|
324
|
+
native-window-results/**/extension-cleanup-stacks.txt
|
|
309
325
|
previous-extension-results/**/previous-extension-evidence.json
|
|
326
|
+
runtime-upgrade-results/**/runtime-upgrade-evidence.json
|
|
327
|
+
runtime-upgrade-results/**/runtime-upgrade-accepted.png
|
|
328
|
+
runtime-upgrade-results/**/extension-*.json
|
|
329
|
+
runtime-upgrade-results/**/extension-chrome.log
|
|
310
330
|
|
|
311
331
|
# ─────────────────────────────────────────────────────────────────────
|
|
312
332
|
# CI-integration dogfood: prove the documented "consumer's CI" recipe
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ai-dev-browser
|
|
3
|
-
Version: 0.58.
|
|
3
|
+
Version: 0.58.4
|
|
4
4
|
Summary: A browser for AI to develop web automation — human-like automation that works seamlessly in a world designed for humans
|
|
5
5
|
Author: sudoprivacy
|
|
6
6
|
License: AGPL-3.0
|
|
@@ -94,6 +94,11 @@ python -m ai_dev_browser.tools.browser_disconnect --session-id <returned-id>
|
|
|
94
94
|
python -m ai_dev_browser.tools.browser_reconnect --session-id <returned-id>
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
Fresh CDP launches prepare a blank owned page before navigating once to the
|
|
98
|
+
requested URL, and return that exact page. If page
|
|
99
|
+
creation is uncertain, Chrome remains running; connect to the reported port and
|
|
100
|
+
inspect its pages before continuing.
|
|
101
|
+
|
|
97
102
|
For extension startup, first install and enable the bundled bridge in the chosen
|
|
98
103
|
Chrome profile. Select `--chrome-path`, `--user-data-dir`, `--profile-directory`
|
|
99
104
|
and `--headless` explicitly as needed. Startup adds neither a debugging port nor
|
|
@@ -453,11 +458,15 @@ and [GitHub Releases](https://github.com/sudoprivacy/ai-dev-browser/releases).
|
|
|
453
458
|
Each GitHub release includes the same wheel and source distribution sent to PyPI,
|
|
454
459
|
the bundled Chrome extension as an unpackable ZIP, and `SHA256SUMS`.
|
|
455
460
|
For extension mode, extract the ZIP and load its directory in `chrome://extensions`.
|
|
456
|
-
After upgrading, reload the extension
|
|
461
|
+
After upgrading, ordinarily reload the extension in the intended Chrome profile,
|
|
462
|
+
then reconnect with `browser_connect --transport extension`.
|
|
457
463
|
`browser_connect` reports the actual loaded `extension_version`, the
|
|
458
464
|
`bundled_extension_version`, and whether they match. Updating Python alone does
|
|
459
465
|
not reload Chrome's extension. Reload can reset control; inspect the session and
|
|
460
466
|
its owned page before resuming input.
|
|
467
|
+
`extension_warning` is non-null when the loaded version is unknown or differs
|
|
468
|
+
from the bundle. Follow its steps before input; `connected=true` confirms a
|
|
469
|
+
connection. Version matching and actual page visibility are separate checks.
|
|
461
470
|
|
|
462
471
|
For a hidden extension page, `window_set --state normal --focus` requests real
|
|
463
472
|
window restoration and reports observed `visibility`, `focused`, and
|
|
@@ -52,6 +52,11 @@ python -m ai_dev_browser.tools.browser_disconnect --session-id <returned-id>
|
|
|
52
52
|
python -m ai_dev_browser.tools.browser_reconnect --session-id <returned-id>
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
Fresh CDP launches prepare a blank owned page before navigating once to the
|
|
56
|
+
requested URL, and return that exact page. If page
|
|
57
|
+
creation is uncertain, Chrome remains running; connect to the reported port and
|
|
58
|
+
inspect its pages before continuing.
|
|
59
|
+
|
|
55
60
|
For extension startup, first install and enable the bundled bridge in the chosen
|
|
56
61
|
Chrome profile. Select `--chrome-path`, `--user-data-dir`, `--profile-directory`
|
|
57
62
|
and `--headless` explicitly as needed. Startup adds neither a debugging port nor
|
|
@@ -411,11 +416,15 @@ and [GitHub Releases](https://github.com/sudoprivacy/ai-dev-browser/releases).
|
|
|
411
416
|
Each GitHub release includes the same wheel and source distribution sent to PyPI,
|
|
412
417
|
the bundled Chrome extension as an unpackable ZIP, and `SHA256SUMS`.
|
|
413
418
|
For extension mode, extract the ZIP and load its directory in `chrome://extensions`.
|
|
414
|
-
After upgrading, reload the extension
|
|
419
|
+
After upgrading, ordinarily reload the extension in the intended Chrome profile,
|
|
420
|
+
then reconnect with `browser_connect --transport extension`.
|
|
415
421
|
`browser_connect` reports the actual loaded `extension_version`, the
|
|
416
422
|
`bundled_extension_version`, and whether they match. Updating Python alone does
|
|
417
423
|
not reload Chrome's extension. Reload can reset control; inspect the session and
|
|
418
424
|
its owned page before resuming input.
|
|
425
|
+
`extension_warning` is non-null when the loaded version is unknown or differs
|
|
426
|
+
from the bundle. Follow its steps before input; `connected=true` confirms a
|
|
427
|
+
connection. Version matching and actual page visibility are separate checks.
|
|
419
428
|
|
|
420
429
|
For a hidden extension page, `window_set --state normal --focus` requests real
|
|
421
430
|
window restoration and reports observed `visibility`, `focused`, and
|
|
@@ -17,6 +17,7 @@ import websockets
|
|
|
17
17
|
from PIL import GifImagePlugin, Image, ImageChops, ImageOps
|
|
18
18
|
|
|
19
19
|
from ._demo import Overlay, index_path
|
|
20
|
+
from ._transport import _local_websocket_options
|
|
20
21
|
from .errors import RecordingError
|
|
21
22
|
from .recording import _read_json, _write_json
|
|
22
23
|
|
|
@@ -212,7 +213,11 @@ class _Capture:
|
|
|
212
213
|
|
|
213
214
|
async def _record(folder: Path, config: dict, partial: Path) -> dict:
|
|
214
215
|
async with websockets.connect(
|
|
215
|
-
config["url"],
|
|
216
|
+
config["url"],
|
|
217
|
+
max_size=16 * 1024 * 1024,
|
|
218
|
+
open_timeout=5,
|
|
219
|
+
close_timeout=2,
|
|
220
|
+
**_local_websocket_options(config["url"]),
|
|
216
221
|
) as ws:
|
|
217
222
|
capture = _Capture(ws)
|
|
218
223
|
receiver = asyncio.create_task(capture.receive())
|
|
@@ -273,13 +278,23 @@ async def _record(folder: Path, config: dict, partial: Path) -> dict:
|
|
|
273
278
|
source, timestamp, metadata or capture.metadata
|
|
274
279
|
)
|
|
275
280
|
|
|
281
|
+
health_metrics = None
|
|
282
|
+
|
|
283
|
+
async def sample_health():
|
|
284
|
+
nonlocal health_metrics
|
|
285
|
+
while True:
|
|
286
|
+
# A quiet page emits no frames. Check target liveness on
|
|
287
|
+
# its own clock: a slow RPC must not stop cursor animation.
|
|
288
|
+
health_metrics = await capture.command("Page.getLayoutMetrics")
|
|
289
|
+
await asyncio.sleep(0.2)
|
|
290
|
+
|
|
291
|
+
tasks.append(asyncio.create_task(sample_health()))
|
|
276
292
|
with partial.open("xb") as output:
|
|
277
293
|
gif = _Gif(output)
|
|
278
294
|
data, started = capture.latest
|
|
279
295
|
await asyncio.to_thread(gif.push, render(data, started), started)
|
|
280
296
|
last_frame = started
|
|
281
297
|
last_received = started
|
|
282
|
-
last_health = 0.0
|
|
283
298
|
last_state = 0.0
|
|
284
299
|
while True:
|
|
285
300
|
if (folder / "abort").exists():
|
|
@@ -289,30 +304,29 @@ async def _record(folder: Path, config: dict, partial: Path) -> dict:
|
|
|
289
304
|
for task in tasks:
|
|
290
305
|
if task.done():
|
|
291
306
|
task.result()
|
|
292
|
-
now = time.monotonic()
|
|
293
307
|
requested_viewport = _read_json(folder / "viewport.json")
|
|
294
308
|
if requested_viewport != viewport:
|
|
295
309
|
await capture.command(
|
|
296
310
|
"Emulation.setDeviceMetricsOverride", requested_viewport
|
|
297
311
|
)
|
|
298
312
|
viewport = requested_viewport
|
|
299
|
-
if
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
)
|
|
303
|
-
if now - last_health >= 0.2:
|
|
304
|
-
# Quiet pages legitimately emit no frames. Probe the target
|
|
305
|
-
# separately so a detached extension cannot look like idle.
|
|
306
|
-
metrics = await capture.command("Page.getLayoutMetrics")
|
|
307
|
-
actual = metrics["cssLayoutViewport"]
|
|
313
|
+
if health_metrics is not None:
|
|
314
|
+
actual = health_metrics["cssLayoutViewport"]
|
|
315
|
+
health_metrics = None
|
|
308
316
|
if (actual["clientWidth"], actual["clientHeight"]) != (
|
|
309
317
|
viewport["width"],
|
|
310
318
|
viewport["height"],
|
|
311
319
|
):
|
|
320
|
+
# Keep viewport writes on this one task, including
|
|
321
|
+
# explicit resize requests and navigation recovery.
|
|
312
322
|
await capture.command(
|
|
313
323
|
"Emulation.setDeviceMetricsOverride", viewport
|
|
314
324
|
)
|
|
315
|
-
|
|
325
|
+
now = time.monotonic()
|
|
326
|
+
if now - started >= config["max_duration"]:
|
|
327
|
+
raise RecordingError(
|
|
328
|
+
"Recording reached max_duration before stop; no GIF published"
|
|
329
|
+
)
|
|
316
330
|
if now - last_state >= 0.5:
|
|
317
331
|
_write_json(
|
|
318
332
|
folder / "state.json",
|
|
@@ -163,6 +163,17 @@ def _cdp_get_module(domain: str | types.ModuleType):
|
|
|
163
163
|
return importlib.import_module(domain)
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
def _local_websocket_options(url: str) -> dict:
|
|
167
|
+
"""Keep loopback control direct on proxy-aware websockets versions."""
|
|
168
|
+
if _is_loopback_host(urlsplit(url).hostname or "") and (
|
|
169
|
+
"proxy" in inspect.signature(websockets.connect).parameters
|
|
170
|
+
):
|
|
171
|
+
# websockets 15+ discovers environment proxies by default. Older
|
|
172
|
+
# supported versions have no proxy parameter and connect directly.
|
|
173
|
+
return {"proxy": None}
|
|
174
|
+
return {}
|
|
175
|
+
|
|
176
|
+
|
|
166
177
|
class CDPConnection:
|
|
167
178
|
"""Async WebSocket connection to a CDP endpoint.
|
|
168
179
|
|
|
@@ -189,19 +200,11 @@ class CDPConnection:
|
|
|
189
200
|
"""Open WebSocket and start listener."""
|
|
190
201
|
if self._websocket and not self._websocket.close_code:
|
|
191
202
|
return # already connected
|
|
192
|
-
options = {}
|
|
193
|
-
if _is_loopback_host(urlsplit(self.websocket_url).hostname or "") and (
|
|
194
|
-
"proxy" in inspect.signature(websockets.connect).parameters
|
|
195
|
-
):
|
|
196
|
-
# websockets 15+ discovers environment proxies by default. Local
|
|
197
|
-
# Chrome/extension control is direct; older supported versions have
|
|
198
|
-
# no proxy parameter and already connect directly.
|
|
199
|
-
options["proxy"] = None
|
|
200
203
|
self._websocket = await websockets.connect(
|
|
201
204
|
self.websocket_url,
|
|
202
205
|
ping_timeout=PING_TIMEOUT,
|
|
203
206
|
max_size=MAX_SIZE,
|
|
204
|
-
**
|
|
207
|
+
**_local_websocket_options(self.websocket_url),
|
|
205
208
|
)
|
|
206
209
|
self._listener_task = asyncio.ensure_future(self._listener())
|
|
207
210
|
await self._register_handlers()
|
|
@@ -144,7 +144,9 @@ def _resolve_do_match(
|
|
|
144
144
|
return env_match if env_match is not None else _proxy_in_args(extra_args)
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
async def _read_navigator_language(
|
|
147
|
+
async def _read_navigator_language(
|
|
148
|
+
port: int, target_id: str | None = None
|
|
149
|
+
) -> str | None:
|
|
148
150
|
"""The locale actually in effect — `navigator.language` read back from the
|
|
149
151
|
launched browser. Chrome maps --lang to its nearest supported UI locale, so
|
|
150
152
|
this is what a site sees, which may differ from the requested locale.
|
|
@@ -156,7 +158,11 @@ async def _read_navigator_language(port: int) -> str | None:
|
|
|
156
158
|
browser = None
|
|
157
159
|
try:
|
|
158
160
|
browser = await connect_browser(port=port)
|
|
159
|
-
tab =
|
|
161
|
+
tab = (
|
|
162
|
+
next(t for t in browser.tabs if str(t._target.target_id) == target_id)
|
|
163
|
+
if target_id
|
|
164
|
+
else await get_active_tab(browser)
|
|
165
|
+
)
|
|
160
166
|
val = await tab.evaluate("navigator.language")
|
|
161
167
|
return val if isinstance(val, str) and val else None
|
|
162
168
|
except Exception:
|
|
@@ -227,6 +233,9 @@ def browser_start(
|
|
|
227
233
|
data directory returns conflict; use browser_connect instead. Startup
|
|
228
234
|
changes neither installed extensions nor profile preferences. A failed
|
|
229
235
|
handshake leaves the explicitly launched Chrome running and reports its PID.
|
|
236
|
+
Read extension_warning before input: an attached extension's unknown or
|
|
237
|
+
mismatched version needs an ordinary update/reload and a fresh connection.
|
|
238
|
+
Fresh CDP launches prepare their owned page before the initial URL navigation.
|
|
230
239
|
|
|
231
240
|
Proxy consistency (automatic): if `extra_args` carries a `--proxy-server`
|
|
232
241
|
and you don't pass an explicit `timezone`/`geo`, this auto-aligns the
|
|
@@ -267,8 +276,10 @@ def browser_start(
|
|
|
267
276
|
reuse: "none" (always new) or "any" (reuse an idle same-profile
|
|
268
277
|
Chrome, default). Only consulted for a named profile — an
|
|
269
278
|
ephemeral session never reuses.
|
|
270
|
-
startup_timeout: Seconds to wait for Chrome's debug port
|
|
271
|
-
identity,
|
|
279
|
+
startup_timeout: Seconds to wait for Chrome's debug port, CDP browser
|
|
280
|
+
identity, owned page/network context and initial navigation, or
|
|
281
|
+
the extension handshake.
|
|
282
|
+
`None` resolves to the
|
|
272
283
|
`AI_DEV_BROWSER_STARTUP_TIMEOUT` env var, else 30s — which
|
|
273
284
|
covers cold-start on slow Windows machines (fresh profile init
|
|
274
285
|
+ Defender scan + I/O contention from the user's main Chrome).
|
|
@@ -506,23 +517,23 @@ def browser_start(
|
|
|
506
517
|
from typing import Any
|
|
507
518
|
|
|
508
519
|
launch_options: dict[str, Any] = {"chrome_path": chrome_path} if chrome_path else {}
|
|
520
|
+
launch_overrides = {"--no-startup-window": "", **(override_default_args or {})}
|
|
509
521
|
if display is not None:
|
|
510
522
|
launch_options["display"] = display
|
|
511
523
|
for attempt in range(max_attempts):
|
|
512
524
|
process = launch_chrome(
|
|
513
525
|
port=port,
|
|
514
526
|
headless=headless_resolved,
|
|
515
|
-
start_url=
|
|
527
|
+
start_url="",
|
|
516
528
|
user_data_dir=str(cdp_data_dir),
|
|
517
529
|
extra_args=extra_args,
|
|
518
|
-
override_default_args=
|
|
530
|
+
override_default_args=launch_overrides,
|
|
519
531
|
silent_stderr=silent_stderr,
|
|
520
532
|
stealth=stealth,
|
|
521
533
|
**launch_options,
|
|
522
534
|
)
|
|
523
535
|
|
|
524
|
-
#
|
|
525
|
-
# Wait for both read-only handshakes before reporting a usable session.
|
|
536
|
+
# Establish this process's browser identity before creating its owned page.
|
|
526
537
|
bound = False
|
|
527
538
|
ready_guid = None
|
|
528
539
|
ready_target = None
|
|
@@ -532,10 +543,8 @@ def browser_start(
|
|
|
532
543
|
if is_port_in_use(port=port):
|
|
533
544
|
ready_guid = _query_chrome_guid(port)
|
|
534
545
|
if ready_guid:
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
bound = True
|
|
538
|
-
break
|
|
546
|
+
bound = True
|
|
547
|
+
break
|
|
539
548
|
if process.poll() is not None:
|
|
540
549
|
stderr = process.stderr.read() if process.stderr else ""
|
|
541
550
|
if not stderr:
|
|
@@ -558,7 +567,7 @@ def browser_start(
|
|
|
558
567
|
last_error = {
|
|
559
568
|
"error": (
|
|
560
569
|
f"Chrome started (PID {orphan_pid}) but port {port} not "
|
|
561
|
-
f"ready with a CDP browser identity
|
|
570
|
+
f"ready with a CDP browser identity after {startup_timeout}s — process killed to "
|
|
562
571
|
f"release profile lockfile. Retry with startup_timeout=<larger> "
|
|
563
572
|
f"if your environment is slow (Windows + main Chrome running, "
|
|
564
573
|
f"first-time profile init, AV scanning, etc.)."
|
|
@@ -584,6 +593,60 @@ def browser_start(
|
|
|
584
593
|
continue
|
|
585
594
|
return last_error
|
|
586
595
|
|
|
596
|
+
ready_target = None
|
|
597
|
+
start_options["headless"] = headless_resolved
|
|
598
|
+
spawned_args = getattr(process, "args", None)
|
|
599
|
+
if isinstance(spawned_args, (list, tuple)) and spawned_args:
|
|
600
|
+
# Preserve the executable launched when macOS reports a code-sign clone.
|
|
601
|
+
start_options["chrome_path"] = str(spawned_args[0])
|
|
602
|
+
try:
|
|
603
|
+
ready_target = _run_coro(
|
|
604
|
+
_create_startup_page(
|
|
605
|
+
port,
|
|
606
|
+
ready_guid,
|
|
607
|
+
"about:blank",
|
|
608
|
+
max(0.0, startup_timeout - (time.monotonic() - wait_started)),
|
|
609
|
+
)
|
|
610
|
+
)
|
|
611
|
+
_run_coro(
|
|
612
|
+
_prepare_startup_page(
|
|
613
|
+
port,
|
|
614
|
+
ready_target,
|
|
615
|
+
start_url,
|
|
616
|
+
max(0.0, startup_timeout - (time.monotonic() - wait_started)),
|
|
617
|
+
)
|
|
618
|
+
)
|
|
619
|
+
except Exception as error:
|
|
620
|
+
# A lost creation or navigation reply is ambiguous. Retain Chrome and
|
|
621
|
+
# the known target so reconnect can inspect the outcome.
|
|
622
|
+
registry.register_instance(
|
|
623
|
+
port=port,
|
|
624
|
+
guid=ready_guid,
|
|
625
|
+
workspace=os.getcwd(),
|
|
626
|
+
pid=process.pid,
|
|
627
|
+
user_data_dir=str(cdp_data_dir),
|
|
628
|
+
)
|
|
629
|
+
state = {
|
|
630
|
+
"transport": "cdp",
|
|
631
|
+
"port": port,
|
|
632
|
+
"pid": process.pid,
|
|
633
|
+
"browser_closed": False,
|
|
634
|
+
"user_data_dir": str(cdp_data_dir),
|
|
635
|
+
"headless": headless_resolved,
|
|
636
|
+
}
|
|
637
|
+
if ready_target:
|
|
638
|
+
state = _describe_cdp(
|
|
639
|
+
{**state, "automation_target_id": ready_target},
|
|
640
|
+
start_options=start_options,
|
|
641
|
+
guid=ready_guid,
|
|
642
|
+
)
|
|
643
|
+
return _session.failure(
|
|
644
|
+
"transient",
|
|
645
|
+
f"Chrome's initial page could not be confirmed: {str(error) or type(error).__name__}. "
|
|
646
|
+
"Reconnect and inspect its pages before continuing.",
|
|
647
|
+
**state,
|
|
648
|
+
)
|
|
649
|
+
|
|
587
650
|
# Record this instance so workspace/profile discovery can find it WITHOUT
|
|
588
651
|
# reading Chrome's command line over CDP (which needs --enable-automation,
|
|
589
652
|
# off under stealth). Keyed later by the browser GUID from /json/version so
|
|
@@ -664,21 +727,96 @@ def browser_start(
|
|
|
664
727
|
# the locale applied while navigator.language was unchanged). Surface
|
|
665
728
|
# the requested value too when Chrome normalized it away.
|
|
666
729
|
requested = identity["locale"]
|
|
667
|
-
effective =
|
|
730
|
+
effective = (
|
|
731
|
+
_run_coro(_read_navigator_language(port, ready_target)) or requested
|
|
732
|
+
)
|
|
668
733
|
result["locale"] = effective
|
|
669
734
|
if effective != requested:
|
|
670
735
|
result["locale_requested"] = requested
|
|
671
736
|
if egress_ip:
|
|
672
737
|
result["egress_ip"] = egress_ip
|
|
673
|
-
start_options["headless"] = headless_resolved
|
|
674
|
-
spawned_args = getattr(process, "args", None)
|
|
675
|
-
if isinstance(spawned_args, (list, tuple)) and spawned_args:
|
|
676
|
-
# psutil can observe a temporary macOS code-sign clone. Restart the
|
|
677
|
-
# executable we actually launched; retain the observed path for PID checks.
|
|
678
|
-
start_options["chrome_path"] = str(spawned_args[0])
|
|
679
738
|
return _describe_cdp(result, start_options=start_options, guid=ready_guid)
|
|
680
739
|
|
|
681
740
|
|
|
741
|
+
async def _create_startup_page(port: int, guid: str, url: str, timeout: float) -> str:
|
|
742
|
+
"""Create the one startup target on the verified browser, without replay."""
|
|
743
|
+
from ai_dev_browser.cdp import target
|
|
744
|
+
from ._transport import CDPConnection
|
|
745
|
+
|
|
746
|
+
connection = CDPConnection(f"ws://127.0.0.1:{port}/devtools/browser/{guid}")
|
|
747
|
+
|
|
748
|
+
async def create():
|
|
749
|
+
await connection.connect()
|
|
750
|
+
return str(await connection.send(target.create_target(url), timeout=timeout))
|
|
751
|
+
|
|
752
|
+
try:
|
|
753
|
+
return await asyncio.wait_for(create(), timeout=timeout)
|
|
754
|
+
finally:
|
|
755
|
+
await connection.disconnect()
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
async def _prepare_startup_page(
|
|
759
|
+
port: int, target_id: str, url: str, timeout: float
|
|
760
|
+
) -> None:
|
|
761
|
+
"""Prepare the blank owned page, then issue the requested navigation once.
|
|
762
|
+
|
|
763
|
+
Chrome's temporary empty document can already report readyState=complete.
|
|
764
|
+
Its navigation entry has no name and no DOMContentLoaded timestamp.
|
|
765
|
+
"""
|
|
766
|
+
from ai_dev_browser.cdp import network, page, runtime
|
|
767
|
+
from ._transport import CDPConnection, ProtocolException
|
|
768
|
+
|
|
769
|
+
connection = CDPConnection(f"ws://127.0.0.1:{port}/devtools/page/{target_id}")
|
|
770
|
+
|
|
771
|
+
async def wait_document(previous_loader=None):
|
|
772
|
+
while True:
|
|
773
|
+
try:
|
|
774
|
+
frame = (await connection.send(page.get_frame_tree())).frame
|
|
775
|
+
if previous_loader is not None and frame.loader_id == previous_loader:
|
|
776
|
+
await asyncio.sleep(0.01)
|
|
777
|
+
continue
|
|
778
|
+
result, exception = await connection.send(
|
|
779
|
+
runtime.evaluate(
|
|
780
|
+
"performance.getEntriesByType('navigation').some("
|
|
781
|
+
"n => n.name && n.domContentLoadedEventEnd > 0)",
|
|
782
|
+
return_by_value=True,
|
|
783
|
+
)
|
|
784
|
+
)
|
|
785
|
+
if not exception and result.value is True:
|
|
786
|
+
return
|
|
787
|
+
except ProtocolException as error:
|
|
788
|
+
# The initial navigation can replace the temporary JS context.
|
|
789
|
+
if "Execution context was destroyed" not in str(error):
|
|
790
|
+
raise
|
|
791
|
+
await asyncio.sleep(0.01)
|
|
792
|
+
|
|
793
|
+
async def prepare():
|
|
794
|
+
await connection.connect()
|
|
795
|
+
await connection.send(page.enable())
|
|
796
|
+
# A parsed blank page can precede Chrome's profile NetworkContext.
|
|
797
|
+
# This origin-scoped CookieManager RPC waits for that context without
|
|
798
|
+
# making an HTTP request. Discard its result; no cookies are modified.
|
|
799
|
+
await asyncio.gather(
|
|
800
|
+
wait_document(),
|
|
801
|
+
connection.send(network.get_cookies(urls=[f"http://127.0.0.1:{port}/"])),
|
|
802
|
+
)
|
|
803
|
+
if url != "about:blank":
|
|
804
|
+
previous_loader = (
|
|
805
|
+
await connection.send(page.get_frame_tree())
|
|
806
|
+
).frame.loader_id
|
|
807
|
+
_, loader, error, *_ = await connection.send(page.navigate(url))
|
|
808
|
+
if error:
|
|
809
|
+
raise ProtocolException(
|
|
810
|
+
f"Navigation failed: {error}", method="Page.navigate"
|
|
811
|
+
)
|
|
812
|
+
await wait_document(previous_loader if loader else None)
|
|
813
|
+
|
|
814
|
+
try:
|
|
815
|
+
await asyncio.wait_for(prepare(), timeout=timeout)
|
|
816
|
+
finally:
|
|
817
|
+
await connection.disconnect()
|
|
818
|
+
|
|
819
|
+
|
|
682
820
|
def _query_initial_page(port: int) -> str | None:
|
|
683
821
|
"""A listening browser endpoint does not prove its initial page exists."""
|
|
684
822
|
import json
|
|
@@ -793,7 +931,7 @@ async def browser_connect(
|
|
|
793
931
|
headless: bool | str | None = None,
|
|
794
932
|
timeout: float = 4.0,
|
|
795
933
|
) -> dict:
|
|
796
|
-
"""Use when: attaching to existing CDP or
|
|
934
|
+
"""Use when: attaching to existing CDP or real Chrome; returns session identity and extension_warning to check before input.
|
|
797
935
|
|
|
798
936
|
{mode_guidance}
|
|
799
937
|
|
|
@@ -813,6 +951,9 @@ async def browser_connect(
|
|
|
813
951
|
`bundled_extension_version`; an unknown version stays null. Updating Python
|
|
814
952
|
does not reload Chrome's extension. Ordinary extension reload can reset
|
|
815
953
|
control: inspect the session and original owned page before further input.
|
|
954
|
+
A non-null extension_warning means the runtime version is unknown or differs
|
|
955
|
+
from the bundle: follow its update/reload steps before input. connected=true
|
|
956
|
+
confirms the connection; actual visibility/readiness comes from window_set.
|
|
816
957
|
|
|
817
958
|
Args:
|
|
818
959
|
transport: "cdp" (default, existing debug Chrome) or "extension"
|
|
@@ -830,6 +971,8 @@ async def browser_connect(
|
|
|
830
971
|
owner, profile_owner, actual headless (null if unknown), account for
|
|
831
972
|
extension, port for CDP, tabs and tab_count. Includes version/git_commit
|
|
832
973
|
and loaded/bundled extension versions with their observed match status.
|
|
974
|
+
extension_warning is null when versions match, otherwise actionable
|
|
975
|
+
compatibility guidance; version matching alone does not prove visibility.
|
|
833
976
|
|
|
834
977
|
Failure:
|
|
835
978
|
Read error_code and setup_instructions. Missing bridge/extension needs
|
|
@@ -1082,6 +1225,8 @@ async def browser_reconnect(session_id: str, timeout: float = 10.0) -> dict:
|
|
|
1082
1225
|
closed page state, or repeat a click/submission. The returned identity and
|
|
1083
1226
|
tabs confirm reconnection; inspect page/server state before continuing an
|
|
1084
1227
|
action whose previous result was uncertain.
|
|
1228
|
+
Read any extension_warning before input: a reconnected extension may still
|
|
1229
|
+
need an ordinary update/reload and another explicit connection.
|
|
1085
1230
|
|
|
1086
1231
|
Args:
|
|
1087
1232
|
session_id: Identity from browser_start/browser_connect.
|
|
@@ -680,6 +680,10 @@ async def connect_extension(session_id: str | None = None) -> BrowserClient:
|
|
|
680
680
|
viewport override, and it follows popups/new tabs your automation opens (an
|
|
681
681
|
OAuth account-chooser, a magic-link tab) while never touching your own tabs.
|
|
682
682
|
|
|
683
|
+
Before initial workflow input, call browser_connect(transport="extension")
|
|
684
|
+
and resolve any extension_warning about the actual loaded version. Bind its
|
|
685
|
+
returned session_id here; an ordinary extension reload can replace the session.
|
|
686
|
+
|
|
683
687
|
Pair with `get_active_tab(browser, url_contains=...)` to pick the tab, same
|
|
684
688
|
as CDP mode.
|
|
685
689
|
|
|
@@ -39,6 +39,7 @@ from urllib.parse import parse_qs, urlsplit
|
|
|
39
39
|
import websockets
|
|
40
40
|
|
|
41
41
|
from .extension import EXTENSION_BRIDGE_PORT
|
|
42
|
+
from ._transport import _local_websocket_options
|
|
42
43
|
|
|
43
44
|
logger = logging.getLogger(__name__)
|
|
44
45
|
|
|
@@ -370,7 +371,8 @@ async def bridge_status(
|
|
|
370
371
|
"""Query the daemon's own state (`{extension_connected, account}`) — a status
|
|
371
372
|
frame it answers without relaying to the extension. None if unreachable."""
|
|
372
373
|
try:
|
|
373
|
-
|
|
374
|
+
url = f"ws://127.0.0.1:{port}"
|
|
375
|
+
async with websockets.connect(url, **_local_websocket_options(url)) as ws:
|
|
374
376
|
await ws.send(json.dumps({"id": 1, "method": "_bridge.status"}))
|
|
375
377
|
resp = json.loads(await asyncio.wait_for(ws.recv(), timeout))
|
|
376
378
|
return resp.get("result")
|
|
@@ -383,7 +385,8 @@ async def shutdown_bridge(
|
|
|
383
385
|
) -> bool:
|
|
384
386
|
"""Close this bridge's sockets through its own protocol, never its host PID."""
|
|
385
387
|
try:
|
|
386
|
-
|
|
388
|
+
url = f"ws://127.0.0.1:{port}"
|
|
389
|
+
async with websockets.connect(url, **_local_websocket_options(url)) as ws:
|
|
387
390
|
await ws.send(
|
|
388
391
|
json.dumps(
|
|
389
392
|
{
|
|
@@ -440,7 +443,7 @@ def _launch_page(connection_or_path, request_or_headers):
|
|
|
440
443
|
path = connection_or_path if legacy else request_or_headers.path
|
|
441
444
|
if not re.fullmatch(r"/adb-launch/[a-f0-9]{32}", path):
|
|
442
445
|
return None
|
|
443
|
-
body = "<!doctype html><meta charset=utf-8><title>
|
|
446
|
+
body = "<!doctype html><meta charset=utf-8><title>AI Dev Browser</title><p>This window is reserved for your browser task.</p>"
|
|
444
447
|
if legacy:
|
|
445
448
|
return (
|
|
446
449
|
HTTPStatus.OK,
|
|
@@ -32,12 +32,30 @@ def extension_version_info(loaded_version: str | None) -> dict:
|
|
|
32
32
|
bundled = json.loads(
|
|
33
33
|
(extension_dir() / "manifest.json").read_text(encoding="utf-8")
|
|
34
34
|
)["version"]
|
|
35
|
+
matches = loaded_version == bundled if loaded_version else None
|
|
36
|
+
warning = None
|
|
37
|
+
if matches is not True:
|
|
38
|
+
observed = (
|
|
39
|
+
f"Loaded extension {loaded_version} differs from SDK bundle {bundled}"
|
|
40
|
+
if loaded_version
|
|
41
|
+
else f"Loaded extension version is unknown (SDK bundle {bundled})"
|
|
42
|
+
)
|
|
43
|
+
warning = (
|
|
44
|
+
f"{observed}; compatibility has not been verified. Before page input, "
|
|
45
|
+
"align the Python package and running extension with an ordinary "
|
|
46
|
+
"update/reload in chrome://extensions in the intended profile. "
|
|
47
|
+
f"This SDK's bundle is {extension_dir()}. "
|
|
48
|
+
"Python updates do not reload Chrome's extension. Then use "
|
|
49
|
+
"browser_connect --transport extension and inspect session/profile, "
|
|
50
|
+
"owned target and real visibility before resuming; reload may replace "
|
|
51
|
+
"the session. Keep Chrome and its existing tabs; do not replay "
|
|
52
|
+
"earlier submissions."
|
|
53
|
+
)
|
|
35
54
|
return {
|
|
36
55
|
"extension_version": loaded_version,
|
|
37
56
|
"bundled_extension_version": bundled,
|
|
38
|
-
"extension_version_matches_bundle":
|
|
39
|
-
|
|
40
|
-
else None,
|
|
57
|
+
"extension_version_matches_bundle": matches,
|
|
58
|
+
"extension_warning": warning,
|
|
41
59
|
}
|
|
42
60
|
|
|
43
61
|
|