machine-bridge-mcp 2.0.0 → 3.0.0-beta.102
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.
- package/CHANGELOG.md +1142 -0
- package/CONTRIBUTING.md +23 -25
- package/GOVERNANCE.md +2 -2
- package/README.md +58 -20
- package/SECURITY.md +161 -120
- package/SUPPORT.md +1 -1
- package/browser-extension/broker-auth.js +83 -0
- package/browser-extension/broker-liveness.js +35 -0
- package/browser-extension/browser-error-boundary.js +96 -0
- package/browser-extension/browser-operations.js +1456 -101
- package/browser-extension/devtools-input.js +195 -34
- package/browser-extension/devtools-observation.js +429 -0
- package/browser-extension/devtools-session.js +48 -0
- package/browser-extension/manifest.json +4 -3
- package/browser-extension/page-automation.js +578 -118
- package/browser-extension/pairing-bootstrap.js +42 -0
- package/browser-extension/pairing.js +28 -12
- package/browser-extension/service-worker.js +126 -106
- package/docs/AGENT_CONTEXT.md +13 -10
- package/docs/ARCHITECTURE.md +159 -67
- package/docs/AUDIT.md +1302 -4
- package/docs/CLIENTS.md +19 -6
- package/docs/COMPUTER_USE.md +297 -0
- package/docs/ENGINEERING.md +72 -13
- package/docs/GETTING_STARTED.md +18 -20
- package/docs/LOCAL_AUTHORIZATION.md +115 -72
- package/docs/LOCAL_AUTOMATION.md +27 -21
- package/docs/LOGGING.md +40 -19
- package/docs/MANAGED_JOBS.md +37 -34
- package/docs/MULTI_ACCOUNT.md +98 -37
- package/docs/OPERATIONS.md +145 -51
- package/docs/OVERVIEW.md +14 -11
- package/docs/POLICY_REFERENCE.md +71 -3
- package/docs/PRIVACY.md +41 -2
- package/docs/PROJECT_STANDARDS.md +29 -6
- package/docs/RELEASING.md +238 -64
- package/docs/TESTING.md +150 -39
- package/docs/THREAT_MODEL.md +164 -67
- package/docs/TOOL_REFERENCE.md +653 -60
- package/docs/UPGRADING.md +78 -18
- package/native/macos/MachineBridgeBackgroundInput.swift +1051 -0
- package/native/macos/MachineBridgeBackgroundInputSmokeFixture.swift +295 -0
- package/native/macos/MachineBridgeTrustBroker.swift +233 -0
- package/package.json +94 -16
- package/scripts/accepted-candidate-tarball.mjs +77 -0
- package/scripts/candidate-runtime-store.mjs +93 -0
- package/scripts/check-plan.mjs +73 -3
- package/scripts/check-runner.mjs +129 -17
- package/scripts/commit-message-check.mjs +5 -2
- package/scripts/consumer-package-security.mjs +293 -0
- package/scripts/coverage-check.mjs +251 -15
- package/scripts/coverage-generation.mjs +41 -0
- package/scripts/coverage-range-merge.mjs +41 -0
- package/scripts/foreground-daemon-recovery.mjs +88 -0
- package/scripts/generate-policy-reference.mjs +28 -5
- package/scripts/generate-tool-reference.mjs +1 -1
- package/scripts/generate-worker-types.mjs +21 -11
- package/scripts/github-backlog.mjs +11 -10
- package/scripts/github-push.mjs +62 -16
- package/scripts/github-release-asset.mjs +58 -0
- package/scripts/github-release.mjs +241 -132
- package/scripts/global-package-installation.mjs +68 -0
- package/scripts/hardened-npm-session.mjs +42 -0
- package/scripts/install-published-prerelease.mjs +198 -0
- package/scripts/local-release-acceptance.mjs +103 -45
- package/scripts/macos-background-input-smoke.mjs +515 -0
- package/scripts/network-retry.mjs +8 -1
- package/scripts/npm-global-prefix.mjs +27 -0
- package/scripts/npm-publication-policy.mjs +17 -0
- package/scripts/official-mcp-conformance.mjs +267 -0
- package/scripts/persistent-activation-process.mjs +101 -0
- package/scripts/prepare-pinned-npm.mjs +12 -69
- package/scripts/prerelease-activation.mjs +160 -0
- package/scripts/privacy-check.mjs +14 -6
- package/scripts/promotion-digest.mjs +128 -0
- package/scripts/publish-npm.mjs +260 -0
- package/scripts/published-release.mjs +132 -0
- package/scripts/release-acceptance.mjs +62 -54
- package/scripts/release-browser-extension-store.mjs +172 -0
- package/scripts/release-candidate-manifest.mjs +52 -0
- package/scripts/release-channel.mjs +106 -0
- package/scripts/release-diagnostic.mjs +41 -0
- package/scripts/release-impact-check.mjs +12 -22
- package/scripts/release-oauth-canary-core.mjs +325 -0
- package/scripts/release-oauth-canary-evidence.mjs +100 -0
- package/scripts/release-oauth-canary.mjs +127 -0
- package/scripts/release-publication-guard.mjs +67 -0
- package/scripts/release-soak.mjs +284 -0
- package/scripts/release-state.mjs +1 -1
- package/scripts/run-checks.mjs +44 -7
- package/scripts/run-worker-dry-run.mjs +21 -0
- package/scripts/sarif-security-gate.mjs +2 -1
- package/scripts/sbom-check.mjs +106 -0
- package/scripts/start-release-candidate.mjs +267 -44
- package/scripts/syntax-check.mjs +36 -14
- package/scripts/verification-generation-guard.mjs +15 -0
- package/scripts/verification-idle-sleep-guard.mjs +37 -0
- package/scripts/verification-state.mjs +108 -0
- package/scripts/wrangler-command-lifecycle.mjs +115 -0
- package/src/local/account-access.mjs +13 -1
- package/src/local/account-admin-response.mjs +92 -0
- package/src/local/account-admin.mjs +52 -19
- package/src/local/agent-context-projection.mjs +26 -7
- package/src/local/agent-context.mjs +88 -49
- package/src/local/agent-skill-discovery.mjs +16 -5
- package/src/local/app-automation-macos-jxa.mjs +350 -0
- package/src/local/app-automation.mjs +873 -163
- package/src/local/application-capability-projection.mjs +24 -0
- package/src/local/authority-context.mjs +106 -0
- package/src/local/autostart-log-maintenance.mjs +36 -0
- package/src/local/browser-bridge-http.mjs +5 -3
- package/src/local/browser-bridge.mjs +109 -31
- package/src/local/browser-broker-auth-http.mjs +67 -0
- package/src/local/browser-broker-auth.mjs +98 -0
- package/src/local/browser-broker-routes.mjs +63 -20
- package/src/local/browser-broker-server.mjs +17 -7
- package/src/local/browser-command.mjs +79 -30
- package/src/local/browser-computer-observation-service.mjs +266 -0
- package/src/local/browser-extension-identity.mjs +50 -0
- package/src/local/browser-extension-path.mjs +22 -0
- package/src/local/browser-extension-protocol.mjs +50 -11
- package/src/local/browser-operation-service.mjs +147 -117
- package/src/local/browser-pairing-grant.mjs +109 -0
- package/src/local/browser-pairing-http.mjs +36 -0
- package/src/local/browser-pairing-launch.mjs +58 -0
- package/src/local/browser-pairing-store.mjs +85 -48
- package/src/local/browser-request-registry.mjs +40 -14
- package/src/local/browser-request-settlement.mjs +78 -0
- package/src/local/browser-resource-input.mjs +74 -0
- package/src/local/browser-trusted-input-health.mjs +63 -0
- package/src/local/call-authority.mjs +21 -0
- package/src/local/call-capacity.mjs +45 -0
- package/src/local/call-registry-drain.mjs +54 -0
- package/src/local/call-registry.mjs +30 -15
- package/src/local/capability-observer.mjs +5 -0
- package/src/local/capability-ranking.mjs +3 -2
- package/src/local/child-process-settlement.mjs +132 -0
- package/src/local/cli-account-admin.mjs +40 -6
- package/src/local/cli-activate.mjs +162 -0
- package/src/local/cli-local-admin.mjs +20 -45
- package/src/local/cli-options.mjs +12 -13
- package/src/local/cli-ready-output.mjs +52 -0
- package/src/local/cli-service.mjs +91 -11
- package/src/local/cli.mjs +232 -150
- package/src/local/computer-use-application-observation.mjs +224 -0
- package/src/local/computer-use-arguments.mjs +270 -0
- package/src/local/computer-use-deadline.mjs +50 -0
- package/src/local/computer-use-dispatch-settlement.mjs +40 -0
- package/src/local/computer-use-expectation.mjs +171 -0
- package/src/local/computer-use-observation.mjs +769 -0
- package/src/local/computer-use-recovery.mjs +82 -0
- package/src/local/computer-use-result-budget.mjs +145 -0
- package/src/local/computer-use-snapshot-store.mjs +67 -0
- package/src/local/computer-use.mjs +2255 -0
- package/src/local/daemon-http-relay-auth.mjs +26 -0
- package/src/local/daemon-http-relay-connection.mjs +298 -0
- package/src/local/daemon-http-relay-request.mjs +64 -0
- package/src/local/daemon-http-relay-sequence.mjs +49 -0
- package/src/local/daemon-process.mjs +40 -7
- package/src/local/delegated-process-sandbox.mjs +148 -0
- package/src/local/device-identity.mjs +156 -49
- package/src/local/device-root-provider.mjs +81 -0
- package/src/local/directory-metadata.mjs +47 -0
- package/src/local/doctor-reporting.mjs +23 -0
- package/src/local/durable-process-spec.mjs +80 -0
- package/src/local/errors.mjs +25 -15
- package/src/local/exclusive-file.mjs +74 -26
- package/src/local/execution-limits.mjs +9 -0
- package/src/local/execution-routing.mjs +276 -0
- package/src/local/execution-surface.mjs +23 -0
- package/src/local/file-mutation-coordinator.mjs +47 -0
- package/src/local/file-snapshot-preservation.mjs +24 -0
- package/src/local/filesystem-identity.mjs +54 -0
- package/src/local/fixed-process-environment.mjs +21 -0
- package/src/local/full-access-test.mjs +31 -13
- package/src/local/git-commit.mjs +69 -0
- package/src/local/git-config-safety.mjs +55 -0
- package/src/local/git-log-parser.mjs +41 -0
- package/src/local/git-metadata-boundary.mjs +77 -0
- package/src/local/git-metadata-tree-safety.mjs +59 -0
- package/src/local/git-operation-state.mjs +19 -0
- package/src/local/git-service.mjs +165 -30
- package/src/local/hardened-npm-download-timeout.mjs +46 -0
- package/src/local/hardened-npm-download.mjs +109 -0
- package/src/local/hardened-npm-extract.mjs +20 -0
- package/src/local/hardened-npm-verification.mjs +104 -0
- package/src/local/hardened-npm.mjs +199 -0
- package/src/local/job-runner.mjs +244 -89
- package/src/local/lifecycle.mjs +6 -0
- package/src/local/log.mjs +19 -45
- package/src/local/macos-background-input.mjs +398 -0
- package/src/local/macos-idle-sleep-assertion.mjs +65 -0
- package/src/local/macos-trust-broker.mjs +404 -0
- package/src/local/managed-job-cancellation.mjs +42 -0
- package/src/local/managed-job-capacity.mjs +16 -0
- package/src/local/managed-job-directory-generation.mjs +60 -0
- package/src/local/managed-job-directory.mjs +75 -0
- package/src/local/managed-job-durable-process.mjs +49 -0
- package/src/local/managed-job-lock.mjs +46 -14
- package/src/local/managed-job-plan.mjs +38 -14
- package/src/local/managed-job-projection.mjs +23 -15
- package/src/local/managed-job-retention.mjs +128 -0
- package/src/local/managed-job-runner-claim.mjs +70 -0
- package/src/local/managed-job-runner.mjs +35 -10
- package/src/local/managed-job-storage.mjs +22 -6
- package/src/local/managed-job-terminal-maintenance.mjs +40 -0
- package/src/local/managed-job-terminal.mjs +150 -0
- package/src/local/managed-jobs.mjs +424 -287
- package/src/local/npm-cli.mjs +42 -0
- package/src/local/npm-environment.mjs +37 -0
- package/src/local/operation-authorization.mjs +68 -324
- package/src/local/operation-risk.mjs +51 -4
- package/src/local/owner-state-lock.mjs +118 -0
- package/src/local/package-identity.mjs +9 -0
- package/src/local/patch.mjs +20 -18
- package/src/local/path-inspection.mjs +23 -0
- package/src/local/policy.mjs +19 -0
- package/src/local/private-toolchain-integrity.mjs +54 -0
- package/src/local/process-execution.mjs +87 -75
- package/src/local/process-foreground-timeout.mjs +50 -0
- package/src/local/process-identity.mjs +41 -7
- package/src/local/process-nonreplayable-settlement.mjs +50 -0
- package/src/local/process-result-projection.mjs +22 -0
- package/src/local/process-session-events.mjs +47 -0
- package/src/local/process-session-remote-activity.mjs +10 -0
- package/src/local/process-session-termination.mjs +73 -0
- package/src/local/process-sessions.mjs +144 -112
- package/src/local/process-tracker.mjs +140 -13
- package/src/local/process-tree-ownership-types.d.ts +37 -0
- package/src/local/process-tree-ownership.mjs +57 -0
- package/src/local/process-tree-signal.mjs +34 -0
- package/src/local/process-tree-snapshot.mjs +77 -0
- package/src/local/process-tree-supervisor.mjs +69 -0
- package/src/local/process-tree.mjs +11 -53
- package/src/local/project-metadata.mjs +19 -6
- package/src/local/relay-call-recovery.mjs +58 -38
- package/src/local/relay-connection-classification.mjs +131 -0
- package/src/local/relay-connection-support.mjs +91 -0
- package/src/local/relay-connection.mjs +179 -272
- package/src/local/relay-diagnostics.mjs +91 -0
- package/src/local/relay-heartbeat.mjs +119 -0
- package/src/local/relay-liveness.mjs +109 -0
- package/src/local/relay-peer-diagnostics.mjs +29 -0
- package/src/local/release-runtime-lock.mjs +18 -0
- package/src/local/remote-activity-idle-sleep-guard.mjs +77 -0
- package/src/local/remote-configuration.mjs +48 -0
- package/src/local/resilient-relay-connection.mjs +178 -0
- package/src/local/resource-admission-diagnostics.mjs +52 -0
- package/src/local/resource-admission-policy.mjs +115 -0
- package/src/local/resource-admission.mjs +339 -0
- package/src/local/resource-build-root.mjs +83 -0
- package/src/local/resource-cargo-concurrency.mjs +38 -0
- package/src/local/resource-cmake-concurrency.mjs +42 -0
- package/src/local/resource-command-concurrency.mjs +37 -0
- package/src/local/resource-command-profile.mjs +176 -0
- package/src/local/resource-coordinator-accounting.mjs +21 -0
- package/src/local/resource-cpu-window.mjs +20 -0
- package/src/local/resource-disk-headroom.mjs +13 -0
- package/src/local/resource-elastic-memory.mjs +19 -0
- package/src/local/resource-elastic-request.mjs +36 -0
- package/src/local/resource-foreground-wait.mjs +29 -0
- package/src/local/resource-go-concurrency.mjs +43 -0
- package/src/local/resource-gradle-concurrency.mjs +57 -0
- package/src/local/resource-host-cache.mjs +39 -0
- package/src/local/resource-host-darwin.mjs +54 -0
- package/src/local/resource-host-linux.mjs +35 -0
- package/src/local/resource-host-sample-file.mjs +23 -0
- package/src/local/resource-host-snapshot.mjs +99 -0
- package/src/local/resource-lease-accounting.mjs +119 -0
- package/src/local/resource-light-command.mjs +6 -0
- package/src/local/resource-make-concurrency.mjs +42 -0
- package/src/local/resource-maven-concurrency.mjs +38 -0
- package/src/local/resource-ninja-command-concurrency.mjs +20 -0
- package/src/local/resource-ninja-concurrency.mjs +28 -0
- package/src/local/resource-operations.mjs +26 -10
- package/src/local/resource-pressure.mjs +67 -0
- package/src/local/resource-probe-command.mjs +35 -0
- package/src/local/resource-process-admission.mjs +59 -0
- package/src/local/resource-process-ancestry-cache.mjs +43 -0
- package/src/local/resource-process-ancestry.mjs +32 -0
- package/src/local/resource-process-priority.mjs +15 -0
- package/src/local/resource-project-key.mjs +53 -0
- package/src/local/resource-pytest-concurrency.mjs +33 -0
- package/src/local/resource-release-control-classification.mjs +14 -0
- package/src/local/resource-release-control-executable.mjs +70 -0
- package/src/local/resource-release-control-workspace.mjs +41 -0
- package/src/local/resource-request-contract.mjs +30 -0
- package/src/local/resource-script-classification.mjs +44 -0
- package/src/local/resource-shell-analysis.mjs +58 -0
- package/src/local/resource-staging-recovery.mjs +97 -0
- package/src/local/resource-swift-concurrency.mjs +31 -0
- package/src/local/resource-transaction-lock.mjs +189 -0
- package/src/local/resource-wait.mjs +48 -0
- package/src/local/resource-waiters.mjs +139 -0
- package/src/local/resource-xcode-command.mjs +7 -0
- package/src/local/resource-xcode-concurrency.mjs +34 -0
- package/src/local/resource-xcode-non-build.mjs +19 -0
- package/src/local/runtime-activation.mjs +700 -0
- package/src/local/runtime-activity-projection.mjs +48 -0
- package/src/local/runtime-capabilities.mjs +48 -15
- package/src/local/runtime-diagnostic-state.mjs +14 -0
- package/src/local/runtime-diagnostics.mjs +40 -13
- package/src/local/runtime-info-projection.mjs +60 -0
- package/src/local/runtime-info-relay-projection.mjs +21 -0
- package/src/local/runtime-paths.mjs +2 -1
- package/src/local/runtime-process-routing.mjs +33 -0
- package/src/local/runtime-relay-connection-options.mjs +44 -0
- package/src/local/runtime-relay-control.mjs +98 -0
- package/src/local/runtime-relay.mjs +26 -50
- package/src/local/runtime-reporting.mjs +37 -21
- package/src/local/runtime-resource-service.mjs +77 -0
- package/src/local/runtime-tool-handlers.mjs +13 -10
- package/src/local/runtime.mjs +244 -208
- package/src/local/secure-file.mjs +73 -20
- package/src/local/security-audit-dispatch.mjs +35 -0
- package/src/local/security-audit-log.mjs +215 -0
- package/src/local/security-audit-state.mjs +139 -0
- package/src/local/security-audit-storage.mjs +135 -0
- package/src/local/security-audit-warning.mjs +31 -0
- package/src/local/security-audit-worker.mjs +87 -0
- package/src/local/service-convergence.mjs +42 -1
- package/src/local/service-definition.mjs +24 -0
- package/src/local/service-environment.mjs +18 -10
- package/src/local/service-owner.mjs +146 -0
- package/src/local/service-ownership.mjs +17 -0
- package/src/local/service-restart-handoff.mjs +64 -0
- package/src/local/service-restart-scheduler.mjs +49 -0
- package/src/local/service-runtime-convergence.mjs +52 -0
- package/src/local/service-runtime.mjs +105 -0
- package/src/local/service-status.mjs +60 -0
- package/src/local/service.mjs +385 -92
- package/src/local/shell.mjs +39 -19
- package/src/local/ssh-key.mjs +158 -42
- package/src/local/state-inventory.mjs +50 -24
- package/src/local/state-owner-lock-inventory.mjs +23 -0
- package/src/local/state-root-owned-namespaces.mjs +83 -0
- package/src/local/state-root-retirement.mjs +89 -0
- package/src/local/state.mjs +386 -96
- package/src/local/stdio.mjs +96 -66
- package/src/local/support-state-projection.mjs +30 -0
- package/src/local/system-network-route.mjs +76 -0
- package/src/local/systemd-removal.mjs +20 -0
- package/src/local/tool-executor.mjs +85 -20
- package/src/local/tool-result-boundary.mjs +51 -0
- package/src/local/toolchain-operation-lock.mjs +41 -0
- package/src/local/tools.mjs +7 -5
- package/src/local/trusted-executable.mjs +53 -0
- package/src/local/trusted-git-executable.mjs +34 -0
- package/src/local/trusted-github-cli.mjs +24 -0
- package/src/local/windows-service-convergence.mjs +49 -0
- package/src/local/windows-service.mjs +52 -45
- package/src/local/worker-deployment-fingerprint.mjs +102 -0
- package/src/local/worker-deployment.mjs +48 -60
- package/src/local/worker-health.mjs +12 -4
- package/src/local/worker-secret-file.mjs +37 -17
- package/src/local/workspace-file-service.mjs +156 -239
- package/src/local/workspace-file-transaction.mjs +189 -0
- package/src/local/workspace-search.mjs +55 -0
- package/src/local/wrangler-toolchain/package-lock.json +1505 -0
- package/src/local/wrangler-toolchain/package.json +23 -0
- package/src/local/wrangler-toolchain-verification.mjs +135 -0
- package/src/local/wrangler-toolchain.mjs +188 -0
- package/src/shared/access-contract.json +7 -1
- package/src/shared/activation-recovery.mjs +49 -0
- package/src/shared/admin-auth.d.mts +2 -1
- package/src/shared/admin-auth.mjs +3 -3
- package/src/shared/authority-revocation.d.mts +9 -0
- package/src/shared/authority-revocation.mjs +28 -0
- package/src/shared/daemon-auth.d.mts +11 -0
- package/src/shared/daemon-auth.mjs +17 -0
- package/src/shared/device-session-auth.d.mts +20 -0
- package/src/shared/device-session-auth.mjs +51 -0
- package/src/shared/foreground-timeout.d.mts +15 -0
- package/src/shared/foreground-timeout.mjs +70 -0
- package/src/shared/log-redaction.d.mts +9 -0
- package/src/shared/log-redaction.mjs +51 -0
- package/src/shared/mcp-protocol.d.mts +27 -0
- package/src/shared/mcp-protocol.mjs +275 -0
- package/src/shared/project-overview-projection.d.mts +4 -0
- package/src/shared/project-overview-projection.mjs +96 -0
- package/src/shared/relay-contract.json +28 -0
- package/src/shared/result-projection.d.mts +2 -1
- package/src/shared/result-projection.mjs +13 -2
- package/src/shared/server-metadata.json +15 -9
- package/src/shared/tool-argument-validation.d.mts +17 -0
- package/src/shared/tool-argument-validation.mjs +325 -0
- package/src/shared/tool-call-capacity.d.mts +41 -0
- package/src/shared/tool-call-capacity.mjs +59 -0
- package/src/shared/tool-catalog.json +626 -57
- package/src/worker/access.ts +12 -0
- package/src/worker/account-admin.ts +55 -33
- package/src/worker/authority-revocations.ts +174 -0
- package/src/worker/authority.ts +52 -6
- package/src/worker/daemon-auth.ts +49 -68
- package/src/worker/daemon-channel.ts +36 -0
- package/src/worker/daemon-http-auth.ts +63 -0
- package/src/worker/daemon-http-channel.ts +120 -0
- package/src/worker/daemon-http-controller.ts +163 -0
- package/src/worker/daemon-http-protocol.ts +62 -0
- package/src/worker/daemon-http-registry.ts +82 -0
- package/src/worker/daemon-last-observation.ts +30 -0
- package/src/worker/daemon-liveness.ts +8 -6
- package/src/worker/daemon-ready-dispatch.ts +17 -0
- package/src/worker/daemon-ready-messages.ts +73 -0
- package/src/worker/daemon-ready-waiters.ts +79 -0
- package/src/worker/daemon-recovery-budget.ts +13 -0
- package/src/worker/daemon-registry.ts +58 -0
- package/src/worker/daemon-relay-diagnostics.ts +127 -0
- package/src/worker/daemon-socket-attachment.ts +55 -0
- package/src/worker/daemon-sockets.ts +72 -62
- package/src/worker/daemon-status.ts +38 -0
- package/src/worker/device-session-verifier.ts +129 -0
- package/src/worker/dpop.ts +157 -0
- package/src/worker/durable-process-timeout.ts +29 -0
- package/src/worker/errors.ts +57 -0
- package/src/worker/http.ts +112 -26
- package/src/worker/index.ts +446 -440
- package/src/worker/mcp-access.ts +57 -0
- package/src/worker/mcp-controller.ts +188 -0
- package/src/worker/mcp-http-accept.ts +11 -0
- package/src/worker/mcp-http-contract.ts +322 -0
- package/src/worker/mcp-initialization-compat.ts +141 -0
- package/src/worker/mcp-jsonrpc.ts +11 -29
- package/src/worker/mcp-removed-protocol.ts +17 -0
- package/src/worker/mcp-response-proxy.ts +135 -0
- package/src/worker/mcp-response-stream.ts +65 -0
- package/src/worker/mcp-stale-schema-compat.ts +57 -0
- package/src/worker/mcp-stream-proxy-contract.ts +48 -0
- package/src/worker/mcp-tool-call-input.ts +23 -0
- package/src/worker/nonce-store.ts +33 -35
- package/src/worker/oauth-authorization-page.ts +21 -1
- package/src/worker/oauth-client-admin.ts +52 -0
- package/src/worker/oauth-client-contract.ts +38 -0
- package/src/worker/oauth-controller.ts +107 -47
- package/src/worker/oauth-field-contract.ts +31 -0
- package/src/worker/oauth-record-contract.ts +7 -0
- package/src/worker/oauth-refresh-authority.ts +27 -0
- package/src/worker/oauth-refresh-exchange.ts +162 -0
- package/src/worker/oauth-refresh-families.ts +130 -27
- package/src/worker/oauth-refresh-persistence.ts +116 -0
- package/src/worker/oauth-state.ts +33 -18
- package/src/worker/oauth-store-validation.ts +131 -0
- package/src/worker/oauth-token-derivation.ts +33 -0
- package/src/worker/oauth-token-issuance.ts +119 -0
- package/src/worker/oauth-tokens.ts +56 -190
- package/src/worker/observability.ts +38 -5
- package/src/worker/pending-admission.ts +15 -0
- package/src/worker/pending-call-capacity.ts +108 -0
- package/src/worker/pending-call-contract.ts +42 -5
- package/src/worker/pending-call-deadlines.ts +75 -0
- package/src/worker/pending-calls.ts +131 -125
- package/src/worker/runtime-alarm-storage.ts +37 -0
- package/src/worker/runtime-alarm.ts +136 -0
- package/src/worker/server-info-activity.ts +36 -0
- package/src/worker/server-info-tool-delivery.ts +15 -0
- package/src/worker/server-info.ts +168 -0
- package/src/worker/tool-catalog.ts +47 -1
- package/src/worker/tool-timeout.ts +75 -14
- package/src/worker/websocket-protocol.ts +26 -2
- package/src/worker/worker-edge-guard.ts +90 -0
- package/src/worker/worker-edge-log.ts +65 -0
- package/src/worker/worker-entry.ts +52 -0
- package/src/worker/worker-mcp-config.ts +23 -0
- package/src/worker/worker-metadata.ts +48 -0
- package/src/worker/worker-rate-limit-key.ts +30 -0
- package/src/worker/worker-runtime-config.ts +20 -0
- package/src/worker/worker-static-routes.ts +54 -0
- package/src/worker/worker-task-lifetime.ts +18 -0
- package/tsconfig.local.json +17 -1
- package/wrangler.jsonc +13 -0
- package/src/local/cli-approval.mjs +0 -117
- package/src/local/operation-state-lock.mjs +0 -92
- package/src/worker/mcp-session.ts +0 -72
|
@@ -2,17 +2,124 @@
|
|
|
2
2
|
if (globalThis.__machineBridgeBrowserOperations) return;
|
|
3
3
|
|
|
4
4
|
const MAX_ACCESSIBLE_FRAMES = 64;
|
|
5
|
+
const PAGE_AUTOMATION_VERSION = 4;
|
|
6
|
+
const MUTATING_METHODS = new Set([
|
|
7
|
+
"manage_tabs", "point_action", "backend_node_action", "action", "fill_form", "upload_files", "screenshot",
|
|
8
|
+
]);
|
|
9
|
+
const MUTATION_RESULT_SETTLEMENT_UNKNOWN = "browser mutation may have completed; the action outcome is unknown because its result could not be delivered. Inspect browser state before retrying.";
|
|
10
|
+
|
|
11
|
+
function exactFiniteNumber(value) {
|
|
12
|
+
return typeof value === "number" && Number.isFinite(value) ? (Object.is(value, -0) ? 0 : value) : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function exactPositiveFiniteNumber(value) {
|
|
16
|
+
const number = exactFiniteNumber(value);
|
|
17
|
+
return number !== null && number > 0 ? number : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function exactNormalizedCoordinate(value) {
|
|
21
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value < 1
|
|
22
|
+
? (Object.is(value, -0) ? 0 : value)
|
|
23
|
+
: null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function exactPositiveInteger(value) {
|
|
27
|
+
return Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function exactNonNegativeInteger(value) {
|
|
31
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function exactBoolean(value, label, fallback) {
|
|
35
|
+
if (value === undefined) return fallback;
|
|
36
|
+
if (typeof value !== "boolean") throw new Error(`${label} must be boolean before dispatch`);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function exactBoundedInteger(value, label, fallback, min, max) {
|
|
41
|
+
if (value === undefined) return fallback;
|
|
42
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`${label} is invalid before dispatch`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function exactImageFormat(value, label, fallback = "png") {
|
|
47
|
+
if (value === undefined) return fallback;
|
|
48
|
+
if (typeof value !== "string" || !["png", "jpeg"].includes(value)) throw new Error(`${label} is invalid before dispatch`);
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function exactOptionalSha256(value, label) {
|
|
53
|
+
if (value === undefined || value === "") return "";
|
|
54
|
+
if (typeof value !== "string" || !/^[A-Fa-f0-9]{64}$/.test(value)) throw new Error(`${label} is invalid before dispatch`);
|
|
55
|
+
return value.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function exactOptionalText(value, label, maxLength) {
|
|
59
|
+
if (value === undefined || value === "") return "";
|
|
60
|
+
if (typeof value !== "string" || value.includes("\0") || value.length > maxLength) throw new Error(`${label} is invalid before dispatch`);
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function requiredSnapshotAuthorityString(value, label, maxLength) {
|
|
65
|
+
const text = exactOptionalAuthorityString(value, maxLength);
|
|
66
|
+
if (!text) throw new Error(`${label} is required before dispatch`);
|
|
67
|
+
return text;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function requiredSnapshotViewport(value) {
|
|
71
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("visual snapshot viewport is required before dispatch");
|
|
72
|
+
const output = {};
|
|
73
|
+
for (const key of ["width", "height", "scale"]) {
|
|
74
|
+
const number = exactPositiveFiniteNumber(value[key]);
|
|
75
|
+
if (number === null) throw new Error(`visual snapshot viewport ${key} is invalid before dispatch`);
|
|
76
|
+
output[key] = number;
|
|
77
|
+
}
|
|
78
|
+
return output;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function exactOptionalAuthorityString(value, maxLength) {
|
|
82
|
+
if (value === undefined || value === "") return "";
|
|
83
|
+
if (typeof value !== "string" || value.length > maxLength || value.includes("\0")) {
|
|
84
|
+
throw new Error("snapshot authority string is invalid before dispatch");
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function methodMayMutate(method) {
|
|
90
|
+
return typeof method === "string" && MUTATING_METHODS.has(method);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function responsePayload({ id, ok, result, error = "", method = "", maxBytes }) {
|
|
94
|
+
const mutatingSuccess = ok === true && methodMayMutate(method);
|
|
95
|
+
let payload;
|
|
96
|
+
try { payload = JSON.stringify({ type: "response", id, ok, ...(ok ? { result } : { error }) }); }
|
|
97
|
+
catch {
|
|
98
|
+
payload = JSON.stringify({ type: "response", id, ok: false,
|
|
99
|
+
error: mutatingSuccess ? MUTATION_RESULT_SETTLEMENT_UNKNOWN : "browser result could not be serialized" });
|
|
100
|
+
}
|
|
101
|
+
const resultLimit = Number.isSafeInteger(maxBytes) && maxBytes > 0 ? maxBytes : 0;
|
|
102
|
+
if (resultLimit === 0 || new TextEncoder().encode(payload).byteLength > resultLimit) {
|
|
103
|
+
payload = JSON.stringify({ type: "response", id, ok: false,
|
|
104
|
+
error: mutatingSuccess ? MUTATION_RESULT_SETTLEMENT_UNKNOWN : "browser result exceeds maximum size" });
|
|
105
|
+
}
|
|
106
|
+
return payload;
|
|
107
|
+
}
|
|
5
108
|
|
|
6
109
|
async function dispatch(method, params, state) {
|
|
7
110
|
if (method === "list_tabs") return listTabs(params);
|
|
8
|
-
if (method === "manage_tabs") return manageTabs(params);
|
|
111
|
+
if (method === "manage_tabs") return manageTabs(params, state);
|
|
9
112
|
if (method === "wait") return browserWait(params, state);
|
|
10
113
|
if (method === "get_source") return getSource(params, state);
|
|
11
114
|
if (method === "inspect_page") return inspectPage(params, state);
|
|
115
|
+
if (method === "observe_computer") return observeComputer(params, state);
|
|
116
|
+
if (method === "document_state") return documentState(params);
|
|
117
|
+
if (method === "point_action") return pointAction(params, state);
|
|
118
|
+
if (method === "backend_node_action") return backendNodeAction(params, state);
|
|
12
119
|
if (method === "action") return browserAction(params, state);
|
|
13
120
|
if (method === "fill_form") return fillForm(params, state);
|
|
14
|
-
if (method === "upload_files") return uploadFiles(params);
|
|
15
|
-
if (method === "screenshot") return screenshot(params);
|
|
121
|
+
if (method === "upload_files") return uploadFiles(params, state);
|
|
122
|
+
if (method === "screenshot") return screenshot(params, state);
|
|
16
123
|
throw new Error(`unknown browser method: ${method}`);
|
|
17
124
|
}
|
|
18
125
|
|
|
@@ -37,40 +144,103 @@
|
|
|
37
144
|
}
|
|
38
145
|
|
|
39
146
|
|
|
40
|
-
async function manageTabs(params) {
|
|
41
|
-
|
|
42
|
-
|
|
147
|
+
async function manageTabs(params, state) {
|
|
148
|
+
const action = exactOptionalText(params?.action, "browser tab action", 32);
|
|
149
|
+
if (!["new", "activate", "close"].includes(action)) throw new Error("unsupported browser tab action");
|
|
150
|
+
let newTabUrl = "";
|
|
151
|
+
let newTabActive = true;
|
|
152
|
+
if (action === "new") {
|
|
153
|
+
newTabUrl = exactOptionalText(params.url, "browser tab url", 32768);
|
|
154
|
+
if (newTabUrl && !/^(https?|file):/i.test(newTabUrl)) throw new Error("browser tab url is invalid before dispatch");
|
|
155
|
+
newTabActive = exactBoolean(params.active, "browser tab active", true);
|
|
156
|
+
} else if (!Number.isSafeInteger(params.tabId) || params.tabId < 1) {
|
|
157
|
+
throw new Error("browser tab id is invalid");
|
|
158
|
+
}
|
|
159
|
+
throwIfCancelled(state);
|
|
160
|
+
if (action === "new") {
|
|
161
|
+
const tab = await invokeBrowserTabMutation(
|
|
162
|
+
() => chrome.tabs.create({ url: newTabUrl || "about:blank", active: newTabActive }),
|
|
163
|
+
"create",
|
|
164
|
+
);
|
|
43
165
|
return { action: "new", ...publicTab(tab) };
|
|
44
166
|
}
|
|
45
167
|
const tab = await chrome.tabs.get(params.tabId);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
await chrome.
|
|
49
|
-
|
|
168
|
+
throwIfCancelled(state);
|
|
169
|
+
if (action === "activate") {
|
|
170
|
+
const activated = await invokeBrowserTabMutation(() => chrome.tabs.update(tab.id, { active: true }), "activate_tab");
|
|
171
|
+
throwIfCancelled(state);
|
|
172
|
+
if (!Number.isInteger(activated?.windowId) || activated.windowId < 1) {
|
|
173
|
+
throw new Error("browser tab activation completed but its current window is unavailable before focus; inspect tabs before retrying");
|
|
174
|
+
}
|
|
175
|
+
let focusTarget;
|
|
176
|
+
try { focusTarget = await chrome.tabs.get(tab.id); }
|
|
177
|
+
catch { throw new Error("browser tab activation completed but the target tab could not be verified before focus; inspect tabs before retrying"); }
|
|
178
|
+
throwIfCancelled(state);
|
|
179
|
+
if (!Number.isInteger(focusTarget?.windowId) || focusTarget.windowId < 1) {
|
|
180
|
+
throw new Error("browser tab activation completed but its current window is unavailable before focus; inspect tabs before retrying");
|
|
181
|
+
}
|
|
182
|
+
if (focusTarget.active !== true) {
|
|
183
|
+
throw new Error("browser tab activation completed but the target tab was no longer active before focus; inspect tabs before retrying");
|
|
184
|
+
}
|
|
185
|
+
const focusWindowId = focusTarget.windowId;
|
|
186
|
+
await invokeBrowserTabMutation(() => chrome.windows.update(focusWindowId, { focused: true }), "focus_window");
|
|
187
|
+
let settled;
|
|
188
|
+
try { settled = await chrome.tabs.get(tab.id); }
|
|
189
|
+
catch { throw new Error("browser tab activation and window focus completed but the target tab could not be verified; inspect tabs before retrying"); }
|
|
190
|
+
if (settled?.windowId !== focusWindowId) {
|
|
191
|
+
throw new Error("browser tab activation and window focus completed but the target tab moved windows; inspect tabs before retrying");
|
|
192
|
+
}
|
|
193
|
+
if (settled.active !== true) {
|
|
194
|
+
throw new Error("browser tab activation and window focus completed but the target tab was no longer active; inspect tabs before retrying");
|
|
195
|
+
}
|
|
196
|
+
return { action: "activate", ...publicTab(settled) };
|
|
50
197
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
198
|
+
const result = { action: "close", closed: true, ...publicTab(tab) };
|
|
199
|
+
await invokeBrowserTabMutation(() => chrome.tabs.remove(tab.id), "close");
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizeBrowserWaitParams(params) {
|
|
204
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("browser wait parameters are invalid");
|
|
205
|
+
const timeoutMs = exactBoundedInteger(params.timeoutMs, "browser wait timeoutMs", 30000, 1, 120000);
|
|
206
|
+
let frameId = params.frameId;
|
|
207
|
+
if (frameId === undefined || frameId === null) frameId = null;
|
|
208
|
+
else if (!Number.isSafeInteger(frameId) || frameId < 0) throw new Error("browser wait frame id is invalid");
|
|
209
|
+
const selector = params.selector === undefined || params.selector === null ? null : params.selector;
|
|
210
|
+
const waitState = exactOptionalText(params.state, "browser wait state", 32);
|
|
211
|
+
if (waitState && !["attached", "detached", "visible", "hidden", "enabled", "editable", "checked", "unchecked"].includes(waitState)) {
|
|
212
|
+
throw new Error("browser wait state is invalid");
|
|
55
213
|
}
|
|
56
|
-
throw new Error("
|
|
214
|
+
if (waitState && !selector) throw new Error("browser wait state requires selector");
|
|
215
|
+
const text = exactOptionalText(params.text, "browser wait text", 4000);
|
|
216
|
+
const urlContains = exactOptionalText(params.urlContains, "browser wait urlContains", 32768);
|
|
217
|
+
const loadState = exactOptionalText(params.loadState, "browser wait loadState", 32);
|
|
218
|
+
if (loadState && !["domcontentloaded", "complete"].includes(loadState)) throw new Error("browser wait loadState is invalid");
|
|
219
|
+
if (!selector && !text && !urlContains && !loadState) throw new Error("browser wait requires a condition");
|
|
220
|
+
return { tabId: params.tabId, frameId, selector, state: waitState, text, urlContains, loadState, timeoutMs };
|
|
57
221
|
}
|
|
58
|
-
|
|
222
|
+
|
|
223
|
+
function pageWaitPayload(params) {
|
|
224
|
+
return { selector: params.selector, state: params.state, text: params.text, loadState: params.loadState };
|
|
225
|
+
}
|
|
226
|
+
|
|
59
227
|
async function browserWait(params, state) {
|
|
228
|
+
params = normalizeBrowserWaitParams(params);
|
|
60
229
|
const tab = await resolveTab(params.tabId);
|
|
61
|
-
const timeoutMs =
|
|
230
|
+
const timeoutMs = params.timeoutMs;
|
|
62
231
|
const startedAt = performance.now();
|
|
63
232
|
let last = null;
|
|
64
233
|
while (performance.now() - startedAt <= timeoutMs) {
|
|
65
234
|
throwIfCancelled(state);
|
|
66
235
|
const current = await chrome.tabs.get(tab.id);
|
|
67
|
-
const
|
|
236
|
+
const currentUrl = exactOptionalAuthorityString(current?.url, 32768);
|
|
237
|
+
const urlMatched = !params.urlContains || currentUrl.includes(params.urlContains);
|
|
68
238
|
const needsPage = Boolean(params.selector || params.text || params.loadState);
|
|
69
239
|
let page = { matched: true, ready_state: current.status || "" };
|
|
70
240
|
if (needsPage) {
|
|
71
241
|
try {
|
|
72
242
|
assertPageTab(current);
|
|
73
|
-
const [execution] = await executePageAutomation(scriptTarget(current.id, params.frameId, false), "checkWait", params);
|
|
243
|
+
const [execution] = await executePageAutomation(scriptTarget(current.id, params.frameId, false), "checkWait", pageWaitPayload(params));
|
|
74
244
|
page = execution?.result || { matched: false };
|
|
75
245
|
} catch (error) {
|
|
76
246
|
const message = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
@@ -87,23 +257,32 @@
|
|
|
87
257
|
throw new Error(`browser wait timed out; last condition: ${JSON.stringify(last || {})}`);
|
|
88
258
|
}
|
|
89
259
|
function scriptTarget(tabId, frameId, allFrames) {
|
|
90
|
-
if (Number.
|
|
91
|
-
if (
|
|
260
|
+
if (!Number.isSafeInteger(tabId) || tabId < 1) throw new Error("browser script target tab is invalid");
|
|
261
|
+
if (frameId !== undefined && frameId !== null) {
|
|
262
|
+
if (!Number.isSafeInteger(frameId) || frameId < 0) throw new Error("browser script target frame is invalid");
|
|
263
|
+
return { tabId, frameIds: [frameId] };
|
|
264
|
+
}
|
|
265
|
+
if (allFrames === true) return { tabId, allFrames: true };
|
|
266
|
+
if (allFrames !== undefined && allFrames !== false) throw new Error("browser script allFrames flag is invalid");
|
|
92
267
|
return { tabId };
|
|
93
268
|
}
|
|
94
269
|
|
|
95
270
|
async function resolveTab(tabId) {
|
|
96
|
-
if (
|
|
271
|
+
if (tabId !== undefined && tabId !== null) {
|
|
272
|
+
if (!Number.isSafeInteger(tabId) || tabId < 1) throw new Error("browser tab id is invalid");
|
|
273
|
+
return chrome.tabs.get(tabId);
|
|
274
|
+
}
|
|
97
275
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
98
276
|
if (!tab?.id) throw new Error("no active browser tab");
|
|
99
277
|
return tab;
|
|
100
278
|
}
|
|
101
279
|
|
|
102
280
|
async function getSource(params, state) {
|
|
281
|
+
const maxBytes = exactBoundedInteger(params.maxBytes, "browser source maxBytes", 1024 * 1024, 1, 4 * 1024 * 1024);
|
|
282
|
+
const allFrames = exactBoolean(params.allFrames, "browser source allFrames", false);
|
|
103
283
|
const tab = await resolveTab(params.tabId);
|
|
104
284
|
assertPageTab(tab);
|
|
105
|
-
const
|
|
106
|
-
const selection = await discoverPageFrames(tab.id, params.frameId, params.allFrames === true);
|
|
285
|
+
const selection = await discoverPageFrames(tab.id, params.frameId, allFrames);
|
|
107
286
|
let remainingBytes = maxBytes;
|
|
108
287
|
const frames = [];
|
|
109
288
|
for (let index = 0; index < selection.frames.length && remainingBytes > 0; index += 1) {
|
|
@@ -116,7 +295,10 @@
|
|
|
116
295
|
args: [frameBudget],
|
|
117
296
|
});
|
|
118
297
|
const result = execution?.result || { source: "", bytes: 0, returned_bytes: 0, truncated: true, url: selection.frames[index].url };
|
|
119
|
-
const
|
|
298
|
+
const rawReturnedBytes = result.returned_bytes ?? result.bytes;
|
|
299
|
+
const returnedBytes = Number.isSafeInteger(rawReturnedBytes) && rawReturnedBytes >= 0
|
|
300
|
+
? Math.min(frameBudget, rawReturnedBytes)
|
|
301
|
+
: frameBudget;
|
|
120
302
|
remainingBytes -= returnedBytes;
|
|
121
303
|
frames.push({ frame_id: execution?.frameId ?? selection.frames[index].frameId, ...result, returned_bytes: returnedBytes, bytes: returnedBytes });
|
|
122
304
|
}
|
|
@@ -132,37 +314,833 @@
|
|
|
132
314
|
}
|
|
133
315
|
|
|
134
316
|
async function inspectPage(params, state) {
|
|
317
|
+
const maxElements = exactBoundedInteger(params.maxElements, "browser inspection maxElements", 300, 1, 1000);
|
|
318
|
+
const allFrames = exactBoolean(params.allFrames, "browser inspection allFrames", true);
|
|
319
|
+
const includeValues = exactBoolean(params.includeValues, "browser inspection includeValues", false);
|
|
320
|
+
const includePrivateHistory = exactBoolean(params.includePrivateHistory, "browser inspection includePrivateHistory", false);
|
|
321
|
+
const focusQueryInput = exactOptionalText(params.focusQuery, "browser inspection focusQuery", 1000);
|
|
135
322
|
const tab = await resolveTab(params.tabId);
|
|
136
323
|
assertPageTab(tab);
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
324
|
+
const selection = await discoverPageFrames(tab.id, params.frameId, allFrames, MAX_ACCESSIBLE_FRAMES);
|
|
325
|
+
const frameCount = Math.max(1, selection.frames.length);
|
|
326
|
+
const probeBudget = Math.max(1, Math.min(maxElements, Math.ceil((maxElements * 2) / frameCount)));
|
|
327
|
+
const probedFrames = [];
|
|
328
|
+
const candidates = [];
|
|
329
|
+
for (let frameIndex = 0; frameIndex < selection.frames.length; frameIndex += 1) {
|
|
142
330
|
throwIfCancelled(state);
|
|
143
|
-
const
|
|
144
|
-
const frameBudget = Math.max(1, Math.floor(remainingElements / remainingFrames));
|
|
331
|
+
const frame = selection.frames[frameIndex];
|
|
145
332
|
const [execution] = await executePageAutomation(
|
|
146
|
-
{ tabId: tab.id, frameIds: [
|
|
333
|
+
{ tabId: tab.id, frameIds: [frame.frameId] },
|
|
147
334
|
"inspect",
|
|
148
|
-
{
|
|
335
|
+
{
|
|
336
|
+
maxElements: probeBudget,
|
|
337
|
+
includeValues,
|
|
338
|
+
focusQuery: focusQueryInput,
|
|
339
|
+
includePrivateHistory,
|
|
340
|
+
},
|
|
149
341
|
);
|
|
150
342
|
const result = execution?.result || { elements: [], truncated: true };
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
343
|
+
const actualFrameId = execution?.frameId ?? frame.frameId;
|
|
344
|
+
const elements = Array.isArray(result.elements) ? result.elements : [];
|
|
345
|
+
probedFrames.push({ frame_id: actualFrameId, ...result, elements: [] });
|
|
346
|
+
for (let elementIndex = 0; elementIndex < elements.length; elementIndex += 1) {
|
|
347
|
+
const element = elements[elementIndex];
|
|
348
|
+
candidates.push({
|
|
349
|
+
frameIndex,
|
|
350
|
+
frameId: actualFrameId,
|
|
351
|
+
element,
|
|
352
|
+
elementIndex,
|
|
353
|
+
score: typeof element?.salience_score === "number" && Number.isFinite(element.salience_score) ? element.salience_score : 0,
|
|
354
|
+
topFrame: actualFrameId === 0,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
154
357
|
}
|
|
358
|
+
candidates.sort((left, right) =>
|
|
359
|
+
right.score - left.score
|
|
360
|
+
|| (right.topFrame ? 1 : 0) - (left.topFrame ? 1 : 0)
|
|
361
|
+
|| left.frameId - right.frameId
|
|
362
|
+
|| left.elementIndex - right.elementIndex);
|
|
363
|
+
const selected = candidates.slice(0, maxElements);
|
|
364
|
+
for (const candidate of selected) probedFrames[candidate.frameIndex].elements.push(candidate.element);
|
|
365
|
+
const probeTruncated = probedFrames.some((frame) => frame.truncated === true);
|
|
366
|
+
const focusQuery = focusQueryInput.trim().replace(/\s+/g, " ").toLowerCase();
|
|
367
|
+
const queryMatchCount = focusQuery
|
|
368
|
+
? probedFrames.reduce((sum, frame) => {
|
|
369
|
+
const count = exactNonNegativeInteger(frame.document?.focus_query_match_count);
|
|
370
|
+
return sum + (count ?? 0);
|
|
371
|
+
}, 0)
|
|
372
|
+
: null;
|
|
373
|
+
const querySearchExhaustive = focusQuery
|
|
374
|
+
? selection.truncated !== true && probedFrames.every((frame) => frame.document?.focus_query_search_exhaustive === true)
|
|
375
|
+
: null;
|
|
155
376
|
return {
|
|
156
377
|
tab_id: tab.id,
|
|
157
378
|
title: tab.title || "",
|
|
158
379
|
url: tab.url || "",
|
|
159
|
-
frames,
|
|
160
|
-
total_elements:
|
|
380
|
+
frames: probedFrames,
|
|
381
|
+
total_elements: selected.length,
|
|
161
382
|
max_elements: maxElements,
|
|
162
383
|
frames_truncated: selection.truncated,
|
|
384
|
+
selection: {
|
|
385
|
+
strategy: "global_salience",
|
|
386
|
+
frames_scanned: probedFrames.length,
|
|
387
|
+
probed_elements: candidates.length,
|
|
388
|
+
per_frame_probe_budget: probeBudget,
|
|
389
|
+
candidate_truncated: probeTruncated || candidates.length > selected.length,
|
|
390
|
+
focus_query: focusQuery,
|
|
391
|
+
query_matched: focusQuery ? queryMatchCount > 0 : null,
|
|
392
|
+
query_match_count: queryMatchCount,
|
|
393
|
+
query_search_exhaustive: querySearchExhaustive,
|
|
394
|
+
},
|
|
163
395
|
};
|
|
164
396
|
}
|
|
165
|
-
|
|
397
|
+
|
|
398
|
+
async function documentState(params) {
|
|
399
|
+
const tab = await resolveTab(params.tabId);
|
|
400
|
+
assertPageTab(tab);
|
|
401
|
+
const [execution] = await executePageAutomation({ tabId: tab.id, frameIds: [0] }, "documentState", {});
|
|
402
|
+
const state = execution?.result || {};
|
|
403
|
+
return {
|
|
404
|
+
tab_id: tab.id,
|
|
405
|
+
title: tab.title || "",
|
|
406
|
+
url: tab.url || "",
|
|
407
|
+
document_epoch: exactOptionalAuthorityString(state.epoch, 9000),
|
|
408
|
+
document_url: exactOptionalAuthorityString(state.url, 8192),
|
|
409
|
+
_machine_history_entry_key: exactOptionalAuthorityString(state._machine_history_entry_key, 512),
|
|
410
|
+
ready_state: String(state.ready_state || ""),
|
|
411
|
+
viewport: state.viewport && typeof state.viewport === "object" ? state.viewport : null,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function pointAction(params, state) {
|
|
416
|
+
const tabId = exactPositiveInteger(params.tabId);
|
|
417
|
+
if (tabId === null) throw new Error("visual snapshot tab id is required before dispatch");
|
|
418
|
+
const expectedDocumentEpoch = requiredSnapshotAuthorityString(params.expectedDocumentEpoch, "visual snapshot document epoch", 9000);
|
|
419
|
+
const expectedViewport = requiredSnapshotViewport(params.expectedViewport);
|
|
420
|
+
const expectedScreenshotSha256 = exactOptionalSha256(params.expectedScreenshotSha256, "visual snapshot digest");
|
|
421
|
+
if (!expectedScreenshotSha256) throw new Error("visual snapshot digest is required before dispatch");
|
|
422
|
+
const visualSnapshot = {
|
|
423
|
+
expectedScreenshotSha256,
|
|
424
|
+
screenshotFormat: exactImageFormat(params.screenshotFormat, "visual snapshot format"),
|
|
425
|
+
screenshotQuality: exactBoundedInteger(params.screenshotQuality, "visual snapshot quality", 90, 1, 100),
|
|
426
|
+
};
|
|
427
|
+
throwIfCancelled(state);
|
|
428
|
+
const tab = await resolveTab(tabId);
|
|
429
|
+
assertPageTab(tab);
|
|
430
|
+
const action = exactOptionalAuthorityString(params.action, 32);
|
|
431
|
+
if (!["click", "double_click", "hover", "drag", "scroll"].includes(action)) throw new Error("visual point action must be click, double_click, hover, drag, or scroll");
|
|
432
|
+
const scrollDeltas = action === "scroll" ? normalizeScrollDeltas(params) : null;
|
|
433
|
+
const normalizedX = exactNormalizedCoordinate(params.normalizedX);
|
|
434
|
+
const normalizedY = exactNormalizedCoordinate(params.normalizedY);
|
|
435
|
+
if (normalizedX === null || normalizedY === null) {
|
|
436
|
+
throw new Error("visual point must use normalized viewport coordinates from 0 (inclusive) to 1 (exclusive)");
|
|
437
|
+
}
|
|
438
|
+
const currentState = await documentState({ tabId: tab.id });
|
|
439
|
+
if (currentState.document_epoch !== expectedDocumentEpoch) {
|
|
440
|
+
throw new Error("computer visual snapshot is stale because the document was replaced");
|
|
441
|
+
}
|
|
442
|
+
const viewport = currentState.viewport || {};
|
|
443
|
+
const width = exactPositiveFiniteNumber(viewport.width);
|
|
444
|
+
const height = exactPositiveFiniteNumber(viewport.height);
|
|
445
|
+
const scale = exactPositiveFiniteNumber(viewport.scale);
|
|
446
|
+
const offsetLeft = exactFiniteNumber(viewport.offset_left);
|
|
447
|
+
const offsetTop = exactFiniteNumber(viewport.offset_top);
|
|
448
|
+
if (width === null || height === null || scale === null || offsetLeft === null || offsetTop === null) {
|
|
449
|
+
throw new Error("browser viewport dimensions are unavailable for visual point action");
|
|
450
|
+
}
|
|
451
|
+
if (Math.abs(scale - 1) > 0.001 || Math.abs(offsetLeft) > 0.5 || Math.abs(offsetTop) > 0.5) {
|
|
452
|
+
throw new Error("visual point action is unavailable while the visual viewport is zoomed or offset");
|
|
453
|
+
}
|
|
454
|
+
if (expectedViewport && !sameViewport(expectedViewport, viewport)) {
|
|
455
|
+
throw new Error("computer visual snapshot is stale because the viewport changed");
|
|
456
|
+
}
|
|
457
|
+
const point = { x: normalizedX * width, y: normalizedY * height };
|
|
458
|
+
const [probe] = await executePageAutomation({ tabId: tab.id, frameIds: [0] }, "pointProbe", point);
|
|
459
|
+
let destinationPoint = null;
|
|
460
|
+
let destinationProbe = null;
|
|
461
|
+
if (action === "drag") {
|
|
462
|
+
const destinationNormalizedX = exactNormalizedCoordinate(params.destinationNormalizedX);
|
|
463
|
+
const destinationNormalizedY = exactNormalizedCoordinate(params.destinationNormalizedY);
|
|
464
|
+
if (destinationNormalizedX === null || destinationNormalizedY === null) {
|
|
465
|
+
throw new Error("visual drag destination must use normalized viewport coordinates from 0 (inclusive) to 1 (exclusive)");
|
|
466
|
+
}
|
|
467
|
+
destinationPoint = { x: destinationNormalizedX * width, y: destinationNormalizedY * height };
|
|
468
|
+
[destinationProbe] = await executePageAutomation({ tabId: tab.id, frameIds: [0] }, "pointProbe", destinationPoint);
|
|
469
|
+
}
|
|
470
|
+
throwIfCancelled(state);
|
|
471
|
+
try {
|
|
472
|
+
const api = globalThis.__machineBridgeDevtoolsInput;
|
|
473
|
+
if (!api?.perform) throw trustedUnavailableError();
|
|
474
|
+
await api.perform(tab.id, action, {
|
|
475
|
+
point,
|
|
476
|
+
...(destinationPoint ? { destinationPoint } : {}),
|
|
477
|
+
...(scrollDeltas || {}),
|
|
478
|
+
...(visualSnapshot || {}),
|
|
479
|
+
beforeDispatch: () => throwIfCancelledBeforeTrustedInput(state),
|
|
480
|
+
});
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error?.machineBridgeBeforeDispatchAbort === true) throw error;
|
|
483
|
+
if (error?.safeToFallback === true) throw error;
|
|
484
|
+
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
485
|
+
throw new Error(`trusted browser input may have been partially dispatched; the action outcome is unknown. Inspect the page before retrying. (${detail})`);
|
|
486
|
+
}
|
|
487
|
+
const current = await postActionTabMetadata(tab.id);
|
|
488
|
+
return {
|
|
489
|
+
ok: true,
|
|
490
|
+
input_mode: "trusted",
|
|
491
|
+
trusted_input_fallback: false,
|
|
492
|
+
...current,
|
|
493
|
+
point: { normalized_x: normalizedX, normalized_y: normalizedY, css_x: point.x, css_y: point.y },
|
|
494
|
+
...(scrollDeltas ? { scroll_delta: { delta_x: scrollDeltas.deltaX, delta_y: scrollDeltas.deltaY } } : {}),
|
|
495
|
+
...(destinationPoint ? { destination_point: {
|
|
496
|
+
normalized_x: params.destinationNormalizedX, normalized_y: params.destinationNormalizedY,
|
|
497
|
+
css_x: destinationPoint.x, css_y: destinationPoint.y,
|
|
498
|
+
} } : {}),
|
|
499
|
+
hit: probe?.result?.hit || null,
|
|
500
|
+
...(destinationPoint ? { destination_hit: destinationProbe?.result?.hit || null } : {}),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function backendNodeAction(params, state) {
|
|
505
|
+
const tabId = exactPositiveInteger(params.tabId);
|
|
506
|
+
if (tabId === null) throw new Error("snapshot backend tab id is required before dispatch");
|
|
507
|
+
const expectedDocumentEpoch = requiredSnapshotAuthorityString(params.expectedDocumentEpoch, "snapshot backend document epoch", 9000);
|
|
508
|
+
throwIfCancelled(state);
|
|
509
|
+
const tab = await resolveTab(tabId);
|
|
510
|
+
assertPageTab(tab);
|
|
511
|
+
const action = exactOptionalAuthorityString(params.action, 32);
|
|
512
|
+
const pointerActions = new Set(["click", "double_click", "hover", "drag", "scroll"]);
|
|
513
|
+
const focusActions = new Set(["press", "type_text", "fill", "check", "uncheck", "submit"]);
|
|
514
|
+
if (!pointerActions.has(action) && !focusActions.has(action)) {
|
|
515
|
+
throw new Error("snapshot backend action must be click, double_click, hover, drag, scroll, press, type_text, fill, check, uncheck, or submit");
|
|
516
|
+
}
|
|
517
|
+
const backendNodeId = exactPositiveInteger(params.backendNodeId);
|
|
518
|
+
if (backendNodeId === null) throw new Error("snapshot backend action requires a valid backend node");
|
|
519
|
+
const destinationBackendNodeId = action === "drag" ? exactPositiveInteger(params.destinationBackendNodeId) : null;
|
|
520
|
+
if (action === "drag" && destinationBackendNodeId === null) {
|
|
521
|
+
throw new Error("snapshot backend drag requires a valid destination backend node");
|
|
522
|
+
}
|
|
523
|
+
const scrollDeltas = action === "scroll" ? normalizeScrollDeltas(params) : null;
|
|
524
|
+
const currentState = await documentState({ tabId: tab.id });
|
|
525
|
+
if (currentState.document_epoch !== expectedDocumentEpoch) {
|
|
526
|
+
throw new Error("snapshot_backend_target_changed_before_dispatch");
|
|
527
|
+
}
|
|
528
|
+
if (params.expectedFrameDocumentEpoch) {
|
|
529
|
+
const extensionFrameId = exactNonNegativeInteger(params.extensionFrameId);
|
|
530
|
+
let frameState = null;
|
|
531
|
+
if (extensionFrameId === 0) {
|
|
532
|
+
frameState = { epoch: currentState.document_epoch, url: currentState.document_url };
|
|
533
|
+
} else if (Number.isInteger(extensionFrameId) && extensionFrameId > 0) {
|
|
534
|
+
try {
|
|
535
|
+
const [execution] = await executePageAutomation({ tabId: tab.id, frameIds: [extensionFrameId] }, "documentState", {});
|
|
536
|
+
frameState = execution?.result || null;
|
|
537
|
+
} catch { /* Missing frame state fails closed below as a stale snapshot target. */ }
|
|
538
|
+
}
|
|
539
|
+
if (!frameState
|
|
540
|
+
|| exactOptionalAuthorityString(frameState.epoch, 9000) !== exactOptionalAuthorityString(params.expectedFrameDocumentEpoch, 9000)
|
|
541
|
+
|| (params.expectedFrameUrl && exactOptionalAuthorityString(frameState.url, 8192) !== exactOptionalAuthorityString(params.expectedFrameUrl, 8192))) {
|
|
542
|
+
throw new Error("snapshot_backend_target_changed_before_dispatch");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (params.extensionRef && params.expectedIdentity) {
|
|
546
|
+
const extensionFrameId = exactNonNegativeInteger(params.extensionFrameId);
|
|
547
|
+
let identityMatched = false;
|
|
548
|
+
if (Number.isInteger(extensionFrameId) && extensionFrameId >= 0) {
|
|
549
|
+
try {
|
|
550
|
+
const [execution] = await executePageAutomation({ tabId: tab.id, frameIds: [extensionFrameId] }, "refIdentity", {
|
|
551
|
+
ref: params.extensionRef,
|
|
552
|
+
expectedIdentity: params.expectedIdentity,
|
|
553
|
+
});
|
|
554
|
+
identityMatched = execution?.result?.attached === true && execution?.result?.matched === true;
|
|
555
|
+
} catch { /* Failed identity read leaves identityMatched=false and fails closed below. */ }
|
|
556
|
+
}
|
|
557
|
+
if (!identityMatched) throw new Error("snapshot_backend_target_changed_before_dispatch");
|
|
558
|
+
}
|
|
559
|
+
if (action === "drag") {
|
|
560
|
+
if (params.destinationExpectedFrameDocumentEpoch) {
|
|
561
|
+
const destinationExtensionFrameId = exactNonNegativeInteger(params.destinationExtensionFrameId);
|
|
562
|
+
let frameState = null;
|
|
563
|
+
if (destinationExtensionFrameId === 0) {
|
|
564
|
+
frameState = { epoch: currentState.document_epoch, url: currentState.document_url };
|
|
565
|
+
} else if (Number.isInteger(destinationExtensionFrameId) && destinationExtensionFrameId > 0) {
|
|
566
|
+
try {
|
|
567
|
+
const [execution] = await executePageAutomation({ tabId: tab.id, frameIds: [destinationExtensionFrameId] }, "documentState", {});
|
|
568
|
+
frameState = execution?.result || null;
|
|
569
|
+
} catch { /* Missing destination frame state fails closed below as a stale snapshot target. */ }
|
|
570
|
+
}
|
|
571
|
+
if (!frameState
|
|
572
|
+
|| exactOptionalAuthorityString(frameState.epoch, 9000) !== exactOptionalAuthorityString(params.destinationExpectedFrameDocumentEpoch, 9000)
|
|
573
|
+
|| (params.destinationExpectedFrameUrl && exactOptionalAuthorityString(frameState.url, 8192) !== exactOptionalAuthorityString(params.destinationExpectedFrameUrl, 8192))) {
|
|
574
|
+
throw new Error("snapshot_backend_target_changed_before_dispatch");
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (params.destinationExtensionRef && params.destinationExpectedIdentity) {
|
|
578
|
+
const destinationExtensionFrameId = exactNonNegativeInteger(params.destinationExtensionFrameId);
|
|
579
|
+
let identityMatched = false;
|
|
580
|
+
if (Number.isInteger(destinationExtensionFrameId) && destinationExtensionFrameId >= 0) {
|
|
581
|
+
try {
|
|
582
|
+
const [execution] = await executePageAutomation({ tabId: tab.id, frameIds: [destinationExtensionFrameId] }, "refIdentity", {
|
|
583
|
+
ref: params.destinationExtensionRef,
|
|
584
|
+
expectedIdentity: params.destinationExpectedIdentity,
|
|
585
|
+
});
|
|
586
|
+
identityMatched = execution?.result?.attached === true && execution?.result?.matched === true;
|
|
587
|
+
} catch { /* Failed destination identity read fails closed below before dispatch. */ }
|
|
588
|
+
}
|
|
589
|
+
if (!identityMatched) throw new Error("snapshot_backend_target_changed_before_dispatch");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const session = globalThis.__machineBridgeDevtoolsSession;
|
|
593
|
+
const input = globalThis.__machineBridgeDevtoolsInput;
|
|
594
|
+
if (!session?.run || !input?.performWithSend) throw new Error("snapshot_backend_trusted_input_unavailable_before_dispatch");
|
|
595
|
+
let focusApplied = false;
|
|
596
|
+
let scrollApplied = false;
|
|
597
|
+
try {
|
|
598
|
+
const result = await session.run(tab.id, async ({ send }) => {
|
|
599
|
+
throwIfCancelled(state);
|
|
600
|
+
if (action === "check" || action === "uncheck") {
|
|
601
|
+
const desired = action === "check";
|
|
602
|
+
const checked = await backendNodeCheckedState(send, backendNodeId);
|
|
603
|
+
throwIfCancelled(state);
|
|
604
|
+
if (checked === desired) {
|
|
605
|
+
return { point: null, coordinateSource: "cdp_ax_state_noop", noInputRequired: true };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (focusActions.has(action)) {
|
|
609
|
+
await focusBackendNode(send, backendNodeId);
|
|
610
|
+
focusApplied = true;
|
|
611
|
+
throwIfCancelled(state);
|
|
612
|
+
const focusResult = await performBackendFocusAction(input, send, action, params, backendNodeId, state);
|
|
613
|
+
return { point: null, coordinateSource: "cdp_dom_focus", ...focusResult };
|
|
614
|
+
}
|
|
615
|
+
if (action === "drag") {
|
|
616
|
+
const sourceGeometry = await backendNodeViewportGeometry(send, backendNodeId);
|
|
617
|
+
throwIfCancelled(state);
|
|
618
|
+
const destinationGeometry = await backendNodeViewportGeometry(send, destinationBackendNodeId);
|
|
619
|
+
throwIfCancelled(state);
|
|
620
|
+
if (!sourceGeometry.insideViewport || !destinationGeometry.insideViewport) {
|
|
621
|
+
throw new Error("snapshot_backend_drag_geometry_unavailable_before_dispatch");
|
|
622
|
+
}
|
|
623
|
+
await input.performWithSend(send, "drag", {
|
|
624
|
+
point: sourceGeometry.point,
|
|
625
|
+
destinationPoint: destinationGeometry.point,
|
|
626
|
+
});
|
|
627
|
+
return {
|
|
628
|
+
point: sourceGeometry.point,
|
|
629
|
+
destinationPoint: destinationGeometry.point,
|
|
630
|
+
coordinateSource: "cdp_content_quad",
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
if (action === "scroll") {
|
|
634
|
+
const geometry = await backendNodeViewportGeometry(send, backendNodeId);
|
|
635
|
+
throwIfCancelled(state);
|
|
636
|
+
if (!geometry.insideViewport) throw new Error("snapshot_backend_scroll_geometry_unavailable_before_dispatch");
|
|
637
|
+
await input.performWithSend(send, "scroll", {
|
|
638
|
+
point: geometry.point,
|
|
639
|
+
deltaX: scrollDeltas.deltaX,
|
|
640
|
+
deltaY: scrollDeltas.deltaY,
|
|
641
|
+
});
|
|
642
|
+
return {
|
|
643
|
+
point: geometry.point,
|
|
644
|
+
scrollDelta: scrollDeltas,
|
|
645
|
+
coordinateSource: "cdp_content_quad",
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
let geometry = await backendNodeViewportGeometry(send, backendNodeId);
|
|
649
|
+
throwIfCancelled(state);
|
|
650
|
+
if (!geometry.insideViewport) {
|
|
651
|
+
await scrollBackendNodeIntoView(send, backendNodeId);
|
|
652
|
+
scrollApplied = true;
|
|
653
|
+
throwIfCancelled(state);
|
|
654
|
+
geometry = await backendNodeViewportGeometry(send, backendNodeId);
|
|
655
|
+
if (!geometry.insideViewport) throw new Error("snapshot_backend_geometry_unavailable_before_dispatch");
|
|
656
|
+
}
|
|
657
|
+
const point = geometry.point;
|
|
658
|
+
throwIfCancelled(state);
|
|
659
|
+
await input.performWithSend(send, action, { point });
|
|
660
|
+
return { point, coordinateSource: "cdp_content_quad" };
|
|
661
|
+
}, { beforeAttach: () => throwIfCancelledBeforeTrustedInput(state) });
|
|
662
|
+
const current = await postActionTabMetadata(tab.id);
|
|
663
|
+
return {
|
|
664
|
+
ok: true,
|
|
665
|
+
input_mode: "trusted",
|
|
666
|
+
trusted_input_fallback: false,
|
|
667
|
+
coordinate_source: result.coordinateSource,
|
|
668
|
+
cross_frame_trusted: exactNonNegativeInteger(params.extensionFrameId) > 0,
|
|
669
|
+
...current,
|
|
670
|
+
no_input_required: result.noInputRequired === true,
|
|
671
|
+
...(result.point ? { point: result.point } : {}),
|
|
672
|
+
...(result.destinationPoint ? { destination_point: result.destinationPoint } : {}),
|
|
673
|
+
...(result.scrollDelta ? { scroll_delta: {
|
|
674
|
+
delta_x: result.scrollDelta.deltaX, delta_y: result.scrollDelta.deltaY,
|
|
675
|
+
} } : {}),
|
|
676
|
+
};
|
|
677
|
+
} catch (error) {
|
|
678
|
+
if (error?.machineBridgeFocusOutcomeUnknown === true
|
|
679
|
+
|| error?.machineBridgeScrollOutcomeUnknown === true
|
|
680
|
+
|| focusApplied
|
|
681
|
+
|| scrollApplied) {
|
|
682
|
+
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
683
|
+
throw new Error(`trusted browser input may have been partially dispatched; the action outcome is unknown. Inspect the page before retrying. (${detail})`);
|
|
684
|
+
}
|
|
685
|
+
if (error?.machineBridgeTrustedInput === true) {
|
|
686
|
+
if (error.safeToFallback === true) throw new Error("snapshot_backend_trusted_input_unavailable_before_dispatch");
|
|
687
|
+
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
688
|
+
throw new Error(`trusted browser input may have been partially dispatched; the action outcome is unknown. Inspect the page before retrying. (${detail})`);
|
|
689
|
+
}
|
|
690
|
+
throw error;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function scrollBackendNodeIntoView(send, backendNodeId) {
|
|
695
|
+
try { await send("DOM.scrollIntoViewIfNeeded", { backendNodeId }); } catch (error) {
|
|
696
|
+
if (backendTargetChanged(error)) throw new Error("snapshot_backend_target_changed_before_dispatch");
|
|
697
|
+
const failure = new Error("snapshot_backend_scroll_outcome_unknown");
|
|
698
|
+
Object.defineProperty(failure, "machineBridgeScrollOutcomeUnknown", { value: true });
|
|
699
|
+
throw failure;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function focusBackendNode(send, backendNodeId) {
|
|
704
|
+
try { await send("DOM.focus", { backendNodeId }); } catch (error) {
|
|
705
|
+
if (backendTargetChanged(error)) throw new Error("snapshot_backend_target_changed_before_dispatch");
|
|
706
|
+
const failure = new Error("snapshot_backend_focus_outcome_unknown");
|
|
707
|
+
Object.defineProperty(failure, "machineBridgeFocusOutcomeUnknown", { value: true });
|
|
708
|
+
throw failure;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function backendNodeViewportGeometry(send, backendNodeId) {
|
|
713
|
+
let quads;
|
|
714
|
+
try {
|
|
715
|
+
quads = (await send("DOM.getContentQuads", { backendNodeId }))?.quads;
|
|
716
|
+
} catch (error) {
|
|
717
|
+
if (backendTargetChanged(error)) throw new Error("snapshot_backend_target_changed_before_dispatch");
|
|
718
|
+
throw new Error("snapshot_backend_geometry_unavailable_before_dispatch");
|
|
719
|
+
}
|
|
720
|
+
const point = pointFromContentQuads(quads);
|
|
721
|
+
if (!point) throw new Error("snapshot_backend_geometry_unavailable_before_dispatch");
|
|
722
|
+
let metrics;
|
|
723
|
+
try { metrics = await send("Page.getLayoutMetrics"); }
|
|
724
|
+
catch { throw new Error("snapshot_backend_geometry_unavailable_before_dispatch"); }
|
|
725
|
+
const insideViewport = pointInsideViewport(point, metrics);
|
|
726
|
+
if (insideViewport === null) throw new Error("snapshot_backend_geometry_unavailable_before_dispatch");
|
|
727
|
+
return { point, insideViewport };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function performBackendFocusAction(input, send, action, params, backendNodeId, state) {
|
|
731
|
+
if (action === "press") {
|
|
732
|
+
await input.performWithSend(send, "press", { key: boundedBackendKey(params.key) || "Enter" });
|
|
733
|
+
return {};
|
|
734
|
+
}
|
|
735
|
+
if (action === "check" || action === "uncheck") {
|
|
736
|
+
const desired = action === "check";
|
|
737
|
+
const checked = await backendNodeCheckedState(send, backendNodeId);
|
|
738
|
+
throwIfCancelled(state);
|
|
739
|
+
if (checked === desired) return { noInputRequired: true };
|
|
740
|
+
await input.performWithSend(send, "press", { key: "Space" });
|
|
741
|
+
return { noInputRequired: false };
|
|
742
|
+
}
|
|
743
|
+
if (action === "submit") {
|
|
744
|
+
await input.performWithSend(send, "press", { key: "Enter" });
|
|
745
|
+
return {};
|
|
746
|
+
}
|
|
747
|
+
const text = boundedBackendText(params.value);
|
|
748
|
+
if (action === "type_text") {
|
|
749
|
+
await input.performWithSend(send, "type_text", { text });
|
|
750
|
+
return {};
|
|
751
|
+
}
|
|
752
|
+
if (action === "fill") {
|
|
753
|
+
await input.performWithSend(send, "fill_text", { text, selectAllKey: selectAllShortcut() });
|
|
754
|
+
return {};
|
|
755
|
+
}
|
|
756
|
+
throw new Error("unsupported snapshot backend focus action");
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function backendNodeCheckedState(send, backendNodeId) {
|
|
760
|
+
let nodes;
|
|
761
|
+
try {
|
|
762
|
+
nodes = (await send("Accessibility.getPartialAXTree", {
|
|
763
|
+
backendNodeId,
|
|
764
|
+
fetchRelatives: false,
|
|
765
|
+
}))?.nodes;
|
|
766
|
+
} catch (error) {
|
|
767
|
+
if (backendTargetChanged(error)) throw new Error("snapshot_backend_target_changed_before_dispatch");
|
|
768
|
+
throw new Error("snapshot_backend_checked_state_unavailable_before_dispatch");
|
|
769
|
+
}
|
|
770
|
+
const candidates = Array.isArray(nodes) ? nodes : [];
|
|
771
|
+
const node = candidates.find((item) => Number.isSafeInteger(item?.backendDOMNodeId) && item.backendDOMNodeId === backendNodeId) || null;
|
|
772
|
+
const property = Array.isArray(node?.properties)
|
|
773
|
+
? node.properties.find((item) => item?.name === "checked")
|
|
774
|
+
: null;
|
|
775
|
+
const value = property?.value?.value;
|
|
776
|
+
if (value === true || value === 1 || value === "true") return true;
|
|
777
|
+
if (value === false || value === 0 || value === "false") return false;
|
|
778
|
+
throw new Error("snapshot_backend_checked_state_unavailable_before_dispatch");
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function boundedBackendText(value) {
|
|
782
|
+
if (typeof value !== "string") throw new Error("snapshot backend text action requires value");
|
|
783
|
+
if (value.includes("\0") || value.length > 128 * 1024) throw new Error("snapshot backend text value exceeds the maximum length or contains a NUL byte");
|
|
784
|
+
return value;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function boundedBackendKey(value) {
|
|
788
|
+
if (value === undefined || value === "") return "";
|
|
789
|
+
if (typeof value !== "string" || value.includes("\0") || value.length > 100) throw new Error("snapshot backend key is invalid");
|
|
790
|
+
return value;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function selectAllShortcut() {
|
|
794
|
+
const platform = String(globalThis.navigator?.userAgentData?.platform || globalThis.navigator?.platform || "").toLowerCase();
|
|
795
|
+
return platform.includes("mac") ? "Meta+A" : "Control+A";
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function pointFromContentQuads(quads) {
|
|
799
|
+
let best = null;
|
|
800
|
+
let bestArea = 0;
|
|
801
|
+
for (const quad of Array.isArray(quads) ? quads : []) {
|
|
802
|
+
if (!Array.isArray(quad) || quad.length !== 8 || !quad.every((value) => typeof value === "number" && Number.isFinite(value))) continue;
|
|
803
|
+
const points = [];
|
|
804
|
+
for (let index = 0; index < 8; index += 2) points.push({ x: quad[index], y: quad[index + 1] });
|
|
805
|
+
let twiceArea = 0;
|
|
806
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
807
|
+
const next = points[(index + 1) % points.length];
|
|
808
|
+
twiceArea += points[index].x * next.y - next.x * points[index].y;
|
|
809
|
+
}
|
|
810
|
+
const area = Math.abs(twiceArea) / 2;
|
|
811
|
+
if (area <= bestArea) continue;
|
|
812
|
+
bestArea = area;
|
|
813
|
+
best = {
|
|
814
|
+
x: points.reduce((sum, point) => sum + point.x, 0) / points.length,
|
|
815
|
+
y: points.reduce((sum, point) => sum + point.y, 0) / points.length,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
return bestArea > 0 ? best : null;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function pointInsideViewport(point, metrics) {
|
|
822
|
+
const viewport = metrics?.cssVisualViewport || metrics?.visualViewport || {};
|
|
823
|
+
const width = exactPositiveFiniteNumber(viewport.clientWidth ?? viewport.width);
|
|
824
|
+
const height = exactPositiveFiniteNumber(viewport.clientHeight ?? viewport.height);
|
|
825
|
+
if (width === null || height === null) return null;
|
|
826
|
+
return point.x >= 0 && point.y >= 0 && point.x < width && point.y < height;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function backendTargetChanged(error) {
|
|
830
|
+
const message = String(error?.message || error || "").toLowerCase();
|
|
831
|
+
return /(?:node|backend).*(?:not found|does not exist|detached)/.test(message)
|
|
832
|
+
|| /(?:not found|does not exist|detached|could not find|no such|no)\s+.*(?:node|backend)/.test(message)
|
|
833
|
+
|| /could not find.*(?:node|backend)/.test(message);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function normalizeScrollDeltas(params) {
|
|
837
|
+
const deltaX = normalizeScrollDelta(params.deltaX, "deltaX");
|
|
838
|
+
const deltaY = normalizeScrollDelta(params.deltaY, "deltaY");
|
|
839
|
+
if (deltaX === 0 && deltaY === 0) throw new Error("scroll requires a non-zero delta");
|
|
840
|
+
return { deltaX, deltaY };
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function normalizeScrollDelta(value, label) {
|
|
844
|
+
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > 10_000) {
|
|
845
|
+
throw new Error(`${label} must be a finite number from -10000 to 10000`);
|
|
846
|
+
}
|
|
847
|
+
return Object.is(value, -0) ? 0 : value;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function sameViewport(expected, current) {
|
|
851
|
+
for (const key of ["width", "height", "scale"]) {
|
|
852
|
+
const left = expected?.[key];
|
|
853
|
+
const right = current?.[key];
|
|
854
|
+
if (typeof left !== "number" || typeof right !== "number"
|
|
855
|
+
|| !Number.isFinite(left) || !Number.isFinite(right) || Math.abs(left - right) > 0.5) return false;
|
|
856
|
+
}
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function trustedUnavailableError() {
|
|
861
|
+
const error = new Error("trusted input module is unavailable");
|
|
862
|
+
Object.defineProperty(error, "safeToFallback", { value: true });
|
|
863
|
+
return error;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function observeComputer(params, state) {
|
|
867
|
+
const normalized = normalizeComputerObservationParams(params);
|
|
868
|
+
let last = null;
|
|
869
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
870
|
+
throwIfCancelled(state);
|
|
871
|
+
last = await captureComputerAttempt(normalized, state);
|
|
872
|
+
if (last.capture.navigation_coherent === true) return last;
|
|
873
|
+
}
|
|
874
|
+
throw new Error("page changed during computer observation; observe again");
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function normalizeComputerObservationParams(params) {
|
|
878
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("computer observation parameters are invalid");
|
|
879
|
+
return {
|
|
880
|
+
tabId: params.tabId,
|
|
881
|
+
allFrames: exactBoolean(params.allFrames, "computer observation allFrames", true),
|
|
882
|
+
maxElements: exactBoundedInteger(params.maxElements, "computer observation maxElements", 300, 1, 1000),
|
|
883
|
+
maxAxNodes: exactBoundedInteger(params.maxAxNodes, "computer observation maxAxNodes", 600, 1, 2000),
|
|
884
|
+
maxFrames: exactBoundedInteger(params.maxFrames, "computer observation maxFrames", 32, 1, MAX_ACCESSIBLE_FRAMES),
|
|
885
|
+
axDepth: exactBoundedInteger(params.axDepth, "computer observation axDepth", 12, 1, 16),
|
|
886
|
+
includeValues: exactBoolean(params.includeValues, "computer observation includeValues", false),
|
|
887
|
+
includeScreenshot: exactBoolean(params.includeScreenshot, "computer observation includeScreenshot", true),
|
|
888
|
+
format: exactImageFormat(params.format, "computer observation format"),
|
|
889
|
+
quality: exactBoundedInteger(params.quality, "computer observation quality", 90, 1, 100),
|
|
890
|
+
focusQuery: exactOptionalText(params.focusQuery, "computer observation focusQuery", 1000),
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
async function captureComputerAttempt(params, state) {
|
|
895
|
+
const tab = await resolveTab(params.tabId);
|
|
896
|
+
assertPageTab(tab);
|
|
897
|
+
const inspected = await inspectPage({
|
|
898
|
+
tabId: tab.id,
|
|
899
|
+
allFrames: params.allFrames,
|
|
900
|
+
maxElements: params.maxElements,
|
|
901
|
+
includeValues: params.includeValues === true,
|
|
902
|
+
focusQuery: params.focusQuery || "",
|
|
903
|
+
includePrivateHistory: true,
|
|
904
|
+
}, state);
|
|
905
|
+
const topFrame = inspected.frames.find((frame) => frame.frame_id === 0) || inspected.frames[0] || null;
|
|
906
|
+
const semanticEpoch = exactOptionalAuthorityString(topFrame?.document?.epoch, 9000);
|
|
907
|
+
let cdp = null;
|
|
908
|
+
let fallbackReason = "";
|
|
909
|
+
try {
|
|
910
|
+
const api = globalThis.__machineBridgeDevtoolsObservation;
|
|
911
|
+
if (!api?.capture) throw new Error("CDP observation module unavailable");
|
|
912
|
+
cdp = await api.capture(tab.id, {
|
|
913
|
+
maxNodes: params.maxAxNodes,
|
|
914
|
+
maxFrames: params.maxFrames,
|
|
915
|
+
depth: params.axDepth,
|
|
916
|
+
includeValues: params.includeValues === true,
|
|
917
|
+
includeScreenshot: params.includeScreenshot,
|
|
918
|
+
format: params.format,
|
|
919
|
+
quality: params.quality,
|
|
920
|
+
focusQuery: params.focusQuery || "",
|
|
921
|
+
}, state);
|
|
922
|
+
} catch (error) {
|
|
923
|
+
if (requestCancelled(error)) throw error;
|
|
924
|
+
fallbackReason = "cdp_observation_unavailable";
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
let fallbackScreenshot = null;
|
|
928
|
+
let screenshotFallbackReason = "";
|
|
929
|
+
if (params.includeScreenshot && !cdp?.screenshot?.data) {
|
|
930
|
+
const [activeTab] = await chrome.tabs.query({ active: true, windowId: tab.windowId }).catch(() => []);
|
|
931
|
+
if (activeTab?.id === tab.id) {
|
|
932
|
+
try {
|
|
933
|
+
fallbackScreenshot = await screenshot({
|
|
934
|
+
tabId: tab.id,
|
|
935
|
+
format: params.format,
|
|
936
|
+
quality: params.quality,
|
|
937
|
+
allowTabSwitch: false,
|
|
938
|
+
});
|
|
939
|
+
} catch (error) {
|
|
940
|
+
if (requestCancelled(error)) throw error;
|
|
941
|
+
screenshotFallbackReason = "visible_tab_fallback_unavailable";
|
|
942
|
+
}
|
|
943
|
+
} else {
|
|
944
|
+
screenshotFallbackReason = activeTab?.id
|
|
945
|
+
? "visible_tab_fallback_skipped_inactive_target"
|
|
946
|
+
: "visible_tab_fallback_skipped_active_tab_unknown";
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
throwIfCancelled(state);
|
|
951
|
+
const postFrameStates = await browserFrameDocumentStates(tab.id, inspected.frames).catch(() => new Map());
|
|
952
|
+
throwIfCancelled(state);
|
|
953
|
+
const postState = postFrameStates.get(0) || null;
|
|
954
|
+
let current;
|
|
955
|
+
try { current = await chrome.tabs.get(tab.id); }
|
|
956
|
+
catch { throw new Error("browser tab became unavailable during computer observation"); }
|
|
957
|
+
throwIfCancelled(state);
|
|
958
|
+
const semanticFramesCoherent = browserSemanticFramesCoherent(inspected.frames, postFrameStates);
|
|
959
|
+
const semanticCoherent = Boolean(semanticEpoch && semanticFramesCoherent);
|
|
960
|
+
const cdpRootUrl = exactOptionalAuthorityString(cdp?.frame_tree?.[0]?.url, 8192);
|
|
961
|
+
const postUrl = exactOptionalAuthorityString(postState?.url, 8192);
|
|
962
|
+
const currentUrl = exactOptionalAuthorityString(current?.url, 32768);
|
|
963
|
+
const cdpCoherent = !cdp || (cdp.navigation_coherent === true && (!cdpRootUrl || (postUrl && cdpRootUrl === postUrl)));
|
|
964
|
+
const navigationCoherent = Boolean(semanticCoherent && cdpCoherent && currentUrl && postUrl && currentUrl === postUrl);
|
|
965
|
+
|
|
966
|
+
if (cdp?.accessibility?.nodes) fuseAccessibilityRefs(inspected.frames, cdp);
|
|
967
|
+
const screenshotResult = typeof cdp?.screenshot?.data === "string" && cdp.screenshot.data
|
|
968
|
+
? {
|
|
969
|
+
mime_type: cdp.screenshot.mime_type,
|
|
970
|
+
data: `data:${cdp.screenshot.mime_type};base64,${cdp.screenshot.data}`,
|
|
971
|
+
source: "cdp_surface",
|
|
972
|
+
}
|
|
973
|
+
: typeof fallbackScreenshot?.data === "string" && fallbackScreenshot.data
|
|
974
|
+
? {
|
|
975
|
+
mime_type: fallbackScreenshot.data.startsWith("data:image/jpeg") ? "image/jpeg" : "image/png",
|
|
976
|
+
data: fallbackScreenshot.data,
|
|
977
|
+
source: "capture_visible_tab_fallback",
|
|
978
|
+
}
|
|
979
|
+
: null;
|
|
980
|
+
return {
|
|
981
|
+
tab_id: current.id,
|
|
982
|
+
title: current.title || "",
|
|
983
|
+
url: current.url || "",
|
|
984
|
+
semantic: inspected,
|
|
985
|
+
accessibility: cdp?.accessibility || null,
|
|
986
|
+
viewport: cdp?.viewport || null,
|
|
987
|
+
frame_tree: cdp?.frame_tree || [],
|
|
988
|
+
document_epoch: exactOptionalAuthorityString(cdp?.document_epoch, 9000) || semanticEpoch,
|
|
989
|
+
_machine_history_entry_key: exactOptionalAuthorityString(postState?._machine_history_entry_key, 512),
|
|
990
|
+
capture: {
|
|
991
|
+
atomic: false,
|
|
992
|
+
navigation_coherent: navigationCoherent,
|
|
993
|
+
frame_epochs_coherent: semanticFramesCoherent,
|
|
994
|
+
semantic_epoch: semanticEpoch,
|
|
995
|
+
cdp_epoch: exactOptionalAuthorityString(cdp?.document_epoch, 9000),
|
|
996
|
+
cdp: Boolean(cdp),
|
|
997
|
+
cdp_components: cdp?.components || null,
|
|
998
|
+
screenshot_source: screenshotResult?.source || "none",
|
|
999
|
+
screenshot_format: screenshotResult?.mime_type === "image/jpeg" ? "jpeg" : screenshotResult ? "png" : "",
|
|
1000
|
+
screenshot_quality: params.format === "jpeg" ? params.quality : null,
|
|
1001
|
+
coherence: cdp
|
|
1002
|
+
? "single_cdp_session_plus_stable_extension_document_epoch"
|
|
1003
|
+
: fallbackScreenshot?.data
|
|
1004
|
+
? "stable_extension_document_epoch_with_visible_tab_screenshot_fallback"
|
|
1005
|
+
: "stable_extension_document_epoch_without_screenshot",
|
|
1006
|
+
...(fallbackReason ? { fallback_reason: fallbackReason } : {}),
|
|
1007
|
+
...(screenshotFallbackReason ? { screenshot_fallback_reason: screenshotFallbackReason } : {}),
|
|
1008
|
+
},
|
|
1009
|
+
screenshot: screenshotResult,
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async function browserFrameDocumentStates(tabId, frames) {
|
|
1014
|
+
const frameIds = [...new Set((Array.isArray(frames) ? frames : [])
|
|
1015
|
+
.map((frame) => exactNonNegativeInteger(frame?.frame_id))
|
|
1016
|
+
.filter((frameId) => frameId !== null))].slice(0, MAX_ACCESSIBLE_FRAMES);
|
|
1017
|
+
if (!frameIds.length) return new Map();
|
|
1018
|
+
const executions = await executePageAutomation({ tabId, frameIds }, "documentState", {});
|
|
1019
|
+
const states = new Map();
|
|
1020
|
+
for (const execution of executions || []) {
|
|
1021
|
+
if (!Number.isInteger(execution?.frameId)) continue;
|
|
1022
|
+
const state = execution?.result;
|
|
1023
|
+
if (!state || typeof state !== "object") continue;
|
|
1024
|
+
states.set(execution.frameId, state);
|
|
1025
|
+
}
|
|
1026
|
+
return states;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function browserSemanticFramesCoherent(frames, postStates) {
|
|
1030
|
+
const observed = Array.isArray(frames) ? frames : [];
|
|
1031
|
+
if (!observed.length || !(postStates instanceof Map)) return false;
|
|
1032
|
+
return observed.every((frame) => {
|
|
1033
|
+
const frameId = exactNonNegativeInteger(frame?.frame_id);
|
|
1034
|
+
const before = frame?.document || {};
|
|
1035
|
+
const after = frameId === null ? null : postStates.get(frameId);
|
|
1036
|
+
const epoch = exactOptionalAuthorityString(before.epoch, 9000);
|
|
1037
|
+
const url = exactOptionalAuthorityString(before.url, 8192);
|
|
1038
|
+
const historyEntryKey = exactOptionalAuthorityString(before._machine_history_entry_key, 512);
|
|
1039
|
+
return frameId !== null
|
|
1040
|
+
&& Boolean(epoch)
|
|
1041
|
+
&& Boolean(after)
|
|
1042
|
+
&& exactOptionalAuthorityString(after.epoch, 9000) === epoch
|
|
1043
|
+
&& exactOptionalAuthorityString(after.url, 8192) === url
|
|
1044
|
+
&& exactOptionalAuthorityString(after._machine_history_entry_key, 512) === historyEntryKey;
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function fuseAccessibilityRefs(frames, cdp) {
|
|
1049
|
+
const cdpFrameUrls = new Map();
|
|
1050
|
+
for (const frame of Array.isArray(cdp.frame_tree) ? cdp.frame_tree : []) {
|
|
1051
|
+
const frameId = exactOptionalAuthorityString(frame?.id, 32768);
|
|
1052
|
+
const frameUrl = exactOptionalAuthorityString(frame?.url, 8192);
|
|
1053
|
+
if (frameId && frameUrl) cdpFrameUrls.set(frameId, frameUrl);
|
|
1054
|
+
}
|
|
1055
|
+
const frameCandidates = new Map();
|
|
1056
|
+
for (const frame of frames || []) {
|
|
1057
|
+
const url = exactOptionalAuthorityString(frame?.document?.url, 8192);
|
|
1058
|
+
if (!url) continue;
|
|
1059
|
+
const list = frameCandidates.get(url) || [];
|
|
1060
|
+
list.push(frame);
|
|
1061
|
+
frameCandidates.set(url, list);
|
|
1062
|
+
}
|
|
1063
|
+
const usedRefs = new Set();
|
|
1064
|
+
for (const node of cdp.accessibility?.nodes || []) {
|
|
1065
|
+
if (!Number.isSafeInteger(node?.backend_dom_node_id) || node.backend_dom_node_id <= 0) continue;
|
|
1066
|
+
const nodeFrameId = exactOptionalAuthorityString(node.frame_id, 32768);
|
|
1067
|
+
if (!nodeFrameId) continue;
|
|
1068
|
+
const frameUrl = cdpFrameUrls.get(nodeFrameId) || "";
|
|
1069
|
+
const matchingFrames = frameCandidates.get(frameUrl) || [];
|
|
1070
|
+
if (matchingFrames.length !== 1) continue;
|
|
1071
|
+
const candidates = [];
|
|
1072
|
+
for (const element of Array.isArray(matchingFrames[0].elements) ? matchingFrames[0].elements : []) {
|
|
1073
|
+
if (typeof element?.ref !== "string" || !element.ref || element.ref.length > 100 || usedRefs.has(element.ref)) continue;
|
|
1074
|
+
const evidence = accessibilityMatchEvidence(node, element);
|
|
1075
|
+
if (evidence.score > 0) candidates.push({ element, ...evidence });
|
|
1076
|
+
}
|
|
1077
|
+
candidates.sort((left, right) => right.score - left.score);
|
|
1078
|
+
if (!candidates.length || candidates[0].score < 100) continue;
|
|
1079
|
+
if (candidates[1] && candidates[0].score - candidates[1].score < 15) continue;
|
|
1080
|
+
node.action_ref = candidates[0].element.ref;
|
|
1081
|
+
const confidence = candidates[0].score >= 180 && candidates[0].executable === true ? "high" : "medium";
|
|
1082
|
+
node.action_ref_confidence = confidence;
|
|
1083
|
+
if (confidence === "high" && Number.isSafeInteger(node.backend_dom_node_id) && node.backend_dom_node_id > 0) {
|
|
1084
|
+
candidates[0].element._machine_backend_node_id = node.backend_dom_node_id;
|
|
1085
|
+
}
|
|
1086
|
+
usedRefs.add(candidates[0].element.ref);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function accessibilityMatchEvidence(node, element) {
|
|
1091
|
+
let score = 0;
|
|
1092
|
+
const roleA = normalizedIdentity(node.role);
|
|
1093
|
+
const roleB = normalizedIdentity(element.role);
|
|
1094
|
+
const nameA = normalizedIdentity(node.name);
|
|
1095
|
+
const nameB = normalizedIdentity(element.name) || normalizedIdentity(element.label) || normalizedIdentity(element.placeholder);
|
|
1096
|
+
const roleExact = Boolean(roleA && roleB && roleA === roleB);
|
|
1097
|
+
const roleConflict = Boolean(roleA && roleB && roleA !== roleB);
|
|
1098
|
+
const nameExact = Boolean(nameA && nameB && nameA === nameB);
|
|
1099
|
+
const nameConflict = Boolean(nameA && nameB && nameA !== nameB);
|
|
1100
|
+
if (roleExact) score += 50;
|
|
1101
|
+
else if (roleConflict) score -= 30;
|
|
1102
|
+
if (nameExact) score += 60;
|
|
1103
|
+
else if (nameConflict) score -= 40;
|
|
1104
|
+
const geometry = boundingSimilarity(node.bounding_box, element.bounding_box);
|
|
1105
|
+
score += geometry.score;
|
|
1106
|
+
if (node.clickable === true && element.visible === true && element.enabled === true) score += 15;
|
|
1107
|
+
return {
|
|
1108
|
+
score,
|
|
1109
|
+
executable: geometry.strong === true
|
|
1110
|
+
&& !roleConflict
|
|
1111
|
+
&& !nameConflict
|
|
1112
|
+
&& (roleExact || nameExact),
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function boundingSimilarity(left, right) {
|
|
1117
|
+
if (!left || typeof left !== "object" || Array.isArray(left)
|
|
1118
|
+
|| !right || typeof right !== "object" || Array.isArray(right)) return { score: 0, strong: false };
|
|
1119
|
+
const values = [left.x, left.y, left.width, left.height, right.x, right.y, right.width, right.height];
|
|
1120
|
+
if (!values.every((value) => typeof value === "number" && Number.isFinite(value))) return { score: 0, strong: false };
|
|
1121
|
+
const [ax1, ay1, aw, ah, bx1, by1, bw, bh] = values;
|
|
1122
|
+
if (aw <= 0 || ah <= 0 || bw <= 0 || bh <= 0) {
|
|
1123
|
+
return { score: 0, strong: false };
|
|
1124
|
+
}
|
|
1125
|
+
const ax2 = ax1 + aw; const ay2 = ay1 + ah; const bx2 = bx1 + bw; const by2 = by1 + bh;
|
|
1126
|
+
const intersection = Math.max(0, Math.min(ax2, bx2) - Math.max(ax1, bx1)) * Math.max(0, Math.min(ay2, by2) - Math.max(ay1, by1));
|
|
1127
|
+
const union = aw * ah + bw * bh - intersection;
|
|
1128
|
+
const iou = union > 0 ? intersection / union : 0;
|
|
1129
|
+
if (iou >= 0.9) return { score: 120, strong: true };
|
|
1130
|
+
if (iou >= 0.7) return { score: 100, strong: true };
|
|
1131
|
+
if (iou >= 0.5) return { score: 75, strong: false };
|
|
1132
|
+
const acx = ax1 + aw / 2; const acy = ay1 + ah / 2; const bcx = bx1 + bw / 2; const bcy = by1 + bh / 2;
|
|
1133
|
+
const distance = Math.hypot(acx - bcx, acy - bcy);
|
|
1134
|
+
if (distance <= 2) return { score: 55, strong: false };
|
|
1135
|
+
if (distance <= 5) return { score: 35, strong: false };
|
|
1136
|
+
return { score: 0, strong: false };
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function normalizedIdentity(value) {
|
|
1140
|
+
if (typeof value !== "string" || value.length > 4000 || value.includes("\0")) return "";
|
|
1141
|
+
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
1142
|
+
}
|
|
1143
|
+
|
|
166
1144
|
async function discoverPageFrames(tabId, frameId, allFrames, resultBudget = MAX_ACCESSIBLE_FRAMES) {
|
|
167
1145
|
if (Number.isInteger(frameId)) return { frames: [{ frameId, url: "" }], truncated: false };
|
|
168
1146
|
if (!allFrames) return { frames: [{ frameId: 0, url: "" }], truncated: false };
|
|
@@ -174,12 +1152,14 @@
|
|
|
174
1152
|
.filter((item) => Number.isInteger(item.frameId))
|
|
175
1153
|
.map((item) => ({ frameId: item.frameId, url: item.result?.url || "" }))
|
|
176
1154
|
.sort((left, right) => (left.frameId === 0 ? -1 : right.frameId === 0 ? 1 : left.frameId - right.frameId));
|
|
177
|
-
const limit =
|
|
1155
|
+
const limit = Number.isSafeInteger(resultBudget) && resultBudget > 0
|
|
1156
|
+
? Math.min(MAX_ACCESSIBLE_FRAMES, resultBudget)
|
|
1157
|
+
: 1;
|
|
178
1158
|
return { frames: ordered.slice(0, limit), truncated: ordered.length > limit };
|
|
179
1159
|
}
|
|
180
1160
|
|
|
181
1161
|
function boundedDocumentSource(limit) {
|
|
182
|
-
const maxBytes =
|
|
1162
|
+
const maxBytes = Number.isSafeInteger(limit) && limit > 0 ? limit : 1;
|
|
183
1163
|
const encoder = new TextEncoder();
|
|
184
1164
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
185
1165
|
const chunks = [];
|
|
@@ -281,15 +1261,108 @@
|
|
|
281
1261
|
};
|
|
282
1262
|
}
|
|
283
1263
|
|
|
1264
|
+
async function assertExpectedNavigationTab(tabId, expectedTabUrl) {
|
|
1265
|
+
const expected = exactOptionalAuthorityString(expectedTabUrl, 32768);
|
|
1266
|
+
if (!expected) return;
|
|
1267
|
+
let current;
|
|
1268
|
+
try { current = await chrome.tabs.get(tabId); }
|
|
1269
|
+
catch { throw new Error("snapshot browser tab could not be verified before navigation dispatch; observe again"); }
|
|
1270
|
+
if (exactOptionalAuthorityString(current?.url, 32768) !== expected) {
|
|
1271
|
+
throw new Error("snapshot browser tab changed before navigation dispatch; observe again");
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
async function assertExpectedHistoryDocument(tabId, expectedTabUrl, expectedDocumentEpoch, expectedHistoryEntryKey) {
|
|
1276
|
+
const expectedDocument = exactOptionalAuthorityString(expectedDocumentEpoch, 9000);
|
|
1277
|
+
const expectedHistoryEntry = exactOptionalAuthorityString(expectedHistoryEntryKey, 512);
|
|
1278
|
+
if (!expectedDocument && !expectedHistoryEntry) {
|
|
1279
|
+
await assertExpectedNavigationTab(tabId, expectedTabUrl);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
let current;
|
|
1283
|
+
try { current = await documentState({ tabId }); }
|
|
1284
|
+
catch { throw new Error("snapshot history document could not be verified before dispatch; observe again"); }
|
|
1285
|
+
const expectedUrl = exactOptionalAuthorityString(expectedTabUrl, 32768);
|
|
1286
|
+
if (expectedUrl && (exactOptionalAuthorityString(current?.url, 32768) !== expectedUrl
|
|
1287
|
+
|| exactOptionalAuthorityString(current?.document_url, 8192) !== expectedUrl)) {
|
|
1288
|
+
throw new Error("snapshot browser tab changed before navigation dispatch; observe again");
|
|
1289
|
+
}
|
|
1290
|
+
if (expectedDocument && exactOptionalAuthorityString(current?.document_epoch, 9000) !== expectedDocument) {
|
|
1291
|
+
throw new Error("snapshot history document changed before dispatch; observe again");
|
|
1292
|
+
}
|
|
1293
|
+
if (expectedHistoryEntry) {
|
|
1294
|
+
const currentHistoryEntry = exactOptionalAuthorityString(current?._machine_history_entry_key, 512);
|
|
1295
|
+
if (!currentHistoryEntry) throw new Error("snapshot history entry could not be verified before dispatch; observe again");
|
|
1296
|
+
if (currentHistoryEntry !== expectedHistoryEntry) throw new Error("snapshot history entry changed before dispatch; observe again");
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function snapshotBoundHistoryAction(params) {
|
|
1301
|
+
return Boolean(exactOptionalAuthorityString(params?.expectedDocumentEpoch, 9000)
|
|
1302
|
+
|| exactOptionalAuthorityString(params?.expectedHistoryEntryKey, 512));
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
async function invokeSnapshotHistoryMutation(tab, params, state) {
|
|
1306
|
+
assertPageTab(tab);
|
|
1307
|
+
await executePageMutation({ tabId: tab.id, frameIds: [0] }, "historyAction", {
|
|
1308
|
+
action: params.action,
|
|
1309
|
+
expectedTabUrl: params.expectedTabUrl,
|
|
1310
|
+
expectedDocumentEpoch: params.expectedDocumentEpoch,
|
|
1311
|
+
expectedHistoryEntryKey: params.expectedHistoryEntryKey,
|
|
1312
|
+
}, state);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function normalizeBrowserActionParams(params) {
|
|
1316
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("browser action parameters are invalid before dispatch");
|
|
1317
|
+
const action = exactOptionalText(params.action, "browser action", 32);
|
|
1318
|
+
const allowed = new Set([
|
|
1319
|
+
"navigate", "reload", "back", "forward", "click", "double_click", "hover", "fill", "type_text", "select",
|
|
1320
|
+
"check", "uncheck", "focus", "press", "submit", "scroll_into_view",
|
|
1321
|
+
]);
|
|
1322
|
+
if (!allowed.has(action)) throw new Error("unsupported browser action");
|
|
1323
|
+
let frameId = params.frameId;
|
|
1324
|
+
if (frameId === undefined || frameId === null) frameId = null;
|
|
1325
|
+
else if (!Number.isSafeInteger(frameId) || frameId < 0) throw new Error("browser action frame id is invalid before dispatch");
|
|
1326
|
+
const waitFor = params.waitFor === undefined ? "none" : exactOptionalText(params.waitFor, "browser action waitFor", 32);
|
|
1327
|
+
if (!["none", "domcontentloaded", "complete"].includes(waitFor)) throw new Error("browser action waitFor is invalid before dispatch");
|
|
1328
|
+
const inputMode = params.inputMode === undefined ? "auto" : exactOptionalText(params.inputMode, "browser action inputMode", 16);
|
|
1329
|
+
if (!["auto", "trusted", "dom"].includes(inputMode)) throw new Error("browser action inputMode is invalid before dispatch");
|
|
1330
|
+
const elementTimeoutMs = exactBoundedInteger(params.elementTimeoutMs, "browser action elementTimeoutMs", 10000, 1, 60000);
|
|
1331
|
+
const expectedTabUrl = exactOptionalAuthorityString(params.expectedTabUrl, 32768);
|
|
1332
|
+
const expectedDocumentEpoch = exactOptionalAuthorityString(params.expectedDocumentEpoch, 9000);
|
|
1333
|
+
const expectedHistoryEntryKey = exactOptionalAuthorityString(params.expectedHistoryEntryKey, 512);
|
|
1334
|
+
let value = params.value;
|
|
1335
|
+
if (value === undefined || value === null) value = null;
|
|
1336
|
+
else value = exactOptionalText(value, "browser action value", 131072);
|
|
1337
|
+
let key = params.key;
|
|
1338
|
+
if (key === undefined) key = "";
|
|
1339
|
+
else key = exactOptionalText(key, "browser action key", 100);
|
|
1340
|
+
let url = params.url;
|
|
1341
|
+
if (action === "navigate") {
|
|
1342
|
+
if (url === undefined || url === null || url === "") throw new Error("navigate requires url");
|
|
1343
|
+
if (typeof url !== "string" || !/^(https?|file):/i.test(url) || url.includes("\0") || url.length > 32768) {
|
|
1344
|
+
throw new Error("navigate requires a valid absolute url before dispatch");
|
|
1345
|
+
}
|
|
1346
|
+
} else if (url !== undefined && url !== null && url !== "") throw new Error("url is only valid for navigate before dispatch");
|
|
1347
|
+
else url = "";
|
|
1348
|
+
return {
|
|
1349
|
+
...params, action, frameId, waitFor, inputMode, elementTimeoutMs, expectedTabUrl, expectedDocumentEpoch, expectedHistoryEntryKey, value, key, url,
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
|
|
284
1353
|
async function browserAction(params, state) {
|
|
1354
|
+
params = normalizeBrowserActionParams(params);
|
|
1355
|
+
throwIfCancelled(state);
|
|
285
1356
|
const tab = await resolveTab(params.tabId);
|
|
1357
|
+
throwIfCancelled(state);
|
|
286
1358
|
if (params.action === "navigate") {
|
|
287
|
-
if (!params.url) throw new Error("navigate requires url");
|
|
288
1359
|
const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
|
|
289
1360
|
try {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
1361
|
+
await assertExpectedNavigationTab(tab.id, params.expectedTabUrl);
|
|
1362
|
+
throwIfCancelled(state);
|
|
1363
|
+
const updated = await invokeBrowserNavigationMutation(() => chrome.tabs.update(tab.id, { url: params.url }));
|
|
1364
|
+
await awaitPostDispatchWait(waiter);
|
|
1365
|
+
return postActionTabMetadata(updated.id);
|
|
293
1366
|
} catch (error) {
|
|
294
1367
|
waiter.cancel();
|
|
295
1368
|
throw error;
|
|
@@ -298,9 +1371,17 @@
|
|
|
298
1371
|
if (params.action === "reload") {
|
|
299
1372
|
const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
|
|
300
1373
|
try {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
1374
|
+
if (snapshotBoundHistoryAction(params)) {
|
|
1375
|
+
await invokeSnapshotHistoryMutation(tab, params, state);
|
|
1376
|
+
} else {
|
|
1377
|
+
await assertExpectedHistoryDocument(
|
|
1378
|
+
tab.id, params.expectedTabUrl, params.expectedDocumentEpoch, params.expectedHistoryEntryKey,
|
|
1379
|
+
);
|
|
1380
|
+
throwIfCancelled(state);
|
|
1381
|
+
await invokeBrowserNavigationMutation(() => chrome.tabs.reload(tab.id));
|
|
1382
|
+
}
|
|
1383
|
+
await awaitPostDispatchWait(waiter);
|
|
1384
|
+
return postActionTabMetadata(tab.id);
|
|
304
1385
|
} catch (error) {
|
|
305
1386
|
waiter.cancel();
|
|
306
1387
|
throw error;
|
|
@@ -309,10 +1390,18 @@
|
|
|
309
1390
|
if (params.action === "back" || params.action === "forward") {
|
|
310
1391
|
const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
|
|
311
1392
|
try {
|
|
312
|
-
if (params
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
1393
|
+
if (snapshotBoundHistoryAction(params)) {
|
|
1394
|
+
await invokeSnapshotHistoryMutation(tab, params, state);
|
|
1395
|
+
} else {
|
|
1396
|
+
await assertExpectedHistoryDocument(
|
|
1397
|
+
tab.id, params.expectedTabUrl, params.expectedDocumentEpoch, params.expectedHistoryEntryKey,
|
|
1398
|
+
);
|
|
1399
|
+
throwIfCancelled(state);
|
|
1400
|
+
if (params.action === "back") await invokeBrowserNavigationMutation(() => chrome.tabs.goBack(tab.id));
|
|
1401
|
+
else await invokeBrowserNavigationMutation(() => chrome.tabs.goForward(tab.id));
|
|
1402
|
+
}
|
|
1403
|
+
await awaitPostDispatchWait(waiter);
|
|
1404
|
+
return postActionTabMetadata(tab.id);
|
|
316
1405
|
} catch (error) {
|
|
317
1406
|
waiter.cancel();
|
|
318
1407
|
throw error;
|
|
@@ -322,17 +1411,26 @@
|
|
|
322
1411
|
const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
|
|
323
1412
|
let result;
|
|
324
1413
|
try {
|
|
325
|
-
result = await performPageAction(tab, params);
|
|
326
|
-
await waiter
|
|
1414
|
+
result = await performPageAction(tab, params, state);
|
|
1415
|
+
await awaitPostDispatchWait(waiter);
|
|
327
1416
|
} catch (error) {
|
|
328
1417
|
waiter.cancel();
|
|
329
1418
|
throw error;
|
|
330
1419
|
}
|
|
331
|
-
const current = await
|
|
332
|
-
return {
|
|
1420
|
+
const current = await postActionTabMetadata(tab.id);
|
|
1421
|
+
return { ...result, ...current };
|
|
333
1422
|
}
|
|
334
1423
|
|
|
335
|
-
|
|
1424
|
+
function pageActionPayload(params) {
|
|
1425
|
+
return {
|
|
1426
|
+
action: params.action, selector: params.selector, value: params.value, key: params.key,
|
|
1427
|
+
elementTimeoutMs: params.elementTimeoutMs, expectedIdentity: params.expectedIdentity,
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
async function performPageAction(tab, params, state) {
|
|
1432
|
+
throwIfCancelled(state);
|
|
1433
|
+
const rendererParams = pageActionPayload(params);
|
|
336
1434
|
const trustedActions = new Set(["click", "double_click", "hover", "press", "type_text"]);
|
|
337
1435
|
if (params.inputMode === "trusted" && !trustedActions.has(params.action)) {
|
|
338
1436
|
throw new Error("trusted input is unavailable for this action");
|
|
@@ -343,86 +1441,260 @@
|
|
|
343
1441
|
throw new Error("trusted input currently requires the top frame; use input_mode=dom for a subframe");
|
|
344
1442
|
}
|
|
345
1443
|
if (wantsTrusted && topFrame) {
|
|
346
|
-
const
|
|
1444
|
+
const api = globalThis.__machineBridgeDevtoolsInput;
|
|
1445
|
+
if (!api?.perform) {
|
|
1446
|
+
if (params.inputMode === "trusted") throw new Error("trusted input module is unavailable");
|
|
1447
|
+
const [fallback] = await executePageMutation(scriptTarget(tab.id, params.frameId, false), "action", rendererParams, state);
|
|
1448
|
+
return {
|
|
1449
|
+
...fallback.result,
|
|
1450
|
+
input_mode: "dom",
|
|
1451
|
+
trusted_input_fallback: true,
|
|
1452
|
+
fallback_reason: "trusted_input_unavailable_before_dispatch",
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
const [prepared] = await executePageMutation(scriptTarget(tab.id, params.frameId, false), "prepareAction", rendererParams, state);
|
|
1456
|
+
throwIfCancelled(state);
|
|
347
1457
|
try {
|
|
348
|
-
const api = globalThis.__machineBridgeDevtoolsInput;
|
|
349
|
-
if (!api?.perform) {
|
|
350
|
-
const unavailable = new Error("trusted input module is unavailable");
|
|
351
|
-
Object.defineProperty(unavailable, "safeToFallback", { value: true });
|
|
352
|
-
throw unavailable;
|
|
353
|
-
}
|
|
354
1458
|
await api.perform(tab.id, params.action, {
|
|
355
1459
|
point: prepared.result?.point,
|
|
356
1460
|
key: params.key || params.value || "Enter",
|
|
357
1461
|
text: params.value || "",
|
|
1462
|
+
beforeDispatch: () => throwIfCancelledBeforeTrustedInput(state),
|
|
358
1463
|
});
|
|
359
1464
|
return { ...prepared.result, input_mode: "trusted", trusted_input_fallback: false };
|
|
360
1465
|
} catch (error) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
364
|
-
throw new Error(`trusted browser input may have been partially dispatched; the action outcome is unknown. Inspect the page before retrying. (${detail})`);
|
|
365
|
-
}
|
|
366
|
-
const [fallback] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "action", params);
|
|
367
|
-
return {
|
|
368
|
-
...fallback.result,
|
|
369
|
-
input_mode: "dom",
|
|
370
|
-
trusted_input_fallback: true,
|
|
371
|
-
fallback_reason: String(error?.message || error).slice(0, 500),
|
|
372
|
-
};
|
|
1466
|
+
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
1467
|
+
throw new Error(`trusted browser input may have been partially dispatched; the action outcome is unknown. Inspect the page before retrying. (${detail})`);
|
|
373
1468
|
}
|
|
374
1469
|
}
|
|
375
|
-
|
|
1470
|
+
throwIfCancelled(state);
|
|
1471
|
+
const [execution] = await executePageMutation(scriptTarget(tab.id, params.frameId, false), "action", rendererParams, state);
|
|
376
1472
|
return { ...execution.result, input_mode: "dom", trusted_input_fallback: false };
|
|
377
1473
|
}
|
|
1474
|
+
function utf8ByteLength(value) {
|
|
1475
|
+
let bytes = 0;
|
|
1476
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1477
|
+
const code = value.charCodeAt(index);
|
|
1478
|
+
if (code < 0x80) bytes += 1;
|
|
1479
|
+
else if (code < 0x800) bytes += 2;
|
|
1480
|
+
else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
|
|
1481
|
+
const next = value.charCodeAt(index + 1);
|
|
1482
|
+
if (next >= 0xdc00 && next <= 0xdfff) { bytes += 4; index += 1; } else bytes += 3;
|
|
1483
|
+
} else bytes += 3;
|
|
1484
|
+
}
|
|
1485
|
+
return bytes;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function exactBrokerFrameId(value, label) {
|
|
1489
|
+
if (value === undefined || value === null) return null;
|
|
1490
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} is invalid before dispatch`);
|
|
1491
|
+
return value;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function exactBrokerSelector(value, label) {
|
|
1495
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object before dispatch`);
|
|
1496
|
+
return value;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function normalizeBrokerFillFormParams(params) {
|
|
1500
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("browser fill form parameters are invalid before dispatch");
|
|
1501
|
+
const allowed = new Set(["tabId", "frameId", "fields", "submit", "submitSelector", "waitFor", "elementTimeoutMs"]);
|
|
1502
|
+
for (const key of Object.keys(params)) if (!allowed.has(key)) throw new Error(`unknown browser fill form property before dispatch: ${key}`);
|
|
1503
|
+
if (!Array.isArray(params.fields) || params.fields.length < 1 || params.fields.length > 200) throw new Error("browser fill form fields are invalid before dispatch");
|
|
1504
|
+
const frameId = exactBrokerFrameId(params.frameId, "browser fill form frame id");
|
|
1505
|
+
const submit = exactBoolean(params.submit, "browser fill form submit", false);
|
|
1506
|
+
const submitSelector = params.submitSelector === undefined || params.submitSelector === null ? null : exactBrokerSelector(params.submitSelector, "browser fill form submit selector");
|
|
1507
|
+
const waitFor = params.waitFor === undefined ? "none" : exactOptionalText(params.waitFor, "browser fill form waitFor", 32);
|
|
1508
|
+
if (!["none", "domcontentloaded", "complete"].includes(waitFor)) throw new Error("browser fill form waitFor is invalid before dispatch");
|
|
1509
|
+
const elementTimeoutMs = exactBoundedInteger(params.elementTimeoutMs, "browser fill form elementTimeoutMs", 10000, 1, 60000);
|
|
1510
|
+
let totalValueBytes = 0;
|
|
1511
|
+
const fields = params.fields.map((field, index) => {
|
|
1512
|
+
if (!field || typeof field !== "object" || Array.isArray(field)) throw new Error(`browser fill form field ${index} is invalid before dispatch`);
|
|
1513
|
+
const allowedField = new Set(["selector", "value", "action", "sensitive"]);
|
|
1514
|
+
for (const key of Object.keys(field)) if (!allowedField.has(key)) throw new Error(`unknown browser fill form field ${index} property before dispatch: ${key}`);
|
|
1515
|
+
const selector = exactBrokerSelector(field.selector, `browser fill form field ${index} selector`);
|
|
1516
|
+
if (typeof field.action !== "string" || !["fill", "select", "check", "uncheck", "click"].includes(field.action)) throw new Error(`browser fill form field ${index} action is invalid before dispatch`);
|
|
1517
|
+
if (typeof field.sensitive !== "boolean") throw new Error(`browser fill form field ${index} sensitive flag must be boolean before dispatch`);
|
|
1518
|
+
let value = field.value;
|
|
1519
|
+
if (["fill", "select"].includes(field.action)) {
|
|
1520
|
+
if (typeof value !== "string" || value.includes("\0") || value.length > 128 * 1024) throw new Error(`browser fill form field ${index} value is invalid before dispatch`);
|
|
1521
|
+
totalValueBytes += utf8ByteLength(value);
|
|
1522
|
+
if (totalValueBytes > 4 * 1024 * 1024) throw new Error("browser fill form values exceed the 4 MiB aggregate budget before dispatch");
|
|
1523
|
+
} else if (value !== undefined && value !== null) throw new Error(`browser fill form field ${index} value is not valid before dispatch`);
|
|
1524
|
+
else value = null;
|
|
1525
|
+
return { selector, value, action: field.action, sensitive: field.sensitive };
|
|
1526
|
+
});
|
|
1527
|
+
return { tabId: params.tabId, frameId, fields, submit, submitSelector, waitFor, elementTimeoutMs };
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function fillFormRendererPayload(params) {
|
|
1531
|
+
return { fields: params.fields, submit: params.submit, submitSelector: params.submitSelector, elementTimeoutMs: params.elementTimeoutMs };
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
function decodedBase64Bytes(value) {
|
|
1535
|
+
if (typeof value !== "string" || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) return null;
|
|
1536
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
1537
|
+
return (value.length / 4) * 3 - padding;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function normalizeBrokerUploadParams(params) {
|
|
1541
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("browser upload parameters are invalid before dispatch");
|
|
1542
|
+
const allowed = new Set(["tabId", "frameId", "selector", "files", "elementTimeoutMs"]);
|
|
1543
|
+
for (const key of Object.keys(params)) if (!allowed.has(key)) throw new Error(`unknown browser upload property before dispatch: ${key}`);
|
|
1544
|
+
const frameId = exactBrokerFrameId(params.frameId, "browser upload frame id");
|
|
1545
|
+
const selector = exactBrokerSelector(params.selector, "browser upload selector");
|
|
1546
|
+
const elementTimeoutMs = exactBoundedInteger(params.elementTimeoutMs, "browser upload elementTimeoutMs", 10000, 1, 60000);
|
|
1547
|
+
if (!Array.isArray(params.files) || params.files.length < 1 || params.files.length > 8) throw new Error("browser upload files are invalid before dispatch");
|
|
1548
|
+
let totalBytes = 0;
|
|
1549
|
+
const files = params.files.map((item, index) => {
|
|
1550
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error(`browser upload file ${index} is invalid before dispatch`);
|
|
1551
|
+
for (const key of Object.keys(item)) if (!["filename", "mime", "data"].includes(key)) throw new Error(`unknown browser upload file ${index} property before dispatch: ${key}`);
|
|
1552
|
+
if (typeof item.filename !== "string" || !item.filename || item.filename === "." || item.filename === ".." || item.filename.length > 255 || /[\/\\\u0000-\u001f\u007f]/.test(item.filename)) throw new Error(`browser upload file ${index} filename is invalid before dispatch`);
|
|
1553
|
+
if (typeof item.mime !== "string" || item.mime.length > 200 || !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(item.mime)) throw new Error(`browser upload file ${index} mime is invalid before dispatch`);
|
|
1554
|
+
const bytes = decodedBase64Bytes(item.data);
|
|
1555
|
+
if (bytes === null) throw new Error(`browser upload file ${index} data is invalid before dispatch`);
|
|
1556
|
+
totalBytes += bytes;
|
|
1557
|
+
if (totalBytes > 5 * 1024 * 1024) throw new Error("browser upload files exceed the 5 MiB aggregate budget before dispatch");
|
|
1558
|
+
return { filename: item.filename, mime: item.mime, data: item.data };
|
|
1559
|
+
});
|
|
1560
|
+
return { tabId: params.tabId, frameId, selector, files, elementTimeoutMs };
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function uploadRendererPayload(params) {
|
|
1564
|
+
return { selector: params.selector, files: params.files, elementTimeoutMs: params.elementTimeoutMs };
|
|
1565
|
+
}
|
|
1566
|
+
|
|
378
1567
|
async function fillForm(params, state) {
|
|
1568
|
+
params = normalizeBrokerFillFormParams(params);
|
|
1569
|
+
throwIfCancelled(state);
|
|
379
1570
|
const tab = await resolveTab(params.tabId);
|
|
1571
|
+
throwIfCancelled(state);
|
|
380
1572
|
assertPageTab(tab);
|
|
381
1573
|
const waiter = beginTabWait(tab.id, params.waitFor, state?.timeoutMs, state);
|
|
382
1574
|
let execution;
|
|
383
1575
|
try {
|
|
384
|
-
[execution] = await
|
|
385
|
-
await waiter
|
|
1576
|
+
[execution] = await executePageMutation(scriptTarget(tab.id, params.frameId, false), "fillForm", fillFormRendererPayload(params), state);
|
|
1577
|
+
await awaitPostDispatchWait(waiter);
|
|
386
1578
|
} catch (error) {
|
|
387
1579
|
waiter.cancel();
|
|
388
1580
|
throw error;
|
|
389
1581
|
}
|
|
390
|
-
|
|
1582
|
+
const current = await postActionTabMetadata(tab.id);
|
|
1583
|
+
return { ...execution.result, ...current };
|
|
391
1584
|
}
|
|
392
1585
|
|
|
393
|
-
async function uploadFiles(params) {
|
|
1586
|
+
async function uploadFiles(params, state) {
|
|
1587
|
+
params = normalizeBrokerUploadParams(params);
|
|
1588
|
+
throwIfCancelled(state);
|
|
394
1589
|
const tab = await resolveTab(params.tabId);
|
|
1590
|
+
throwIfCancelled(state);
|
|
395
1591
|
assertPageTab(tab);
|
|
396
|
-
const [execution] = await
|
|
397
|
-
|
|
1592
|
+
const [execution] = await executePageMutation(scriptTarget(tab.id, params.frameId, false), "uploadFiles", uploadRendererPayload(params), state);
|
|
1593
|
+
const current = await postActionTabMetadata(tab.id);
|
|
1594
|
+
return { ...execution.result, ...current };
|
|
398
1595
|
}
|
|
399
1596
|
|
|
400
|
-
async function screenshot(params) {
|
|
1597
|
+
async function screenshot(params, state) {
|
|
1598
|
+
const format = exactImageFormat(params.format, "browser screenshot format");
|
|
1599
|
+
const quality = exactBoundedInteger(params.quality, "browser screenshot quality", 90, 1, 100);
|
|
1600
|
+
const allowTabSwitch = exactBoolean(params.allowTabSwitch, "browser screenshot allowTabSwitch", true);
|
|
1601
|
+
throwIfCancelled(state);
|
|
401
1602
|
const tab = await resolveTab(params.tabId);
|
|
402
1603
|
const [previousActive] = await chrome.tabs.query({ active: true, windowId: tab.windowId });
|
|
1604
|
+
throwIfCancelled(state);
|
|
403
1605
|
const changedActiveTab = previousActive?.id !== tab.id;
|
|
404
|
-
if (changedActiveTab
|
|
1606
|
+
if (changedActiveTab && !allowTabSwitch) {
|
|
1607
|
+
throw new Error("browser screenshot target tab is not active for non-disruptive capture");
|
|
1608
|
+
}
|
|
1609
|
+
if (changedActiveTab && !previousActive?.id) throw new Error("browser screenshot cannot safely switch tabs without a restore baseline");
|
|
1610
|
+
let data = null;
|
|
1611
|
+
let failure = null;
|
|
1612
|
+
let captureActive = null;
|
|
1613
|
+
let activationAttempted = false;
|
|
405
1614
|
try {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
1615
|
+
if (changedActiveTab) {
|
|
1616
|
+
let activationBaseline = null;
|
|
1617
|
+
try { [activationBaseline] = await chrome.tabs.query({ active: true, windowId: tab.windowId }); }
|
|
1618
|
+
catch { throw new Error("browser screenshot could not revalidate the active tab before temporary activation"); }
|
|
1619
|
+
if (activationBaseline?.id !== previousActive.id) {
|
|
1620
|
+
throw new Error("browser screenshot active tab changed before temporary activation");
|
|
1621
|
+
}
|
|
1622
|
+
throwIfCancelled(state);
|
|
1623
|
+
activationAttempted = true;
|
|
1624
|
+
let activated;
|
|
1625
|
+
try { activated = await chrome.tabs.update(tab.id, { active: true }); }
|
|
1626
|
+
catch (error) {
|
|
1627
|
+
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
1628
|
+
throw new Error(`browser screenshot temporary tab activation may have been dispatched; the outcome is unknown. Inspect tabs before retrying. (${detail})`);
|
|
1629
|
+
}
|
|
1630
|
+
if (activated?.id !== tab.id || activated.windowId !== tab.windowId) {
|
|
1631
|
+
throw new Error("browser screenshot temporary tab activation may have been dispatched; the outcome is unknown because target window provenance changed. Inspect tabs before retrying.");
|
|
1632
|
+
}
|
|
415
1633
|
}
|
|
1634
|
+
throwIfCancelled(state);
|
|
1635
|
+
try { [captureActive] = await chrome.tabs.query({ active: true, windowId: tab.windowId }); }
|
|
1636
|
+
catch { failure = new Error("browser screenshot could not verify target tab at capture boundary"); }
|
|
1637
|
+
if (!failure && captureActive?.id !== tab.id) failure = new Error("browser screenshot target tab was not active at capture boundary");
|
|
1638
|
+
if (!failure) {
|
|
1639
|
+
throwIfCancelled(state);
|
|
1640
|
+
try {
|
|
1641
|
+
data = await chrome.tabs.captureVisibleTab(tab.windowId, { format, quality });
|
|
1642
|
+
throwIfCancelled(state);
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
failure = error;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
if (!failure) {
|
|
1648
|
+
try { [captureActive] = await chrome.tabs.query({ active: true, windowId: tab.windowId }); }
|
|
1649
|
+
catch { failure = new Error("browser screenshot could not verify the active tab after capture"); }
|
|
1650
|
+
if (!failure && captureActive?.id !== tab.id) failure = new Error("browser screenshot active tab changed during capture");
|
|
1651
|
+
}
|
|
1652
|
+
} finally {
|
|
1653
|
+
if (activationAttempted && previousActive?.id) await restoreScreenshotActiveTab(tab, previousActive);
|
|
1654
|
+
}
|
|
1655
|
+
if (failure) throw failure;
|
|
1656
|
+
if (typeof data !== "string" || !/^data:image\/(?:png|jpeg);base64,[A-Za-z0-9+/=]+$/.test(data)) {
|
|
1657
|
+
throw new Error("browser screenshot returned invalid image data");
|
|
1658
|
+
}
|
|
1659
|
+
return { ...publicTab(captureActive), tab_metadata_verified: true, data };
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
async function restoreScreenshotActiveTab(tab, previousActive) {
|
|
1663
|
+
let currentActive = null;
|
|
1664
|
+
try { [currentActive] = await chrome.tabs.query({ active: true, windowId: tab.windowId }); }
|
|
1665
|
+
catch { throw new Error("browser screenshot could not verify active-tab restoration"); }
|
|
1666
|
+
if (!currentActive?.id) throw new Error("browser screenshot could not verify active-tab restoration");
|
|
1667
|
+
if (currentActive.id !== tab.id) return;
|
|
1668
|
+
let restoreBaseline = null;
|
|
1669
|
+
try { restoreBaseline = await chrome.tabs.get(previousActive.id); }
|
|
1670
|
+
catch { throw new Error("browser screenshot could not restore the previous active tab"); }
|
|
1671
|
+
if (restoreBaseline?.windowId !== tab.windowId) throw new Error("browser screenshot could not restore the previous active tab");
|
|
1672
|
+
try {
|
|
1673
|
+
const restoredTab = await chrome.tabs.update(previousActive.id, { active: true });
|
|
1674
|
+
if (restoredTab?.id !== previousActive.id || restoredTab.windowId !== tab.windowId) throw new Error("restore window changed");
|
|
1675
|
+
const [restored] = await chrome.tabs.query({ active: true, windowId: tab.windowId });
|
|
1676
|
+
if (restored?.id !== previousActive.id) throw new Error("restore verification mismatch");
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
1679
|
+
throw new Error(`browser screenshot restoration may have been dispatched; the active-tab outcome is unknown. Inspect tabs before retrying. (${detail})`);
|
|
416
1680
|
}
|
|
417
1681
|
}
|
|
418
1682
|
|
|
1683
|
+
async function postActionTabMetadata(tabId) {
|
|
1684
|
+
try {
|
|
1685
|
+
return { ...publicTab(await chrome.tabs.get(tabId)), tab_metadata_verified: true };
|
|
1686
|
+
} catch {
|
|
1687
|
+
return { tab_id: tabId, tab_metadata_verified: false };
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
419
1691
|
function publicTab(tab) {
|
|
420
1692
|
return { tab_id: tab.id, window_id: tab.windowId, title: tab.title || "", url: tab.url || "", status: tab.status || "" };
|
|
421
1693
|
}
|
|
422
1694
|
|
|
423
1695
|
function assertPageTab(tab) {
|
|
424
|
-
const url =
|
|
425
|
-
if (!/^(https?|file):/i.test(url)) throw new Error("this page cannot be scripted by a browser extension");
|
|
1696
|
+
const url = exactOptionalAuthorityString(tab?.url, 32768);
|
|
1697
|
+
if (!url || !/^(https?|file):/i.test(url)) throw new Error("this page cannot be scripted by a browser extension");
|
|
426
1698
|
}
|
|
427
1699
|
|
|
428
1700
|
function beginTabWait(tabId, mode, requestTimeoutMs = 30000, state = null) {
|
|
@@ -487,7 +1759,80 @@
|
|
|
487
1759
|
timeout = setTimeout(() => settle(new Error(`browser navigation wait timed out${lastPollError ? `: ${lastPollError}` : ""}`)), waitTimeoutMs);
|
|
488
1760
|
return { promise, cancel: () => settle() };
|
|
489
1761
|
}
|
|
1762
|
+
|
|
1763
|
+
async function awaitPostDispatchWait(waiter) {
|
|
1764
|
+
try {
|
|
1765
|
+
await waiter.promise;
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
1768
|
+
throw new Error(`browser action may have been dispatched; the action outcome is unknown because post-dispatch wait failed. Inspect the page before retrying. (${detail})`);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
async function invokeBrowserNavigationMutation(operation) {
|
|
1773
|
+
try {
|
|
1774
|
+
return await operation();
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
1777
|
+
throw new Error(`browser action may have been dispatched; the action outcome is unknown because the navigation mutation API failed. Inspect the page before retrying. (${detail})`);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
async function invokeBrowserTabMutation(operation, phase) {
|
|
1782
|
+
try {
|
|
1783
|
+
return await operation();
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
1786
|
+
throw new Error(`browser tab mutation may have been dispatched; the outcome is unknown during ${phase}. Inspect tabs before retrying. (${detail})`);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
490
1789
|
|
|
1790
|
+
async function executePageMutation(target, method, params, state) {
|
|
1791
|
+
await chrome.scripting.executeScript({ target, files: ["page-automation.js"] });
|
|
1792
|
+
throwIfCancelled(state);
|
|
1793
|
+
let executions;
|
|
1794
|
+
try {
|
|
1795
|
+
executions = await chrome.scripting.executeScript({
|
|
1796
|
+
target,
|
|
1797
|
+
func: async (operation, payload, expectedVersion) => {
|
|
1798
|
+
const protocol = "machine_bridge_page_mutation_v1";
|
|
1799
|
+
try {
|
|
1800
|
+
const api = globalThis.__machineBridgePageAutomation;
|
|
1801
|
+
if (!api || api.version !== expectedVersion) throw new Error("page automation module version mismatch");
|
|
1802
|
+
if (typeof api[operation] !== "function") throw new Error("page automation module is unavailable");
|
|
1803
|
+
return { protocol, ok: true, result: await api[operation](payload) };
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
return { protocol, ok: false, error: String(error?.message || error).replace(/\s+/g, " ").slice(0, 1000) };
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
args: [method, params, PAGE_AUTOMATION_VERSION],
|
|
1809
|
+
});
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
throw pageMutationDispatchUnknown(error);
|
|
1812
|
+
}
|
|
1813
|
+
const settlement = Array.isArray(executions) && executions.length === 1 ? executions[0]?.result : null;
|
|
1814
|
+
if (settlement?.protocol !== "machine_bridge_page_mutation_v1") {
|
|
1815
|
+
throw pageMutationDispatchUnknown(new Error("page mutation settlement response was unavailable"));
|
|
1816
|
+
}
|
|
1817
|
+
if (settlement.ok === false) {
|
|
1818
|
+
if (typeof settlement.error !== "string" || !settlement.error || settlement.error.includes("\0") || settlement.error.length > 1000) {
|
|
1819
|
+
throw pageMutationSettlementUnknown();
|
|
1820
|
+
}
|
|
1821
|
+
throw new Error(settlement.error);
|
|
1822
|
+
}
|
|
1823
|
+
if (settlement.ok !== true) throw pageMutationSettlementUnknown();
|
|
1824
|
+
return [{ ...executions[0], result: settlement.result }];
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function pageMutationSettlementUnknown() {
|
|
1828
|
+
return new Error("browser action may have been dispatched; the action outcome is unknown because the page mutation settlement was malformed. Inspect the page before retrying.");
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
function pageMutationDispatchUnknown(error) {
|
|
1832
|
+
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
1833
|
+
return new Error(`browser action may have been dispatched; the action outcome is unknown because the page mutation scripting call did not settle. Inspect the page before retrying. (${detail})`);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
491
1836
|
function executePageAutomation(target, method, params) {
|
|
492
1837
|
return chrome.scripting.executeScript({ target, files: ["page-automation.js"] })
|
|
493
1838
|
.then(() => chrome.scripting.executeScript({
|
|
@@ -506,17 +1851,27 @@
|
|
|
506
1851
|
}
|
|
507
1852
|
|
|
508
1853
|
function boundedRequestTimeout(value) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
1854
|
+
if (!Number.isSafeInteger(value)) return 30000;
|
|
1855
|
+
return Math.max(1000, Math.min(185000, value));
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
function requestCancelled(error) {
|
|
1859
|
+
return String(error?.message || error) === "browser request cancelled";
|
|
512
1860
|
}
|
|
513
1861
|
|
|
514
1862
|
function throwIfCancelled(state) {
|
|
515
1863
|
if (state?.cancelled) throw new Error("browser request cancelled");
|
|
516
1864
|
}
|
|
517
1865
|
|
|
1866
|
+
function throwIfCancelledBeforeTrustedInput(state) {
|
|
1867
|
+
if (!state?.cancelled) return;
|
|
1868
|
+
const error = new Error("browser request cancelled");
|
|
1869
|
+
Object.defineProperty(error, "machineBridgeBeforeDispatchAbort", { value: true });
|
|
1870
|
+
throw error;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
518
1873
|
Object.defineProperty(globalThis, "__machineBridgeBrowserOperations", {
|
|
519
|
-
value: Object.freeze({ dispatch, boundedRequestTimeout, boundedDocumentSource }),
|
|
1874
|
+
value: Object.freeze({ dispatch, methodMayMutate, responsePayload, boundedRequestTimeout, boundedDocumentSource }),
|
|
520
1875
|
configurable: false,
|
|
521
1876
|
});
|
|
522
1877
|
})();
|