@reticlehq/server 3.8.2 → 3.10.0
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/LICENSE +201 -104
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/SKILL.md +13 -12
- package/bin/launch.js +13 -0
- package/bin/package.json +3 -0
- package/bin/reticle.js +66 -8
- package/dist/chromium-launch-options.d.ts +25 -0
- package/dist/chromium-launch-options.js +21 -1
- package/dist/command/cli/adhoc-suite.js +2 -0
- package/dist/command/cli/adhoc-verdict.d.ts +2 -43
- package/dist/command/cli/adhoc-verdict.js +50 -160
- package/dist/command/cli/answers/gate-exit.d.ts +6 -2
- package/dist/command/cli/answers/gate-exit.js +12 -2
- package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
- package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
- package/dist/command/cli/ci-hint.d.ts +44 -0
- package/dist/command/cli/ci-hint.js +72 -0
- package/dist/command/cli/cli-doctor.d.ts +6 -1
- package/dist/command/cli/cli-doctor.js +46 -5
- package/dist/command/cli/cli-flow-commands.d.ts +7 -1
- package/dist/command/cli/cli-flow-commands.js +39 -24
- package/dist/command/cli/cli-kill.d.ts +21 -0
- package/dist/command/cli/cli-kill.js +47 -4
- package/dist/command/cli/cli-parse-compare.d.ts +22 -0
- package/dist/command/cli/cli-parse-compare.js +75 -0
- package/dist/command/cli/cli-parse-grade.d.ts +36 -0
- package/dist/command/cli/cli-parse-grade.js +75 -0
- package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
- package/dist/command/cli/cli-parse-grammar.js +18 -20
- package/dist/command/cli/cli-parse-verify.d.ts +10 -0
- package/dist/command/cli/cli-parse-verify.js +63 -14
- package/dist/command/cli/cli-parse.d.ts +22 -17
- package/dist/command/cli/cli-parse.js +54 -32
- package/dist/command/cli/cli-update-commands.d.ts +1 -1
- package/dist/command/cli/cli-update-commands.js +29 -20
- package/dist/command/cli/cli-usage.js +61 -31
- package/dist/command/cli/cli-verify.d.ts +17 -3
- package/dist/command/cli/cli-verify.js +59 -21
- package/dist/command/cli/cloud-cli.js +2 -119
- package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
- package/dist/command/cli/daemon-link/daemon-client.js +205 -0
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
- package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
- package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
- package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
- package/dist/command/cli/doctor/doctor-fix.js +89 -0
- package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
- package/dist/command/cli/doctor/doctor-rows.js +4 -0
- package/dist/command/cli/doctor-repairs.d.ts +64 -0
- package/dist/command/cli/doctor-repairs.js +214 -0
- package/dist/command/cli/drive/drive-command.js +3 -7
- package/dist/command/cli/ensure-daemon.d.ts +26 -0
- package/dist/command/cli/ensure-daemon.js +57 -0
- package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
- package/dist/command/cli/finished-run/grade-command.js +180 -0
- package/dist/command/cli/first-use-checks.d.ts +41 -0
- package/dist/command/cli/first-use-checks.js +127 -0
- package/dist/command/cli/first-verdict-node.d.ts +3 -0
- package/dist/command/cli/first-verdict-node.js +189 -0
- package/dist/command/cli/first-verdict.d.ts +126 -0
- package/dist/command/cli/first-verdict.js +267 -0
- package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
- package/dist/command/cli/launch/sdk-version-match.js +3 -2
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +7 -6
- package/dist/command/cli/mcp-command.js +13 -4
- package/dist/command/cli/ports/port-holder.d.ts +11 -0
- package/dist/command/cli/ports/port-holder.js +36 -4
- package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
- package/dist/command/cli/setup-mcp-cli.js +98 -23
- package/dist/command/cli/status/status-lines.d.ts +2 -0
- package/dist/command/cli/status/status-lines.js +72 -17
- package/dist/command/cli/status-fields.d.ts +5 -1
- package/dist/command/cli/status-fields.js +24 -6
- package/dist/command/cli/try-command.d.ts +7 -0
- package/dist/command/cli/try-command.js +11 -3
- package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
- package/dist/command/cli/two-builds/compare-command.js +314 -0
- package/dist/command/cli/verify-against.d.ts +61 -0
- package/dist/command/cli/verify-against.js +187 -0
- package/dist/command/cli/verify-no-session.d.ts +2 -0
- package/dist/command/cli/verify-no-session.js +13 -0
- package/dist/command/cli/verify-results.d.ts +35 -2
- package/dist/command/cli/verify-results.js +76 -10
- package/dist/command/cli.js +112 -70
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
- package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
- package/dist/command/daemon/daemon-argv.d.ts +9 -0
- package/dist/command/daemon/daemon-argv.js +11 -0
- package/dist/command/daemon/daemon-resolve.d.ts +8 -1
- package/dist/command/daemon/daemon-resolve.js +27 -1
- package/dist/command/daemon/daemon.d.ts +22 -4
- package/dist/command/daemon/daemon.js +52 -10
- package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
- package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
- package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
- package/dist/command/daemon/lifetime/death-cause.js +149 -0
- package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
- package/dist/command/daemon/lifetime/heartbeat.js +11 -15
- package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
- package/dist/command/daemon/lifetime/startup-failure.js +62 -0
- package/dist/command/daemon/port-state.d.ts +14 -0
- package/dist/command/daemon/port-state.js +31 -0
- package/dist/command/setup/agent-configs.d.ts +2 -0
- package/dist/command/setup/agent-configs.js +42 -15
- package/dist/command/setup/agent-next-steps.d.ts +77 -0
- package/dist/command/setup/agent-next-steps.js +284 -0
- package/dist/command/setup/agent-writer.js +11 -6
- package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
- package/dist/command/setup/bringup/dev-server-files.js +17 -0
- package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
- package/dist/command/setup/bringup/static-page-server.js +43 -9
- package/dist/command/setup/init/init-host.js +2 -0
- package/dist/command/setup/init/init-runtime.d.ts +6 -1
- package/dist/command/setup/init/init-runtime.js +63 -21
- package/dist/command/setup/mcp-launcher.d.ts +3 -0
- package/dist/command/setup/mcp-launcher.js +11 -0
- package/dist/command/setup/node-effects.d.ts +3 -2
- package/dist/command/setup/node-effects.js +9 -3
- package/dist/command/setup/remaining-steps.d.ts +8 -0
- package/dist/command/setup/remaining-steps.js +10 -0
- package/dist/command/setup/run-setup.d.ts +2 -0
- package/dist/command/setup/run-setup.js +9 -1
- package/dist/command/setup/setup-command.js +36 -15
- package/dist/command/setup/setup-mcp.d.ts +8 -0
- package/dist/command/setup/setup-mcp.js +47 -20
- package/dist/command/setup/start-dev-server.d.ts +8 -0
- package/dist/command/setup/start-dev-server.js +43 -0
- package/dist/command/update/updater.js +6 -0
- package/dist/command/version/identity/server-version.d.ts +2 -0
- package/dist/command/version/identity/server-version.js +2 -0
- package/dist/command/version/version-skew.d.ts +7 -0
- package/dist/command/version/version-skew.js +14 -3
- package/dist/features/compare/action-plan.d.ts +260 -0
- package/dist/features/compare/action-plan.js +543 -0
- package/dist/features/compare/behaviour-compare.d.ts +24 -0
- package/dist/features/compare/behaviour-compare.js +85 -0
- package/dist/features/compare/behaviour-diff.d.ts +97 -0
- package/dist/features/compare/behaviour-diff.js +202 -0
- package/dist/features/compare/behaviour-report.d.ts +40 -0
- package/dist/features/compare/behaviour-report.js +128 -0
- package/dist/features/compare/build-readings.d.ts +106 -0
- package/dist/features/compare/build-readings.js +250 -0
- package/dist/features/compare/compare-diff.d.ts +62 -0
- package/dist/features/compare/compare-diff.js +177 -0
- package/dist/features/compare/compare-report-html.d.ts +6 -0
- package/dist/features/compare/compare-report-html.js +102 -0
- package/dist/features/compare/compare-report.d.ts +15 -0
- package/dist/features/compare/compare-report.js +73 -0
- package/dist/features/compare/compare-run.d.ts +60 -0
- package/dist/features/compare/compare-run.js +123 -0
- package/dist/features/compare/outside/effect-lines.d.ts +26 -0
- package/dist/features/compare/outside/effect-lines.js +103 -0
- package/dist/features/compare/outside/state-sources.d.ts +76 -0
- package/dist/features/compare/outside/state-sources.js +164 -0
- package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
- package/dist/features/compare/outside/value-dictionary.js +227 -0
- package/dist/features/compare/visible-content.d.ts +20 -0
- package/dist/features/compare/visible-content.js +125 -0
- package/dist/features/crawl/crawl-empty.js +2 -1
- package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
- package/dist/features/crawl/crawl-exhaustive.js +138 -0
- package/dist/features/crawl/crawl-tools.d.ts +9 -2
- package/dist/features/crawl/crawl-tools.js +28 -12
- package/dist/features/crawl/crawl.d.ts +8 -9
- package/dist/features/crawl/crawl.js +61 -65
- package/dist/features/exhaust/app-coverage.d.ts +5 -1
- package/dist/features/exhaust/app-coverage.js +3 -1
- package/dist/features/exhaust/code-coverage.js +8 -4
- package/dist/features/exhaust/edge-cases.d.ts +133 -0
- package/dist/features/exhaust/edge-cases.js +310 -0
- package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
- package/dist/features/exhaust/edges/edge-drive.js +364 -0
- package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
- package/dist/features/exhaust/edges/edge-run.js +169 -0
- package/dist/features/exhaust/explorer.d.ts +87 -14
- package/dist/features/exhaust/explorer.js +159 -25
- package/dist/features/exhaust/failure-branches.d.ts +5 -0
- package/dist/features/exhaust/failure-branches.js +29 -11
- package/dist/features/exhaust/invariants.d.ts +112 -0
- package/dist/features/exhaust/invariants.js +188 -0
- package/dist/features/exhaust/ledger.d.ts +68 -0
- package/dist/features/exhaust/ledger.js +121 -13
- package/dist/features/exhaust/session-fold.d.ts +10 -0
- package/dist/features/exhaust/session-fold.js +46 -3
- package/dist/features/exhaust/session-port.d.ts +3 -1
- package/dist/features/exhaust/session-port.js +97 -28
- package/dist/features/grade/grade-answer.d.ts +99 -0
- package/dist/features/grade/grade-answer.js +112 -0
- package/dist/features/grade/grade-noise.d.ts +27 -0
- package/dist/features/grade/grade-noise.js +55 -0
- package/dist/features/grade/grade-renumber.d.ts +12 -0
- package/dist/features/grade/grade-renumber.js +86 -0
- package/dist/features/grade/grade-state.d.ts +50 -0
- package/dist/features/grade/grade-state.js +93 -0
- package/dist/features/grade/grade-suggest.d.ts +33 -0
- package/dist/features/grade/grade-suggest.js +98 -0
- package/dist/features/license/license.d.ts +11 -71
- package/dist/features/license/license.js +43 -136
- package/dist/features/visual/desktop-capture.d.ts +33 -0
- package/dist/features/visual/desktop-capture.js +94 -0
- package/dist/features/visual/step-shots.js +139 -35
- package/dist/features/visual/visual-tools.d.ts +0 -18
- package/dist/features/visual/visual-tools.js +67 -99
- package/dist/import-graph.js +5 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +19 -7
- package/dist/judgement/capsule/causal-summary.js +9 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +16 -2
- package/dist/judgement/runs/run-ledger.d.ts +7 -5
- package/dist/judgement/runs/run-ledger.js +98 -69
- package/dist/judgement/runs/runner-port.d.ts +2 -0
- package/dist/judgement/runs/runner-port.js +23 -5
- package/dist/judgement/runs/verification-sync.d.ts +6 -3
- package/dist/judgement/runs/verification-sync.js +21 -10
- package/dist/language/flows/affected-tools.d.ts +1 -1
- package/dist/language/flows/affected-tools.js +49 -30
- package/dist/language/flows/change/git-changed.d.ts +2 -1
- package/dist/language/flows/change/git-changed.js +19 -2
- package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
- package/dist/language/flows/fields/flow-secret-field.js +77 -21
- package/dist/language/flows/flow-anchor.d.ts +60 -2
- package/dist/language/flows/flow-anchor.js +125 -20
- package/dist/language/flows/flow-classify.d.ts +20 -0
- package/dist/language/flows/flow-classify.js +90 -12
- package/dist/language/flows/flow-cross-step.d.ts +7 -1
- package/dist/language/flows/flow-cross-step.js +25 -1
- package/dist/language/flows/flow-journey.d.ts +38 -4
- package/dist/language/flows/flow-journey.js +138 -8
- package/dist/language/flows/flow-replay-run.d.ts +0 -16
- package/dist/language/flows/flow-replay-run.js +76 -14
- package/dist/language/flows/flow-replay-types.d.ts +4 -2
- package/dist/language/flows/flow-replay.d.ts +19 -1
- package/dist/language/flows/flow-replay.js +169 -52
- package/dist/language/flows/flow-replayable.js +7 -1
- package/dist/language/flows/flow-start-path.d.ts +34 -5
- package/dist/language/flows/flow-start-path.js +202 -15
- package/dist/language/flows/flow-step-runners.d.ts +14 -13
- package/dist/language/flows/flow-step-runners.js +184 -109
- package/dist/language/flows/flow-store-for-session.d.ts +26 -25
- package/dist/language/flows/flow-store-for-session.js +51 -5
- package/dist/language/flows/flow-tools.js +46 -33
- package/dist/language/flows/flows.js +176 -76
- package/dist/language/flows/recording/tape/recordings.d.ts +21 -16
- package/dist/language/flows/recording/tape/recordings.js +112 -49
- package/dist/language/flows/replay.d.ts +36 -12
- package/dist/language/flows/replay.js +193 -50
- package/dist/language/flows/steps/page-steps.d.ts +68 -0
- package/dist/language/flows/steps/page-steps.js +198 -0
- package/dist/language/flows/suite/sequential-run.d.ts +68 -0
- package/dist/language/flows/suite/sequential-run.js +96 -0
- package/dist/language/flows/verify-change-tools.js +26 -15
- package/dist/launch-chromium.d.ts +1 -0
- package/dist/launch-chromium.js +49 -12
- package/dist/memory/cloud/background-sync.d.ts +13 -0
- package/dist/memory/cloud/background-sync.js +24 -3
- package/dist/memory/cloud/sync-daemon.js +26 -27
- package/dist/memory/cloud/sync-screenshots.js +6 -1
- package/dist/memory/journal/attach-journal.js +2 -1
- package/dist/memory/journal/drive-flow.d.ts +7 -0
- package/dist/memory/journal/drive-flow.js +44 -13
- package/dist/memory/journal/journal-recorder.d.ts +4 -2
- package/dist/memory/journal/session-journal.d.ts +7 -0
- package/dist/memory/journal/session-journal.js +58 -10
- package/dist/memory/project/root-claims.d.ts +9 -0
- package/dist/memory/project/root-claims.js +9 -4
- package/dist/portal/bridge/bridge.d.ts +13 -4
- package/dist/portal/bridge/bridge.js +10 -9
- package/dist/portal/input/download-capture.d.ts +7 -0
- package/dist/portal/input/download-capture.js +92 -0
- package/dist/portal/input/network-detail.d.ts +6 -0
- package/dist/portal/input/network-detail.js +12 -1
- package/dist/portal/input/network-mock-tools.js +49 -13
- package/dist/portal/input/network-mock.d.ts +8 -0
- package/dist/portal/input/network-mock.js +30 -10
- package/dist/portal/input/page-capture.d.ts +24 -0
- package/dist/portal/input/page-capture.js +33 -0
- package/dist/portal/input/real-input.d.ts +35 -18
- package/dist/portal/input/real-input.js +99 -33
- package/dist/portal/input/viewport-tools.d.ts +8 -0
- package/dist/portal/input/viewport-tools.js +81 -21
- package/dist/portal/pool/browser-pool.d.ts +16 -30
- package/dist/portal/pool/browser-pool.js +64 -52
- package/dist/portal/pool/context-permissions.d.ts +34 -17
- package/dist/portal/pool/context-permissions.js +94 -1
- package/dist/portal/pool/csp-widen.d.ts +62 -0
- package/dist/portal/pool/csp-widen.js +132 -0
- package/dist/portal/pool/determinism.d.ts +52 -0
- package/dist/portal/pool/determinism.js +259 -0
- package/dist/portal/pool/lease-emulation.d.ts +34 -1
- package/dist/portal/pool/lease-emulation.js +18 -0
- package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
- package/dist/portal/pool/lease-marker-stamp.js +15 -1
- package/dist/portal/pool/lease-page-error.d.ts +56 -0
- package/dist/portal/pool/lease-page-error.js +160 -0
- package/dist/portal/pool/page-error-journal.d.ts +63 -0
- package/dist/portal/pool/page-error-journal.js +208 -0
- package/dist/portal/pool/playwright-launcher.js +71 -15
- package/dist/portal/pool/pool-contract.d.ts +49 -1
- package/dist/portal/pool/pool-lease.d.ts +70 -2
- package/dist/portal/pool/pool-lease.js +23 -0
- package/dist/portal/pool/zero-install.d.ts +8 -0
- package/dist/portal/pool/zero-install.js +91 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +11 -2
- package/dist/portal/session/dev-server/dev-command.js +110 -16
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
- package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
- package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
- package/dist/portal/session/dev-server/project-ports.js +62 -0
- package/dist/portal/session/facts/departed-network.d.ts +66 -0
- package/dist/portal/session/facts/departed-network.js +142 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
- package/dist/portal/session/no-session-diagnosis.js +76 -27
- package/dist/portal/session/no-session-next-action.d.ts +14 -0
- package/dist/portal/session/no-session-next-action.js +56 -7
- package/dist/portal/session/no-session-watch.d.ts +6 -0
- package/dist/portal/session/no-session-watch.js +41 -4
- package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
- package/dist/portal/session/page-commands/undeclared-command.js +16 -7
- package/dist/portal/session/recorded-gaps.d.ts +1 -1
- package/dist/portal/session/recorded-gaps.js +5 -2
- package/dist/portal/session/serving-directory.d.ts +10 -0
- package/dist/portal/session/serving-directory.js +39 -8
- package/dist/portal/session/session-manager.d.ts +10 -0
- package/dist/portal/session/session-manager.js +211 -81
- package/dist/portal/session/session.js +4 -3
- package/dist/portal/session/zero-install-first-call.d.ts +20 -1
- package/dist/portal/session/zero-install-first-call.js +34 -2
- package/dist/start-wiring.d.ts +30 -2
- package/dist/start-wiring.js +74 -9
- package/dist/surface/mcp/mcp-proxy.js +10 -2
- package/dist/surface/mcp/mcp.js +8 -5
- package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
- package/dist/surface/mcp/peer/agents-connected.js +58 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
- package/dist/surface/mcp/shared-params.d.ts +2 -0
- package/dist/surface/mcp/shared-params.js +31 -0
- package/dist/surface/tools/act/act-observation.d.ts +40 -2
- package/dist/surface/tools/act/act-observation.js +118 -12
- package/dist/surface/tools/act/act-preflight.js +38 -18
- package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
- package/dist/surface/tools/act/act-sequence-retry.js +61 -5
- package/dist/surface/tools/act/act-target.js +3 -1
- package/dist/surface/tools/act/after-match.d.ts +10 -0
- package/dist/surface/tools/act/after-match.js +40 -0
- package/dist/surface/tools/act/already-true.js +1 -1
- package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
- package/dist/surface/tools/act/navigation/reload-result.js +25 -2
- package/dist/surface/tools/act/resolve-target.d.ts +17 -1
- package/dist/surface/tools/act/resolve-target.js +59 -9
- package/dist/surface/tools/act/sequence-grade.d.ts +15 -1
- package/dist/surface/tools/act/sequence-grade.js +45 -27
- package/dist/surface/tools/act/settle-in-flight.js +27 -15
- package/dist/surface/tools/act-command.d.ts +23 -0
- package/dist/surface/tools/act-command.js +50 -0
- package/dist/surface/tools/act-sequence-tool.js +81 -50
- package/dist/surface/tools/act-tools.d.ts +1 -13
- package/dist/surface/tools/act-tools.js +83 -95
- package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
- package/dist/surface/tools/agent/build-staleness.js +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +18 -12
- package/dist/surface/tools/browser-tools.js +87 -13
- package/dist/surface/tools/compare-tools.d.ts +11 -0
- package/dist/surface/tools/compare-tools.js +94 -0
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.js +11 -3
- package/dist/surface/tools/empty-session-list.d.ts +2 -0
- package/dist/surface/tools/empty-session-list.js +4 -2
- package/dist/surface/tools/error-recovery.d.ts +12 -1
- package/dist/surface/tools/error-recovery.js +131 -40
- package/dist/surface/tools/explore-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +29 -21
- package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
- package/dist/surface/tools/lease-coverage-tools.js +128 -0
- package/dist/surface/tools/lease-hint.d.ts +42 -1
- package/dist/surface/tools/lease-hint.js +52 -3
- package/dist/surface/tools/lease-readiness.d.ts +47 -0
- package/dist/surface/tools/lease-readiness.js +126 -0
- package/dist/surface/tools/lease-seed.d.ts +16 -1
- package/dist/surface/tools/lease-seed.js +37 -4
- package/dist/surface/tools/lease-tools.d.ts +3 -41
- package/dist/surface/tools/lease-tools.js +104 -124
- package/dist/surface/tools/lease-url-identity.d.ts +46 -0
- package/dist/surface/tools/lease-url-identity.js +97 -0
- package/dist/surface/tools/merge-tools.js +99 -2
- package/dist/surface/tools/merged-name-redirect.js +5 -1
- package/dist/surface/tools/observe-tools.js +15 -9
- package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
- package/dist/surface/tools/read/console-pre-attach.js +23 -0
- package/dist/surface/tools/read-tools.js +33 -30
- package/dist/surface/tools/real-input-attempt.d.ts +0 -8
- package/dist/surface/tools/real-input-attempt.js +143 -28
- package/dist/surface/tools/repeat-hint.d.ts +40 -0
- package/dist/surface/tools/repeat-hint.js +171 -0
- package/dist/surface/tools/run-report.d.ts +2 -2
- package/dist/surface/tools/run-report.js +27 -15
- package/dist/surface/tools/session-ports.d.ts +14 -0
- package/dist/surface/tools/session-ports.js +53 -0
- package/dist/surface/tools/tool-kit.d.ts +18 -0
- package/dist/surface/tools/tool-kit.js +9 -3
- package/dist/surface/tools/tool-surface.js +0 -1
- package/dist/surface/tools/tools.js +54 -18
- package/dist/surface/tools/type-spelling.d.ts +13 -0
- package/dist/surface/tools/type-spelling.js +36 -0
- package/dist/telemetry/agent-client.d.ts +6 -0
- package/dist/telemetry/agent-client.js +10 -0
- package/dist/telemetry/argument-shape.js +17 -7
- package/dist/telemetry/cli-telemetry.d.ts +10 -1
- package/dist/telemetry/cli-telemetry.js +15 -0
- package/dist/telemetry/init-telemetry.d.ts +2 -1
- package/dist/telemetry/init-telemetry.js +47 -2
- package/dist/telemetry/install-trace.js +23 -4
- package/dist/telemetry/onboarding-funnel.d.ts +19 -2
- package/dist/telemetry/onboarding-funnel.js +25 -3
- package/dist/telemetry/proxy-telemetry.d.ts +2 -1
- package/dist/telemetry/proxy-telemetry.js +2 -2
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +67 -27
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +60 -28
- package/dist/telemetry/tool-refused.d.ts +6 -3
- package/dist/telemetry/tool-refused.js +15 -3
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/architecture.md +3 -6
- package/docs/capabilities.mdx +61 -1
- package/docs/cli/affected.mdx +9 -7
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/compare.mdx +94 -0
- package/docs/cli/doctor.mdx +41 -2
- package/docs/cli/grade.mdx +97 -0
- package/docs/cli/help.mdx +4 -2
- package/docs/cli/license.mdx +13 -12
- package/docs/cli/restart.mdx +7 -5
- package/docs/cli/rollback.mdx +5 -5
- package/docs/cli/setup.mdx +13 -2
- package/docs/cli/status.mdx +4 -0
- package/docs/cli/stop.mdx +3 -3
- package/docs/cli/update.mdx +2 -2
- package/docs/cli/verify.mdx +23 -1
- package/docs/cli.mdx +34 -24
- package/docs/deploy-checks.md +25 -0
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +17 -22
- package/docs/environment.mdx +1 -0
- package/docs/first-drive.md +1 -1
- package/docs/flows.md +11 -1
- package/docs/frameworks.mdx +9 -9
- package/docs/getting-started.md +1 -1
- package/docs/install-manual.mdx +16 -1
- package/docs/license-activation.mdx +3 -3
- package/docs/packages/engine.mdx +1 -1
- package/docs/packages/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/platform-integration.md +1 -1
- package/docs/predicates.mdx +1 -1
- package/docs/quickstart.mdx +68 -49
- package/docs/skill-setup.md +1 -1
- package/docs/skill.md +14 -13
- package/docs/telemetry.md +6 -4
- package/docs/tools/navigate.mdx +1 -1
- package/docs/tools/verify.mdx +4 -1
- package/docs/troubleshooting.mdx +1 -1
- package/docs/usage.md +17 -14
- package/package.json +18 -14
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
- package/dist/features/exhaust/exhaust-run.d.ts +0 -33
- package/dist/features/exhaust/exhaust-run.js +0 -82
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser permissions on a leased context: granted before the first navigation, and read back where
|
|
3
|
+
* the browser can contradict the grant.
|
|
4
|
+
*
|
|
5
|
+
* Never changed on a lease that is already open. Acquire hands an origin's live lease to whoever asks
|
|
6
|
+
* for that origin, so the lease may be another agent's tab mid-flow, and nothing here knows whose it
|
|
7
|
+
* is. A different set means a fresh lease.
|
|
8
|
+
*
|
|
9
|
+
* Each grant is scoped to the origin the lease was opened on, so a third-party frame the app embeds
|
|
10
|
+
* is not handed the same grant as the app.
|
|
11
|
+
*
|
|
12
|
+
* Notifications are why the read-back exists. Playwright's headless shell — what the pool launches
|
|
13
|
+
* when Playwright's own Chromium is installed — has no notification support: after a grant,
|
|
14
|
+
* `navigator.permissions.query` reports `granted` while `Notification.permission` still reads
|
|
15
|
+
* `denied`. A page that gates its UI on the second never sees the grant, so a lease that reported
|
|
16
|
+
* "granted" there would be reporting something the page cannot see.
|
|
17
|
+
*/
|
|
18
|
+
import { isLoopbackHostname } from '@reticlehq/core';
|
|
19
|
+
import { allowDaemonInCsp, allowDaemonInHtml } from './csp-widen.js';
|
|
1
20
|
/** The permission whose grant the headless shell does not pass on to `Notification.permission`. */
|
|
2
21
|
export const NOTIFICATIONS_PERMISSION = 'notifications';
|
|
3
22
|
/** Evaluated in the leased page. A string, because the pool's page surface evaluates scripts. */
|
|
@@ -19,8 +38,9 @@ function reasonOf(err) {
|
|
|
19
38
|
}
|
|
20
39
|
/** Grant `permissions` on the origin of `url`. Nothing to grant is a no-op, not a refusal. */
|
|
21
40
|
export async function grantLeasePermissions(context, permissions, url) {
|
|
22
|
-
if (0 === permissions.length)
|
|
41
|
+
if (0 === permissions.length) {
|
|
23
42
|
return;
|
|
43
|
+
}
|
|
24
44
|
if (context.grantPermissions === undefined) {
|
|
25
45
|
throw new LeasePermissionError('this browser context cannot grant permissions');
|
|
26
46
|
}
|
|
@@ -31,6 +51,36 @@ export async function grantLeasePermissions(context, permissions, url) {
|
|
|
31
51
|
throw new LeasePermissionError(`could not grant ${permissions.join(', ')}: ${reasonOf(err)}`);
|
|
32
52
|
}
|
|
33
53
|
}
|
|
54
|
+
/** The origin of `url` when the page is not on loopback, else undefined (also for no url). */
|
|
55
|
+
export function offLoopbackOrigin(url) {
|
|
56
|
+
if (url === undefined || !URL.canParse(url)) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const target = new URL(url);
|
|
60
|
+
return isLoopbackHostname(target.hostname) ? undefined : target.origin;
|
|
61
|
+
}
|
|
62
|
+
/** Playwright's name for Chrome's Local Network Access grant (local + loopback network). */
|
|
63
|
+
export const LOCAL_NETWORK_ACCESS = 'local-network-access';
|
|
64
|
+
/**
|
|
65
|
+
* Let a page that is not on loopback dial the bridge on loopback. Chromium blocks that unless a
|
|
66
|
+
* person grants Local Network Access, and a leased browser has no person, so a lease on a hosted
|
|
67
|
+
* preview loaded, signed in, and answered `sdk_never_dialled`. A loopback page needs no grant.
|
|
68
|
+
*
|
|
69
|
+
* ponytail: best effort. A Playwright older than the permission name refuses it; the lease then
|
|
70
|
+
* opens as before, and a page that cannot dial still reports `sdk_never_dialled`.
|
|
71
|
+
*/
|
|
72
|
+
export async function grantLocalNetworkAccess(context, url) {
|
|
73
|
+
const origin = offLoopbackOrigin(url);
|
|
74
|
+
if (context.grantPermissions === undefined || origin === undefined) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await context.grantPermissions([LOCAL_NETWORK_ACCESS], { origin });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// An unknown permission name on an older Playwright: open the lease without it.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
34
84
|
/**
|
|
35
85
|
* What the page's `Notification.permission` says, or undefined when it cannot be asked.
|
|
36
86
|
*
|
|
@@ -57,3 +107,46 @@ export async function readNotificationPermission(page) {
|
|
|
57
107
|
clearTimeout(timer);
|
|
58
108
|
}
|
|
59
109
|
}
|
|
110
|
+
/** How long the loopback CSP probe may take. Bounded because acquire waits on it. */
|
|
111
|
+
export const CSP_PROBE_TIMEOUT_MS = 2_000;
|
|
112
|
+
/**
|
|
113
|
+
* Whether the document at `url` sends a CSP (header or meta tag) that would block a dial to
|
|
114
|
+
* `daemonOrigin` (#2163). False when it cannot be asked: the lease then opens as before.
|
|
115
|
+
*/
|
|
116
|
+
export async function pageCspBlocksDaemon(url, daemonOrigin, probe = fetch) {
|
|
117
|
+
try {
|
|
118
|
+
const response = await probe(url, { signal: AbortSignal.timeout(CSP_PROBE_TIMEOUT_MS) });
|
|
119
|
+
const header = response.headers.get('content-security-policy');
|
|
120
|
+
if (header !== null && allowDaemonInCsp(header, daemonOrigin).changed) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (!/html/i.test(response.headers.get('content-type') ?? '')) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return allowDaemonInHtml(await response.text(), daemonOrigin).changed;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* On a lease whose page's CSP would block the daemon, add `daemonOrigin` to its `connect-src`
|
|
134
|
+
* (#2016). Off loopback this always installs the route. On loopback it installs it only when a
|
|
135
|
+
* probe of the page finds a CSP that blocks the dial (#2163), because the route sees every request
|
|
136
|
+
* in the context, and a dev server that sends no CSP should not pay for it. The returned record
|
|
137
|
+
* gets `widenedFor` once a response was really widened, so the lease can say so.
|
|
138
|
+
*/
|
|
139
|
+
export async function widenLeaseCsp(context, url, daemonOrigin, probe) {
|
|
140
|
+
const widening = {};
|
|
141
|
+
if (daemonOrigin === undefined || context.widenCsp === undefined) {
|
|
142
|
+
return widening;
|
|
143
|
+
}
|
|
144
|
+
const needed = offLoopbackOrigin(url) !== undefined ||
|
|
145
|
+
(URL.canParse(url) && (await pageCspBlocksDaemon(url, daemonOrigin, probe)));
|
|
146
|
+
if (needed) {
|
|
147
|
+
await context.widenCsp(daemonOrigin, () => {
|
|
148
|
+
widening.widenedFor = daemonOrigin;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return widening;
|
|
152
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Let the zero-install reader dial the daemon from a page whose CSP does not allow it (#2016).
|
|
3
|
+
*
|
|
4
|
+
* The page keeps its own Content-Security-Policy. The rewrite adds one source, the daemon's
|
|
5
|
+
* WebSocket origin, to `connect-src`, and changes nothing else. A bypass of page CSP would let a
|
|
6
|
+
* lease pass where a real user hits a CSP block, which is a false green.
|
|
7
|
+
*
|
|
8
|
+
* The rewrite changes the page that the agent verifies, so it reports each change (`changed`, and
|
|
9
|
+
* the `onWidened` call), and the lease result says `cspWidenedFor`.
|
|
10
|
+
*/
|
|
11
|
+
/** The result of a rewrite: the new text, and whether it differs from the text that came in. */
|
|
12
|
+
export interface CspRewrite {
|
|
13
|
+
policy: string;
|
|
14
|
+
changed: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Add `origin` to the `connect-src` of each policy in `policy`. A comma joins several policies,
|
|
18
|
+
* and a browser enforces all of them, so each one gets the origin. A policy with no `connect-src`
|
|
19
|
+
* and no `default-src` already allows the dial, and stays as it is.
|
|
20
|
+
*/
|
|
21
|
+
export declare function allowDaemonInCsp(policy: string, origin: string): CspRewrite;
|
|
22
|
+
/**
|
|
23
|
+
* Apply `allowDaemonInCsp` to each `<meta http-equiv="Content-Security-Policy">` in `html`.
|
|
24
|
+
* ponytail: reads the attribute text as it is; an entity-encoded quote inside it stays encoded.
|
|
25
|
+
*/
|
|
26
|
+
export declare function allowDaemonInHtml(html: string, origin: string): {
|
|
27
|
+
html: string;
|
|
28
|
+
changed: boolean;
|
|
29
|
+
};
|
|
30
|
+
/** The part of a Playwright `Route` the rewrite uses. A real `Route` satisfies it. */
|
|
31
|
+
export interface CspRoute<R extends CspResponse = CspResponse> {
|
|
32
|
+
request(): {
|
|
33
|
+
resourceType(): string;
|
|
34
|
+
frame(): {
|
|
35
|
+
parentFrame(): unknown;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
fallback(): Promise<void>;
|
|
39
|
+
fetch(opts: {
|
|
40
|
+
maxRedirects: number;
|
|
41
|
+
}): Promise<R>;
|
|
42
|
+
fulfill(opts: {
|
|
43
|
+
response: R;
|
|
44
|
+
headers?: Record<string, string>;
|
|
45
|
+
body?: string;
|
|
46
|
+
}): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** The part of a Playwright `APIResponse` the rewrite uses. */
|
|
49
|
+
export interface CspResponse {
|
|
50
|
+
headers(): Record<string, string>;
|
|
51
|
+
headersArray(): {
|
|
52
|
+
name: string;
|
|
53
|
+
value: string;
|
|
54
|
+
}[];
|
|
55
|
+
text(): Promise<string>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A route handler for a lease context: on the top document, add `origin` to its CSP header and
|
|
59
|
+
* CSP meta tags, and call `onWidened` when that changed anything. Every other request goes on.
|
|
60
|
+
* `Content-Security-Policy-Report-Only` blocks nothing, so it stays as the server sent it.
|
|
61
|
+
*/
|
|
62
|
+
export declare function widenDocumentCsp<R extends CspResponse>(route: CspRoute<R>, origin: string, onWidened: () => void): Promise<void>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Let the zero-install reader dial the daemon from a page whose CSP does not allow it (#2016).
|
|
3
|
+
*
|
|
4
|
+
* The page keeps its own Content-Security-Policy. The rewrite adds one source, the daemon's
|
|
5
|
+
* WebSocket origin, to `connect-src`, and changes nothing else. A bypass of page CSP would let a
|
|
6
|
+
* lease pass where a real user hits a CSP block, which is a false green.
|
|
7
|
+
*
|
|
8
|
+
* The rewrite changes the page that the agent verifies, so it reports each change (`changed`, and
|
|
9
|
+
* the `onWidened` call), and the lease result says `cspWidenedFor`.
|
|
10
|
+
*/
|
|
11
|
+
const CONNECT_SRC = 'connect-src';
|
|
12
|
+
const DEFAULT_SRC = 'default-src';
|
|
13
|
+
const NONE_SOURCE = "'none'";
|
|
14
|
+
const CSP_HEADER = 'content-security-policy';
|
|
15
|
+
const DOCUMENT = 'document';
|
|
16
|
+
const tokensOf = (directive) => directive.trim().split(/\s+/);
|
|
17
|
+
const nameOf = (directive) => (tokensOf(directive)[0] ?? '').toLowerCase();
|
|
18
|
+
/** `name` plus its sources plus `origin`. `'none'` goes, because it is valid only alone. */
|
|
19
|
+
function withOrigin(name, sources, origin) {
|
|
20
|
+
const kept = sources.filter((s) => s.toLowerCase() !== NONE_SOURCE);
|
|
21
|
+
return [name, ...kept, origin].join(' ');
|
|
22
|
+
}
|
|
23
|
+
/** One policy (no commas). Only the first `connect-src` counts; a browser ignores a repeat. */
|
|
24
|
+
function allowInOnePolicy(policy, origin) {
|
|
25
|
+
const directives = policy.split(';');
|
|
26
|
+
const connect = directives.findIndex((d) => nameOf(d) === CONNECT_SRC);
|
|
27
|
+
if (connect !== -1) {
|
|
28
|
+
const [name = CONNECT_SRC, ...sources] = tokensOf(directives[connect] ?? '');
|
|
29
|
+
if (sources.includes(origin)) {
|
|
30
|
+
return policy;
|
|
31
|
+
}
|
|
32
|
+
const lead = /^\s*/.exec(directives[connect] ?? '')?.[0] ?? '';
|
|
33
|
+
directives[connect] = `${lead}${withOrigin(name, sources, origin)}`;
|
|
34
|
+
return directives.join(';');
|
|
35
|
+
}
|
|
36
|
+
const fallback = directives.find((d) => nameOf(d) === DEFAULT_SRC);
|
|
37
|
+
if (fallback === undefined) {
|
|
38
|
+
return policy;
|
|
39
|
+
}
|
|
40
|
+
const head = policy.replace(/[\s;]+$/, '');
|
|
41
|
+
return `${head}; ${withOrigin(CONNECT_SRC, tokensOf(fallback).slice(1), origin)}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Add `origin` to the `connect-src` of each policy in `policy`. A comma joins several policies,
|
|
45
|
+
* and a browser enforces all of them, so each one gets the origin. A policy with no `connect-src`
|
|
46
|
+
* and no `default-src` already allows the dial, and stays as it is.
|
|
47
|
+
*/
|
|
48
|
+
export function allowDaemonInCsp(policy, origin) {
|
|
49
|
+
const next = policy
|
|
50
|
+
.split(',')
|
|
51
|
+
.map((one) => allowInOnePolicy(one, origin))
|
|
52
|
+
.join(',');
|
|
53
|
+
return { policy: next, changed: next !== policy };
|
|
54
|
+
}
|
|
55
|
+
const META_TAG = /<meta\b[^>]*>/gi;
|
|
56
|
+
const CSP_EQUIV = /\bhttp-equiv\s*=\s*["']?content-security-policy["']?(?=[\s/>])/i;
|
|
57
|
+
const CONTENT_ATTR = /(\bcontent\s*=\s*)("([^"]*)"|'([^']*)')/i;
|
|
58
|
+
/**
|
|
59
|
+
* Apply `allowDaemonInCsp` to each `<meta http-equiv="Content-Security-Policy">` in `html`.
|
|
60
|
+
* ponytail: reads the attribute text as it is; an entity-encoded quote inside it stays encoded.
|
|
61
|
+
*/
|
|
62
|
+
export function allowDaemonInHtml(html, origin) {
|
|
63
|
+
const next = html.replace(META_TAG, (tag) => {
|
|
64
|
+
if (!CSP_EQUIV.test(tag)) {
|
|
65
|
+
return tag;
|
|
66
|
+
}
|
|
67
|
+
return tag.replace(CONTENT_ATTR, (_all, prefix, quoted) => {
|
|
68
|
+
const quote = quoted.charAt(0);
|
|
69
|
+
const widened = allowDaemonInCsp(quoted.slice(1, -1), origin).policy;
|
|
70
|
+
return `${prefix}${quote}${widened}${quote}`;
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
return { html: next, changed: next !== html };
|
|
74
|
+
}
|
|
75
|
+
function isTopDocument(route) {
|
|
76
|
+
const request = route.request();
|
|
77
|
+
if (request.resourceType() !== DOCUMENT) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return null === request.frame().parentFrame();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// A request with no frame (a service worker's own fetch) is no page the reader runs in.
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A route handler for a lease context: on the top document, add `origin` to its CSP header and
|
|
90
|
+
* CSP meta tags, and call `onWidened` when that changed anything. Every other request goes on.
|
|
91
|
+
* `Content-Security-Policy-Report-Only` blocks nothing, so it stays as the server sent it.
|
|
92
|
+
*/
|
|
93
|
+
export async function widenDocumentCsp(route, origin, onWidened) {
|
|
94
|
+
if (!isTopDocument(route)) {
|
|
95
|
+
await route.fallback();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
let response;
|
|
99
|
+
try {
|
|
100
|
+
// No redirects followed here: a redirect goes back to the browser, so the page URL stays true.
|
|
101
|
+
response = await route.fetch({ maxRedirects: 0 });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
await route.fallback();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const policies = response
|
|
108
|
+
.headersArray()
|
|
109
|
+
.filter((h) => h.name.toLowerCase() === CSP_HEADER)
|
|
110
|
+
.map((h) => h.value);
|
|
111
|
+
const header = allowDaemonInCsp(policies.join(', '), origin);
|
|
112
|
+
const isHtml = /html/i.test(response.headers()['content-type'] ?? '');
|
|
113
|
+
const meta = isHtml ? allowDaemonInHtml(await response.text(), origin) : undefined;
|
|
114
|
+
if (!header.changed && true !== meta?.changed) {
|
|
115
|
+
await route.fulfill({ response });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const headers = { ...response.headers() };
|
|
119
|
+
if (header.changed) {
|
|
120
|
+
headers[CSP_HEADER] = header.policy;
|
|
121
|
+
}
|
|
122
|
+
if (true === meta?.changed) {
|
|
123
|
+
// The new body is plain text: the old encoding and length describe a body that is gone.
|
|
124
|
+
delete headers['content-encoding'];
|
|
125
|
+
delete headers['content-length'];
|
|
126
|
+
await route.fulfill({ response, headers, body: meta.html });
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
await route.fulfill({ response, headers });
|
|
130
|
+
}
|
|
131
|
+
onWidened();
|
|
132
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deterministic lease: the same page, driven the same way, gives the same pixels and text.
|
|
3
|
+
*
|
|
4
|
+
* Two runs of a canvas game differ because time and randomness differ. This script replaces both
|
|
5
|
+
* before any page script runs. Math.random and crypto take values from a seeded generator. Date,
|
|
6
|
+
* performance.now, the timers and requestAnimationFrame read a virtual clock that starts at a fixed
|
|
7
|
+
* epoch. The clock moves only when the caller steps frames, through `reticle_clock { stepFrames }`.
|
|
8
|
+
*
|
|
9
|
+
* The SDK in the page must keep real time, or it cannot wait, reconnect or send its heartbeat. The
|
|
10
|
+
* script keeps the real timers under `DETERMINISM_GLOBAL_KEY`, and the SDK reads them from there.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
/** What the caller asks for. */
|
|
14
|
+
export interface DeterminismOptions {
|
|
15
|
+
seed?: number;
|
|
16
|
+
/** The value of `navigator.platform`, for example "Linux x86_64". Absent keeps the real one. */
|
|
17
|
+
platform?: string;
|
|
18
|
+
}
|
|
19
|
+
/** What the page script is given. Plain data, because Playwright sends it as JSON. */
|
|
20
|
+
export interface DeterminismConfig {
|
|
21
|
+
key: string;
|
|
22
|
+
seed: number;
|
|
23
|
+
epochMs: number;
|
|
24
|
+
frameMs: number;
|
|
25
|
+
platform?: string;
|
|
26
|
+
}
|
|
27
|
+
/** What one step reports: the frames stepped since the page loaded, and the virtual time. */
|
|
28
|
+
export interface DeterminismStep {
|
|
29
|
+
frame: number;
|
|
30
|
+
nowMs: number;
|
|
31
|
+
}
|
|
32
|
+
export declare const DeterminismSchema: z.ZodObject<{
|
|
33
|
+
seed: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
platform: z.ZodOptional<z.ZodString>;
|
|
35
|
+
}, z.core.$strict>;
|
|
36
|
+
export declare const LEASE_DETERMINISTIC_ARG: z.ZodOptional<z.ZodObject<{
|
|
37
|
+
seed: z.ZodOptional<z.ZodNumber>;
|
|
38
|
+
platform: z.ZodOptional<z.ZodString>;
|
|
39
|
+
}, z.core.$strict>>;
|
|
40
|
+
/** A lease's `deterministic` argument, read; a field it does not know is refused by name. */
|
|
41
|
+
export declare function leaseDeterminismOf(raw: unknown): DeterminismOptions | undefined;
|
|
42
|
+
export declare function determinismConfigOf(opts: DeterminismOptions): DeterminismConfig;
|
|
43
|
+
export declare const StepFramesSchema: z.ZodNumber;
|
|
44
|
+
/** Step the page's virtual clock by `frames` frames. Undefined when the page runs no such clock. */
|
|
45
|
+
export declare function stepPage(page: {
|
|
46
|
+
evaluate?(script: string): Promise<unknown>;
|
|
47
|
+
}, frames: number): Promise<DeterminismStep | undefined>;
|
|
48
|
+
/**
|
|
49
|
+
* The page script. It must stay self-contained: Playwright sends its source text into the page, so
|
|
50
|
+
* it can read nothing from this module. The unit test runs it from its source text for that reason.
|
|
51
|
+
*/
|
|
52
|
+
export declare function installDeterminism(config: DeterminismConfig): void;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deterministic lease: the same page, driven the same way, gives the same pixels and text.
|
|
3
|
+
*
|
|
4
|
+
* Two runs of a canvas game differ because time and randomness differ. This script replaces both
|
|
5
|
+
* before any page script runs. Math.random and crypto take values from a seeded generator. Date,
|
|
6
|
+
* performance.now, the timers and requestAnimationFrame read a virtual clock that starts at a fixed
|
|
7
|
+
* epoch. The clock moves only when the caller steps frames, through `reticle_clock { stepFrames }`.
|
|
8
|
+
*
|
|
9
|
+
* The SDK in the page must keep real time, or it cannot wait, reconnect or send its heartbeat. The
|
|
10
|
+
* script keeps the real timers under `DETERMINISM_GLOBAL_KEY`, and the SDK reads them from there.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { DETERMINISM_DEFAULT_SEED, DETERMINISM_EPOCH_MS, DETERMINISM_FRAME_MS, DETERMINISM_GLOBAL_KEY, DETERMINISM_MAX_STEP_FRAMES, } from '@reticlehq/core';
|
|
14
|
+
export const DeterminismSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
seed: z.number().int().min(0).max(0xffffffff).optional(),
|
|
17
|
+
platform: z.string().min(1).max(64).optional(),
|
|
18
|
+
})
|
|
19
|
+
.strict();
|
|
20
|
+
export const LEASE_DETERMINISTIC_ARG = DeterminismSchema.optional().describe('Make the leased tab deterministic, for canvas games and animations. Before any page script ' +
|
|
21
|
+
'runs, Math.random and crypto.getRandomValues use a seeded generator (seed, default 1). Date, ' +
|
|
22
|
+
'performance.now, setTimeout, setInterval and requestAnimationFrame use a virtual clock that ' +
|
|
23
|
+
'starts at 2026-01-01T00:00:00Z and moves only when you call reticle_clock { stepFrames: N }. ' +
|
|
24
|
+
'devicePixelRatio is 1. platform sets navigator.platform, for example "Linux x86_64". CSS ' +
|
|
25
|
+
'animations, workers and media still run on real time. Example: deterministic: { seed: 7 }.');
|
|
26
|
+
/** A lease's `deterministic` argument, read; a field it does not know is refused by name. */
|
|
27
|
+
export function leaseDeterminismOf(raw) {
|
|
28
|
+
if (raw === undefined) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const parsed = DeterminismSchema.safeParse(raw);
|
|
32
|
+
if (!parsed.success) {
|
|
33
|
+
const issue = parsed.error.issues[0];
|
|
34
|
+
const field = issue?.path.join('.') ?? '';
|
|
35
|
+
throw new Error(`deterministic${'' === field ? '' : `.${field}`}: ${issue?.message ?? 'not valid'}`);
|
|
36
|
+
}
|
|
37
|
+
const { seed, platform } = parsed.data;
|
|
38
|
+
return {
|
|
39
|
+
...(seed === undefined ? {} : { seed }),
|
|
40
|
+
...(platform === undefined ? {} : { platform }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function determinismConfigOf(opts) {
|
|
44
|
+
return {
|
|
45
|
+
key: DETERMINISM_GLOBAL_KEY,
|
|
46
|
+
seed: opts.seed ?? DETERMINISM_DEFAULT_SEED,
|
|
47
|
+
epochMs: DETERMINISM_EPOCH_MS,
|
|
48
|
+
frameMs: DETERMINISM_FRAME_MS,
|
|
49
|
+
...(opts.platform === undefined ? {} : { platform: opts.platform }),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export const StepFramesSchema = z.number().int().min(1).max(DETERMINISM_MAX_STEP_FRAMES);
|
|
53
|
+
const StepResultSchema = z.object({ frame: z.number(), nowMs: z.number() });
|
|
54
|
+
/** Step the page's virtual clock by `frames` frames. Undefined when the page runs no such clock. */
|
|
55
|
+
export async function stepPage(page, frames) {
|
|
56
|
+
if (page.evaluate === undefined) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const key = JSON.stringify(DETERMINISM_GLOBAL_KEY);
|
|
60
|
+
const script = `globalThis[Symbol.for(${key})]?.step(${String(StepFramesSchema.parse(frames))})`;
|
|
61
|
+
const parsed = StepResultSchema.safeParse(await page.evaluate(script));
|
|
62
|
+
return parsed.success ? parsed.data : undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The page script. It must stay self-contained: Playwright sends its source text into the page, so
|
|
66
|
+
* it can read nothing from this module. The unit test runs it from its source text for that reason.
|
|
67
|
+
*/
|
|
68
|
+
export function installDeterminism(config) {
|
|
69
|
+
const g = globalThis;
|
|
70
|
+
const define = (target, name, value) => {
|
|
71
|
+
Object.defineProperty(target, name, { value, configurable: true, writable: true });
|
|
72
|
+
};
|
|
73
|
+
const getter = (target, name, value) => {
|
|
74
|
+
Object.defineProperty(target, name, { get: () => value, configurable: true });
|
|
75
|
+
};
|
|
76
|
+
// The real timers, kept before anything is replaced, for the SDK and for this script.
|
|
77
|
+
const realSetTimeout = g.setTimeout?.bind(g);
|
|
78
|
+
const realClearTimeout = g.clearTimeout?.bind(g);
|
|
79
|
+
const realFrame = 'function' === typeof g.requestAnimationFrame ? g.requestAnimationFrame.bind(g) : undefined;
|
|
80
|
+
const realPerformanceNow = g.performance?.now.bind(g.performance);
|
|
81
|
+
// Random: mulberry32, a small 32-bit generator. The same seed gives the same sequence.
|
|
82
|
+
let state = config.seed >>> 0;
|
|
83
|
+
const next = () => {
|
|
84
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
85
|
+
let t = state;
|
|
86
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
87
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
88
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
89
|
+
};
|
|
90
|
+
g.Math.random = next;
|
|
91
|
+
const fill = (view) => {
|
|
92
|
+
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
93
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
94
|
+
bytes[i] = Math.floor(next() * 256);
|
|
95
|
+
}
|
|
96
|
+
return view;
|
|
97
|
+
};
|
|
98
|
+
if (g.crypto !== undefined) {
|
|
99
|
+
define(g.crypto, 'getRandomValues', fill);
|
|
100
|
+
define(g.crypto, 'randomUUID', () => {
|
|
101
|
+
const b = fill(new Uint8Array(16));
|
|
102
|
+
b[6] = ((b[6] ?? 0) & 0x0f) | 0x40;
|
|
103
|
+
b[8] = ((b[8] ?? 0) & 0x3f) | 0x80;
|
|
104
|
+
const hex = Array.from(b, (n) => n.toString(16).padStart(2, '0')).join('');
|
|
105
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// Clock: `elapsed` is the virtual performance.now. It moves only in `step`.
|
|
109
|
+
let elapsed = 0;
|
|
110
|
+
let frame = 0;
|
|
111
|
+
const RealDate = g.Date;
|
|
112
|
+
const wallNow = () => config.epochMs + elapsed;
|
|
113
|
+
const VirtualDate = function (...args) {
|
|
114
|
+
if (new.target === undefined) {
|
|
115
|
+
return new RealDate(wallNow()).toString();
|
|
116
|
+
}
|
|
117
|
+
return Reflect.construct(RealDate, 0 === args.length ? [wallNow()] : args, new.target);
|
|
118
|
+
};
|
|
119
|
+
VirtualDate.prototype = RealDate.prototype;
|
|
120
|
+
define(VirtualDate, 'now', wallNow);
|
|
121
|
+
define(VirtualDate, 'parse', RealDate.parse.bind(RealDate));
|
|
122
|
+
define(VirtualDate, 'UTC', RealDate.UTC.bind(RealDate));
|
|
123
|
+
g.Date = VirtualDate;
|
|
124
|
+
if (g.performance !== undefined) {
|
|
125
|
+
define(g.performance, 'now', () => elapsed);
|
|
126
|
+
}
|
|
127
|
+
let nextId = 1;
|
|
128
|
+
const timers = new Map();
|
|
129
|
+
const schedule = (cb, ms, args, repeat) => {
|
|
130
|
+
const id = nextId;
|
|
131
|
+
nextId += 1;
|
|
132
|
+
if ('function' !== typeof cb) {
|
|
133
|
+
return id;
|
|
134
|
+
}
|
|
135
|
+
const delay = Math.max(0, Number(ms) || 0);
|
|
136
|
+
const run = () => {
|
|
137
|
+
cb(...args);
|
|
138
|
+
};
|
|
139
|
+
timers.set(id, {
|
|
140
|
+
id,
|
|
141
|
+
at: elapsed + delay,
|
|
142
|
+
run,
|
|
143
|
+
...(repeat ? { every: Math.max(1, delay) } : {}),
|
|
144
|
+
});
|
|
145
|
+
return id;
|
|
146
|
+
};
|
|
147
|
+
const cancel = (id) => {
|
|
148
|
+
timers.delete(Number(id));
|
|
149
|
+
};
|
|
150
|
+
g.setTimeout = (cb, ms, ...args) => schedule(cb, ms, args, false);
|
|
151
|
+
g.setInterval = (cb, ms, ...args) => schedule(cb, ms, args, true);
|
|
152
|
+
g.clearTimeout = cancel;
|
|
153
|
+
g.clearInterval = cancel;
|
|
154
|
+
let frames = [];
|
|
155
|
+
g.requestAnimationFrame = (cb) => {
|
|
156
|
+
const id = nextId;
|
|
157
|
+
nextId += 1;
|
|
158
|
+
if ('function' === typeof cb) {
|
|
159
|
+
frames.push({ id, cb: cb });
|
|
160
|
+
}
|
|
161
|
+
return id;
|
|
162
|
+
};
|
|
163
|
+
g.cancelAnimationFrame = (id) => {
|
|
164
|
+
frames = frames.filter((f) => f.id !== Number(id));
|
|
165
|
+
};
|
|
166
|
+
// A callback that throws is reported as the page reports it, and the rest still run. After each
|
|
167
|
+
// callback the microtasks run, as they do in a browser between two tasks.
|
|
168
|
+
const settle = () => new Promise((resolve) => {
|
|
169
|
+
if (g.MessageChannel === undefined) {
|
|
170
|
+
void Promise.resolve().then(resolve);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const channel = new g.MessageChannel();
|
|
174
|
+
channel.port1.onmessage = () => {
|
|
175
|
+
resolve();
|
|
176
|
+
};
|
|
177
|
+
channel.port2.postMessage(0);
|
|
178
|
+
});
|
|
179
|
+
const invoke = async (run) => {
|
|
180
|
+
try {
|
|
181
|
+
run();
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
realSetTimeout?.(() => {
|
|
185
|
+
throw error;
|
|
186
|
+
}, 0);
|
|
187
|
+
}
|
|
188
|
+
await settle();
|
|
189
|
+
};
|
|
190
|
+
const earliestDue = (until) => {
|
|
191
|
+
let found;
|
|
192
|
+
for (const t of timers.values()) {
|
|
193
|
+
if (t.at <= until && (found === undefined || t.at < found.at)) {
|
|
194
|
+
found = t;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return found;
|
|
198
|
+
};
|
|
199
|
+
const step = async (count) => {
|
|
200
|
+
for (let i = 0; i < count; i += 1) {
|
|
201
|
+
frame += 1;
|
|
202
|
+
const target = frame * config.frameMs;
|
|
203
|
+
for (let guard = 0; guard < 100000; guard += 1) {
|
|
204
|
+
const due = earliestDue(target);
|
|
205
|
+
if (due === undefined) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
elapsed = Math.max(elapsed, due.at);
|
|
209
|
+
timers.delete(due.id);
|
|
210
|
+
if (due.every !== undefined) {
|
|
211
|
+
timers.set(due.id, { ...due, at: due.at + due.every });
|
|
212
|
+
}
|
|
213
|
+
await invoke(due.run);
|
|
214
|
+
}
|
|
215
|
+
elapsed = target;
|
|
216
|
+
const flush = frames;
|
|
217
|
+
frames = [];
|
|
218
|
+
for (const f of flush) {
|
|
219
|
+
await invoke(() => {
|
|
220
|
+
f.cb(target);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { frame, nowMs: elapsed };
|
|
225
|
+
};
|
|
226
|
+
// Keyboard labels and shortcuts differ between Mac and Linux, so the platform can be set.
|
|
227
|
+
const nav = g.navigator;
|
|
228
|
+
if (config.platform !== undefined && nav !== undefined) {
|
|
229
|
+
const platform = config.platform;
|
|
230
|
+
const families = [
|
|
231
|
+
['Mac', 'macOS', '(Macintosh; Intel Mac OS X 10_15_7)'],
|
|
232
|
+
['Win', 'Windows', '(Windows NT 10.0; Win64; x64)'],
|
|
233
|
+
['Linux', 'Linux', `(X11; ${platform})`],
|
|
234
|
+
];
|
|
235
|
+
const family = families.find(([prefix]) => platform.startsWith(prefix));
|
|
236
|
+
getter(nav, 'platform', platform);
|
|
237
|
+
if (family !== undefined) {
|
|
238
|
+
if (nav.userAgentData !== undefined) {
|
|
239
|
+
getter(nav.userAgentData, 'platform', family[1]);
|
|
240
|
+
}
|
|
241
|
+
if (nav.userAgent !== undefined) {
|
|
242
|
+
getter(nav, 'userAgent', nav.userAgent.replace(/\([^)]*\)/, family[2]));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
Object.defineProperty(g, Symbol.for(config.key), {
|
|
247
|
+
value: {
|
|
248
|
+
step,
|
|
249
|
+
natives: {
|
|
250
|
+
setTimeout: realSetTimeout,
|
|
251
|
+
clearTimeout: realClearTimeout,
|
|
252
|
+
requestAnimationFrame: realFrame,
|
|
253
|
+
performanceNow: realPerformanceNow,
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
configurable: false,
|
|
257
|
+
enumerable: false,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* so the CSS half of reduced motion and the dark theme could not be checked live. A lease is a
|
|
6
6
|
* Playwright context, which takes all three before the first paint and the media again per call.
|
|
7
7
|
*/
|
|
8
|
+
import type { DeterminismOptions } from './determinism.js';
|
|
8
9
|
export declare const ColorScheme: {
|
|
9
10
|
readonly LIGHT: "light";
|
|
10
11
|
readonly DARK: "dark";
|
|
@@ -16,18 +17,50 @@ export declare const ReducedMotion: {
|
|
|
16
17
|
readonly NO_PREFERENCE: "no-preference";
|
|
17
18
|
};
|
|
18
19
|
export type ReducedMotion = (typeof ReducedMotion)[keyof typeof ReducedMotion];
|
|
20
|
+
export declare const Pointer: {
|
|
21
|
+
readonly FINE: "fine";
|
|
22
|
+
readonly COARSE: "coarse";
|
|
23
|
+
};
|
|
24
|
+
export type Pointer = (typeof Pointer)[keyof typeof Pointer];
|
|
19
25
|
/** The media features a page reads with matchMedia. */
|
|
20
26
|
export interface LeaseMedia {
|
|
21
27
|
colorScheme?: ColorScheme;
|
|
22
28
|
reducedMotion?: ReducedMotion;
|
|
23
29
|
}
|
|
24
|
-
/** What a lease opens with: the media, and
|
|
30
|
+
/** What a lease opens with: the media, a viewport, and the pointer the page reads. */
|
|
25
31
|
export interface LeaseEmulation extends LeaseMedia {
|
|
26
32
|
viewport?: {
|
|
27
33
|
width: number;
|
|
28
34
|
height: number;
|
|
29
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* The primary pointer `@media (pointer: ...)` reports. Unlike the media features, it is fixed
|
|
38
|
+
* when the context opens: Playwright's touch support is a context option, so a live lease cannot
|
|
39
|
+
* be given one. Without it a touch-only rule could not be reached at all, and a lease answered
|
|
40
|
+
* every `(pointer: coarse)` query with a mouse (#1903).
|
|
41
|
+
*/
|
|
42
|
+
pointer?: Pointer;
|
|
43
|
+
/** A seeded random source and a stepped clock, installed before any page script (determinism.ts). */
|
|
44
|
+
deterministic?: DeterminismOptions;
|
|
30
45
|
}
|
|
46
|
+
/** The same emulation in the names Playwright's own context options use. */
|
|
47
|
+
export interface ContextEmulation extends LeaseMedia {
|
|
48
|
+
viewport?: {
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
};
|
|
52
|
+
hasTouch?: boolean;
|
|
53
|
+
deviceScaleFactor?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Translate a lease's emulation into context options. `pointer` is not one of Playwright's: a
|
|
57
|
+
* coarse primary pointer is what touch support produces, so it travels as `hasTouch`.
|
|
58
|
+
*
|
|
59
|
+
* Measured in real headless Chromium: `hasTouch: true` makes `(pointer: coarse)` match and
|
|
60
|
+
* `(pointer: fine)` stop matching, with `maxTouchPoints` at 1. `isMobile` does the same and also
|
|
61
|
+
* changes viewport-meta handling, so it is deliberately left out.
|
|
62
|
+
*/
|
|
63
|
+
export declare function contextEmulationOf(emulate: LeaseEmulation): ContextEmulation;
|
|
31
64
|
/** The size a page reports for itself, or undefined when it cannot be read. */
|
|
32
65
|
export declare function readViewport(page: {
|
|
33
66
|
evaluate?(script: string): Promise<unknown>;
|