browsertap-mcp 0.4.12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. browsertap_mcp-0.4.12/.github/dependabot.yml +28 -0
  2. browsertap_mcp-0.4.12/.github/workflows/live.yml +115 -0
  3. browsertap_mcp-0.4.12/.github/workflows/release.yml +175 -0
  4. browsertap_mcp-0.4.12/.github/workflows/supply-chain.yml +132 -0
  5. browsertap_mcp-0.4.12/.github/workflows/test.yml +226 -0
  6. browsertap_mcp-0.4.12/.gitignore +120 -0
  7. browsertap_mcp-0.4.12/AGENTS.md +393 -0
  8. browsertap_mcp-0.4.12/CHANGELOG.md +937 -0
  9. browsertap_mcp-0.4.12/CONTRIBUTING.md +402 -0
  10. browsertap_mcp-0.4.12/CONTRIBUTING.zh-CN.md +295 -0
  11. browsertap_mcp-0.4.12/LICENSE +21 -0
  12. browsertap_mcp-0.4.12/MANIFEST.in +30 -0
  13. browsertap_mcp-0.4.12/PKG-INFO +652 -0
  14. browsertap_mcp-0.4.12/README.md +596 -0
  15. browsertap_mcp-0.4.12/README.zh-CN.md +629 -0
  16. browsertap_mcp-0.4.12/SECURITY.md +139 -0
  17. browsertap_mcp-0.4.12/THIRD-PARTY-NOTICES.md +64 -0
  18. browsertap_mcp-0.4.12/docs/TROUBLESHOOTING.md +283 -0
  19. browsertap_mcp-0.4.12/docs/TROUBLESHOOTING.zh-CN.md +231 -0
  20. browsertap_mcp-0.4.12/docs/USAGE.md +178 -0
  21. browsertap_mcp-0.4.12/docs/USAGE.zh-CN.md +156 -0
  22. browsertap_mcp-0.4.12/examples/claude-desktop-config.json +9 -0
  23. browsertap_mcp-0.4.12/examples/cursor-mcp.json +9 -0
  24. browsertap_mcp-0.4.12/examples/hermes-config.yaml +5 -0
  25. browsertap_mcp-0.4.12/pyproject.toml +175 -0
  26. browsertap_mcp-0.4.12/scripts/__init__.py +1 -0
  27. browsertap_mcp-0.4.12/scripts/acceptance_report.py +458 -0
  28. browsertap_mcp-0.4.12/scripts/check_distribution.py +317 -0
  29. browsertap_mcp-0.4.12/scripts/check_install.py +379 -0
  30. browsertap_mcp-0.4.12/scripts/check_release_tag.py +198 -0
  31. browsertap_mcp-0.4.12/scripts/check_tool_docs.py +399 -0
  32. browsertap_mcp-0.4.12/scripts/evidence_manifest.py +240 -0
  33. browsertap_mcp-0.4.12/scripts/finalize_change.py +189 -0
  34. browsertap_mcp-0.4.12/scripts/tool_coverage_report.py +414 -0
  35. browsertap_mcp-0.4.12/scripts/versioning.py +535 -0
  36. browsertap_mcp-0.4.12/server.json +45 -0
  37. browsertap_mcp-0.4.12/setup.cfg +4 -0
  38. browsertap_mcp-0.4.12/src/browsertap_mcp/__init__.py +9 -0
  39. browsertap_mcp-0.4.12/src/browsertap_mcp/_version.py +1 -0
  40. browsertap_mcp-0.4.12/src/browsertap_mcp/bridge.py +468 -0
  41. browsertap_mcp-0.4.12/src/browsertap_mcp/browser_bridge.py +1635 -0
  42. browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/_locales/en/messages.json +17 -0
  43. browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/_locales/zh_CN/messages.json +17 -0
  44. browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/background.js +4600 -0
  45. browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/content.js +81 -0
  46. browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/disable_dialogs.js +94 -0
  47. browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/manifest.json +53 -0
  48. browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/popup.html +27 -0
  49. browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/popup.js +82 -0
  50. browsertap_mcp-0.4.12/src/browsertap_mcp/cli.py +192 -0
  51. browsertap_mcp-0.4.12/src/browsertap_mcp/page_input.py +740 -0
  52. browsertap_mcp-0.4.12/src/browsertap_mcp/paths.py +109 -0
  53. browsertap_mcp-0.4.12/src/browsertap_mcp/physical_input.py +730 -0
  54. browsertap_mcp-0.4.12/src/browsertap_mcp/server.py +5971 -0
  55. browsertap_mcp-0.4.12/src/browsertap_mcp/simphtml.py +1277 -0
  56. browsertap_mcp-0.4.12/src/browsertap_mcp/skills/browsertap-bridge-recovery/SKILL.md +330 -0
  57. browsertap_mcp-0.4.12/src/browsertap_mcp/skills/browsertap-default/SKILL.md +253 -0
  58. browsertap_mcp-0.4.12/src/browsertap_mcp.egg-info/PKG-INFO +652 -0
  59. browsertap_mcp-0.4.12/src/browsertap_mcp.egg-info/SOURCES.txt +104 -0
  60. browsertap_mcp-0.4.12/src/browsertap_mcp.egg-info/dependency_links.txt +1 -0
  61. browsertap_mcp-0.4.12/src/browsertap_mcp.egg-info/entry_points.txt +2 -0
  62. browsertap_mcp-0.4.12/src/browsertap_mcp.egg-info/requires.txt +23 -0
  63. browsertap_mcp-0.4.12/src/browsertap_mcp.egg-info/top_level.txt +1 -0
  64. browsertap_mcp-0.4.12/tests/conftest.py +379 -0
  65. browsertap_mcp-0.4.12/tests/live_preflight.py +351 -0
  66. browsertap_mcp-0.4.12/tests/test_acceptance_report.py +347 -0
  67. browsertap_mcp-0.4.12/tests/test_all_tools_behavior.py +372 -0
  68. browsertap_mcp-0.4.12/tests/test_bridge_coverage.py +461 -0
  69. browsertap_mcp-0.4.12/tests/test_browser_bridge_coverage.py +1611 -0
  70. browsertap_mcp-0.4.12/tests/test_cli_coverage.py +284 -0
  71. browsertap_mcp-0.4.12/tests/test_dialog_policy.py +1647 -0
  72. browsertap_mcp-0.4.12/tests/test_distribution_contract.py +774 -0
  73. browsertap_mcp-0.4.12/tests/test_documentation_contract.py +601 -0
  74. browsertap_mcp-0.4.12/tests/test_downloads.py +670 -0
  75. browsertap_mcp-0.4.12/tests/test_evidence_manifest.py +307 -0
  76. browsertap_mcp-0.4.12/tests/test_finalize_change.py +62 -0
  77. browsertap_mcp-0.4.12/tests/test_hit_verification.py +333 -0
  78. browsertap_mcp-0.4.12/tests/test_image_dimensions.py +260 -0
  79. browsertap_mcp-0.4.12/tests/test_install_check.py +372 -0
  80. browsertap_mcp-0.4.12/tests/test_link_auth.py +390 -0
  81. browsertap_mcp-0.4.12/tests/test_link_refs.py +124 -0
  82. browsertap_mcp-0.4.12/tests/test_live_browser.py +1172 -0
  83. browsertap_mcp-0.4.12/tests/test_live_preflight.py +544 -0
  84. browsertap_mcp-0.4.12/tests/test_locator_contract.py +136 -0
  85. browsertap_mcp-0.4.12/tests/test_log_redaction.py +157 -0
  86. browsertap_mcp-0.4.12/tests/test_offline.py +2044 -0
  87. browsertap_mcp-0.4.12/tests/test_offscreen_note.py +56 -0
  88. browsertap_mcp-0.4.12/tests/test_page_input.py +202 -0
  89. browsertap_mcp-0.4.12/tests/test_paths.py +141 -0
  90. browsertap_mcp-0.4.12/tests/test_phase0_recovery.py +4818 -0
  91. browsertap_mcp-0.4.12/tests/test_phase1_tools.py +407 -0
  92. browsertap_mcp-0.4.12/tests/test_physical_input.py +1617 -0
  93. browsertap_mcp-0.4.12/tests/test_release_tag.py +177 -0
  94. browsertap_mcp-0.4.12/tests/test_response_classification.py +108 -0
  95. browsertap_mcp-0.4.12/tests/test_screen_bounds.py +531 -0
  96. browsertap_mcp-0.4.12/tests/test_screenshot_content.py +251 -0
  97. browsertap_mcp-0.4.12/tests/test_server_coverage.py +943 -0
  98. browsertap_mcp-0.4.12/tests/test_setup_diagnostics.py +369 -0
  99. browsertap_mcp-0.4.12/tests/test_simphtml_coverage.py +432 -0
  100. browsertap_mcp-0.4.12/tests/test_site_permissions.py +542 -0
  101. browsertap_mcp-0.4.12/tests/test_state_paths_report.py +173 -0
  102. browsertap_mcp-0.4.12/tests/test_stdio_contract.py +25 -0
  103. browsertap_mcp-0.4.12/tests/test_supply_chain.py +259 -0
  104. browsertap_mcp-0.4.12/tests/test_tool_registration.py +292 -0
  105. browsertap_mcp-0.4.12/tests/test_versioning.py +431 -0
  106. browsertap_mcp-0.4.12/tests/tool_coverage_manifest.py +355 -0
@@ -0,0 +1,28 @@
1
+ # Only the GitHub Actions ecosystem is watched here, and that is a decision
2
+ # rather than an omission.
3
+ #
4
+ # Every action in this repository is pinned to a commit SHA (see release.yml),
5
+ # which is the right thing for a workflow holding `id-token: write` -- but a SHA
6
+ # has no update channel, so without this file the pins simply age. Dependabot is
7
+ # that channel.
8
+ #
9
+ # The Python side is deliberately absent. `supply-chain.yml` already audits the
10
+ # real dependency closure with pip-audit and publishes an SBOM, and the upper
11
+ # bounds in pyproject.toml's `dev` extra are load-bearing: they exist so that a
12
+ # new ruff or pytest minor cannot move a gate number with no commit behind it.
13
+ # A bot raising those bounds would undo exactly what they are for. Runtime
14
+ # dependencies carry lower bounds on purpose too -- a user's resolver picks the
15
+ # version, not this repository.
16
+ version: 2
17
+ updates:
18
+ - package-ecosystem: github-actions
19
+ directory: "/"
20
+ schedule:
21
+ interval: monthly
22
+ open-pull-requests-limit: 3
23
+ commit-message:
24
+ prefix: chore
25
+ groups:
26
+ actions:
27
+ patterns:
28
+ - "*"
@@ -0,0 +1,115 @@
1
+ name: BTAP live Chrome gates
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ permissions:
7
+ contents: read
8
+
9
+ concurrency:
10
+ group: btap-live-browser
11
+ cancel-in-progress: false
12
+
13
+ env:
14
+ BTAP_PYTHON: ${{ vars.BTAP_LIVE_PYTHON || 'python' }}
15
+
16
+ jobs:
17
+ preflight:
18
+ # This job exists because the `live` job below cannot report its own missing
19
+ # prerequisite. A job whose `runs-on` labels match no online runner does not
20
+ # fail -- it queues, for up to 24 hours, and `timeout-minutes` governs
21
+ # execution time only, not queue time. So a "check the runner first" step
22
+ # placed inside `live` would never execute on the one path where it matters.
23
+ # Only a job that is guaranteed to start can say "there is no runner", which
24
+ # means a GitHub-hosted one.
25
+ #
26
+ # It gates on a repository variable rather than querying the runner API on
27
+ # purpose: listing self-hosted runners needs the Administration permission,
28
+ # which `permissions:` cannot grant to GITHUB_TOKEN, so an API check would
29
+ # need a long-lived PAT secret just to produce a better error message.
30
+ name: Check live prerequisites
31
+ if: github.repository == 'LinVireo/browsertap-mcp'
32
+ runs-on: ubuntu-latest
33
+ timeout-minutes: 5
34
+ steps:
35
+ - name: Require a registered self-hosted browser runner
36
+ if: vars.BTAP_LIVE_RUNNER != 'ready'
37
+ run: |
38
+ echo "::error title=No live runner registered::The live browser suite has no machine to run on; failing now instead of queueing."
39
+ cat <<'EOF'
40
+ The live suite drives a real Chrome window through the bridge and the
41
+ unpacked extension, so it only runs on a self-hosted Windows runner
42
+ that a maintainer owns. None is declared, and without one the `live`
43
+ job would sit in Queued for up to 24 hours and then expire -- which
44
+ reads as an infrastructure hiccup rather than as "this was never
45
+ configured".
46
+
47
+ To enable it:
48
+
49
+ 1. Register a self-hosted runner on a Windows machine with Chrome,
50
+ the bridge and the unpacked extension already working. Give it the
51
+ labels: self-hosted Windows X64 btap
52
+ 2. Optionally set the repository variable BTAP_LIVE_PYTHON to the
53
+ interpreter to use; it defaults to `python` on PATH.
54
+ 3. Set the repository variable BTAP_LIVE_RUNNER to: ready
55
+ (Settings -> Secrets and variables -> Actions -> Variables)
56
+
57
+ The `btap-live` environment referenced by the live job is created by
58
+ GitHub on first use; add required reviewers there if a run should need
59
+ approval before it touches that desktop.
60
+
61
+ Until then, run the live gates locally:
62
+
63
+ python -m pytest tests/ -q -m live
64
+
65
+ and leave the browser idle while they run. The suite checks that for
66
+ itself now: it samples the tab list before the first test and skips the
67
+ whole live layer if anything moved, then compares the inventory again
68
+ at the end. CONTRIBUTING.md describes both checks and the override.
69
+ EOF
70
+ exit 1
71
+ - name: Report prerequisites satisfied
72
+ run: |
73
+ echo "BTAP_LIVE_RUNNER=ready; handing off to the self-hosted browser runner."
74
+ echo "Interpreter: ${BTAP_PYTHON}"
75
+
76
+ live:
77
+ name: Live Chrome verification
78
+ needs: preflight
79
+ environment: btap-live
80
+ runs-on: [self-hosted, Windows, X64, btap]
81
+ timeout-minutes: 10
82
+ steps:
83
+ - name: Check out source
84
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
85
+ - name: Resolve configured Python
86
+ shell: powershell
87
+ run: |
88
+ $python = Get-Command $env:BTAP_PYTHON -ErrorAction Stop
89
+ "BTAP_PYTHON=$($python.Source)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
90
+ - name: Install editable package
91
+ shell: powershell
92
+ run: |
93
+ & $env:BTAP_PYTHON -m pip install -e ".[desktop,dev]"
94
+ - name: Verify bridge and extension diagnostics
95
+ shell: powershell
96
+ run: |
97
+ & $env:BTAP_PYTHON -c 'import json; from browsertap_mcp import server as S; s = S.get_setup_status(); print(json.dumps(s, ensure_ascii=False, indent=2, default=str)); raise SystemExit(2 if s.get("reload_extension_required") or s.get("restart_bridge_required") else 0)'
98
+ - name: Run live browser suite
99
+ shell: powershell
100
+ run: |
101
+ New-Item -ItemType Directory -Force artifacts | Out-Null
102
+ & $env:BTAP_PYTHON -m pytest tests -q -m live --junitxml=artifacts/live-junit.xml
103
+ - name: Recheck diagnostics after cleanup
104
+ if: always()
105
+ shell: powershell
106
+ run: |
107
+ New-Item -ItemType Directory -Force artifacts | Out-Null
108
+ & $env:BTAP_PYTHON -c "import json; from browsertap_mcp import server as S; print(json.dumps(S.get_setup_status(), ensure_ascii=False, indent=2, default=str))" | Out-File -Encoding utf8 artifacts/setup-status.json
109
+ - name: Upload live evidence
110
+ if: always()
111
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
112
+ with:
113
+ name: btap-live-evidence
114
+ path: artifacts/
115
+ if-no-files-found: ignore
@@ -0,0 +1,175 @@
1
+ name: BTAP publish to PyPI
2
+
3
+ # Nothing here runs on a push. Publishing is a deliberate act with an
4
+ # irreversible result -- a PyPI filename can never be reused, so a wrong upload
5
+ # burns that version number for good -- so the only triggers are a manual run
6
+ # and a published GitHub Release.
7
+ on:
8
+ workflow_dispatch:
9
+ inputs:
10
+ index:
11
+ description: Which index to upload to
12
+ type: choice
13
+ default: testpypi
14
+ options:
15
+ - testpypi
16
+ - pypi
17
+ release:
18
+ types: [published]
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ concurrency:
24
+ group: btap-publish
25
+ cancel-in-progress: false
26
+
27
+ jobs:
28
+ build:
29
+ name: Build and check distributions
30
+ runs-on: ubuntu-latest
31
+ if: github.repository == 'LinVireo/browsertap-mcp'
32
+ timeout-minutes: 15
33
+ steps:
34
+ - name: Check out source
35
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
36
+ with:
37
+ fetch-depth: 0
38
+ - name: Set up Python
39
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
40
+ with:
41
+ python-version: '3.13'
42
+ cache: pip
43
+ # The version being published comes from the source tree, not from the ref
44
+ # that started this run, so a tag left behind on an older commit would ship
45
+ # a tree nobody validated under that name. This is the cheapest step that
46
+ # can fail, so it runs before anything is installed or built.
47
+ # A TestPyPI rehearsal may legitimately run from a branch that is not tagged
48
+ # yet, so there a missing tag is a note -- but a tag that points somewhere
49
+ # else is still a failure.
50
+ - name: Check the release tag names this commit (rehearsal)
51
+ if: github.event_name == 'workflow_dispatch' && inputs.index == 'testpypi'
52
+ run: python -m scripts.check_release_tag --allow-missing-tag
53
+ - name: Check the release tag names this commit
54
+ if: github.event_name == 'release' || inputs.index == 'pypi'
55
+ run: python -m scripts.check_release_tag
56
+ - name: Install package and development dependencies
57
+ run: python -m pip install --upgrade pip && python -m pip install -e ".[dev,desktop]"
58
+ # A release must not ship a tree that fails its own gates. This is the
59
+ # offline suite only: the live-Chrome gates need a real browser and are a
60
+ # separate, self-hosted workflow.
61
+ - name: Run offline tests with coverage
62
+ run: >-
63
+ python -m pytest tests -q
64
+ --cov=browsertap_mcp
65
+ --cov-fail-under=85
66
+ - name: Check documentation and versions
67
+ run: python -m scripts.check_tool_docs --format markdown
68
+ - name: Build distributions
69
+ run: python -m build --wheel --sdist --outdir dist
70
+ # `check_distribution` reads what is inside the archives -- required files,
71
+ # no machine-local data, and the publishing metadata the index needs.
72
+ - name: Check distribution contents and metadata
73
+ run: python -m scripts.check_distribution dist
74
+ # And this is the other question a reader actually cares about: does a
75
+ # stranger's `pip install` leave them with something that runs. The wheel
76
+ # goes into a throwaway virtual environment with no repository on the path,
77
+ # and the console script, the packaged skills, and the extension files are
78
+ # exercised there rather than inferred from the archive listing.
79
+ - name: Check a clean install of the built wheel
80
+ run: python -m scripts.check_install dist
81
+ # `twine check` is the other half: it renders the long description the way
82
+ # the index will. Only a built archive can be checked this way, and only
83
+ # before the upload -- afterwards the version is spent.
84
+ - name: Check the rendered long description
85
+ run: python -m twine check --strict dist/*
86
+ # Supply-chain evidence for exactly what is about to be published: the
87
+ # dependency closure this wheel pulls onto a user's machine, audited against
88
+ # the advisory database, plus a CycloneDX SBOM of that same closure. Blocking
89
+ # on purpose, unlike the informational run in supply-chain.yml -- a
90
+ # known-vulnerable dependency is a reason not to publish, and this is the
91
+ # last point at which not publishing is still free.
92
+ - name: Audit and describe what the wheel installs
93
+ run: |
94
+ set -euo pipefail
95
+ python -m venv sbom-venv
96
+ ./sbom-venv/bin/python -m pip install --quiet --no-input dist/*.whl
97
+ ./sbom-venv/bin/python -m pip freeze --exclude-editable > runtime-requirements.txt
98
+ cat runtime-requirements.txt
99
+ # `pip freeze` lists the package under audit alongside its dependencies,
100
+ # and because it was installed from a local wheel it comes out as a PEP
101
+ # 610 direct reference -- `browsertap-mcp @ file:///.../*.whl#sha256=...`.
102
+ # `pip-audit --strict` then fails the whole run with "Dependency not found
103
+ # on PyPI and could not be audited", naming the exact version this
104
+ # workflow is about to upload, which by definition is not on the index
105
+ # yet. That made the first release of any name impossible to publish and
106
+ # went unnoticed because the same step does not block in
107
+ # supply-chain.yml. The subject of an audit is not one of its own
108
+ # dependencies, so it is dropped from the auditor's input -- after the
109
+ # full freeze has been printed above, so the wheel and its hash stay in
110
+ # the log as evidence. The SBOM below still describes the whole
111
+ # environment, package included, which is what an SBOM is for.
112
+ sed -i -E '/^browsertap[-_]mcp([[:space:]=@]|$)/d' runtime-requirements.txt
113
+ python -m pip install --quiet pip-audit==2.10.1 cyclonedx-bom==7.3.1
114
+ pip-audit --requirement runtime-requirements.txt --no-deps --strict --desc
115
+ mkdir -p sbom
116
+ python -m cyclonedx_py environment ./sbom-venv --spec-version 1.6 \
117
+ --output-format JSON --output-file sbom/browsertap-mcp.cdx.json
118
+ # A separate artifact, never dist/: the publish job uploads everything under
119
+ # dist/ to the index, and a stray .json there would either be rejected or
120
+ # published as part of the release.
121
+ - name: Upload the SBOM
122
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
123
+ with:
124
+ name: btap-sbom
125
+ path: sbom/
126
+ if-no-files-found: error
127
+ # The version the archives carry is what the index will hold forever, so it
128
+ # is printed here rather than inferred from the tag by a reader later.
129
+ - name: Report the version being published
130
+ run: python -c "import pathlib; print('\n'.join(p.name for p in sorted(pathlib.Path('dist').iterdir())))"
131
+ - name: Upload built distributions
132
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
133
+ with:
134
+ name: btap-dist
135
+ path: dist/
136
+ if-no-files-found: error
137
+
138
+ publish:
139
+ name: Upload to ${{ github.event_name == 'release' && 'pypi' || inputs.index }}
140
+ needs: build
141
+ runs-on: ubuntu-latest
142
+ # The environment is the approval gate: configure required reviewers on it so
143
+ # a human confirms before an upload that cannot be undone. It is also where
144
+ # the PyPI Trusted Publisher is scoped -- see CONTRIBUTING.md, "Publishing to
145
+ # PyPI", for the one-time setup this workflow assumes.
146
+ environment: ${{ github.event_name == 'release' && 'pypi' || inputs.index }}
147
+ permissions:
148
+ # Trusted Publishing exchanges this short-lived OIDC token for an upload
149
+ # token at request time. No API token is stored in the repository, so there
150
+ # is none to leak or rotate.
151
+ id-token: write
152
+ timeout-minutes: 15
153
+ steps:
154
+ - name: Download built distributions
155
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
156
+ with:
157
+ name: btap-dist
158
+ path: dist/
159
+ # The only action here not pinned to a commit. PyPA publishes security and
160
+ # protocol fixes for Trusted Publishing onto this branch and asks callers
161
+ # to track it, and a stale pin on the one step holding `id-token: write`
162
+ # fails closed in the worst place -- a release that cannot upload. The
163
+ # trade-off is deliberate and asserted in tests/test_supply_chain.py, so
164
+ # adding a second floating action is a test failure rather than a habit.
165
+ - name: Publish to TestPyPI
166
+ if: github.event_name == 'workflow_dispatch' && inputs.index == 'testpypi'
167
+ uses: pypa/gh-action-pypi-publish@release/v1
168
+ with:
169
+ repository-url: https://test.pypi.org/legacy/
170
+ print-hash: true
171
+ - name: Publish to PyPI
172
+ if: github.event_name == 'release' || inputs.index == 'pypi'
173
+ uses: pypa/gh-action-pypi-publish@release/v1
174
+ with:
175
+ print-hash: true
@@ -0,0 +1,132 @@
1
+ name: BTAP supply chain
2
+
3
+ # Two questions this repository could not answer before: did a credential ever
4
+ # reach a commit, and what exactly does `pip install browsertap-mcp` pull onto a
5
+ # stranger's machine. Both are answered here on every push and once a week --
6
+ # weekly because an advisory can appear without a single line of this repository
7
+ # changing.
8
+ on:
9
+ push:
10
+ pull_request:
11
+ schedule:
12
+ # Monday morning, off the hour so it does not queue behind everyone's cron.
13
+ - cron: '17 6 * * 1'
14
+ workflow_dispatch:
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ concurrency:
20
+ group: supply-chain-${{ github.workflow }}-${{ github.ref }}
21
+ cancel-in-progress: true
22
+
23
+ jobs:
24
+ secrets:
25
+ name: Secret scan
26
+ runs-on: ubuntu-latest
27
+ timeout-minutes: 10
28
+ steps:
29
+ - name: Check out source
30
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
31
+ with:
32
+ # The whole point is the history: a secret that was committed and then
33
+ # deleted is still published. A shallow clone would scan one commit and
34
+ # report a clean repository.
35
+ fetch-depth: 0
36
+ - name: Install the pinned gitleaks build
37
+ env:
38
+ GITLEAKS_VERSION: 8.30.1
39
+ # sha256 of gitleaks_8.30.1_linux_x64.tar.gz, read out of that release's
40
+ # own checksums file. Version and digest are bumped together and never
41
+ # separately: a scanner resolved at run time is a third party that can
42
+ # change what it does to this repository between two green runs.
43
+ GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
44
+ run: |
45
+ set -euo pipefail
46
+ base=https://github.com/gitleaks/gitleaks/releases/download
47
+ archive="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
48
+ curl --fail --silent --show-error --location --output gitleaks.tar.gz \
49
+ "${base}/v${GITLEAKS_VERSION}/${archive}"
50
+ echo "${GITLEAKS_SHA256} gitleaks.tar.gz" | sha256sum --check --strict -
51
+ tar -xzf gitleaks.tar.gz gitleaks
52
+ ./gitleaks version
53
+ # --redact is not cosmetic: a scan report is world-readable build output, so
54
+ # printing the secret it found would publish the very thing it detected.
55
+ - name: Scan the full history for secrets
56
+ run: >-
57
+ ./gitleaks git . --no-banner --redact --exit-code 1
58
+ --report-format json --report-path gitleaks-history.json
59
+ - name: Scan the working tree for secrets
60
+ run: >-
61
+ ./gitleaks dir . --no-banner --redact --exit-code 1
62
+ --report-format json --report-path gitleaks-worktree.json
63
+ - name: Upload scan reports
64
+ if: always()
65
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
66
+ with:
67
+ name: btap-secret-scan
68
+ path: |
69
+ gitleaks-history.json
70
+ gitleaks-worktree.json
71
+ if-no-files-found: ignore
72
+
73
+ dependencies:
74
+ name: Dependency audit and SBOM
75
+ runs-on: ubuntu-latest
76
+ timeout-minutes: 15
77
+ steps:
78
+ - name: Check out source
79
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
80
+ - name: Set up Python
81
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
82
+ with:
83
+ python-version: '3.13'
84
+ cache: pip
85
+ # The runtime closure, not this job's environment: development extras are
86
+ # not what a user installs, and auditing them would report risk nobody is
87
+ # exposed to while hiding nothing.
88
+ - name: Resolve the closure a plain install produces
89
+ run: |
90
+ set -euo pipefail
91
+ python -m venv runtime-venv
92
+ ./runtime-venv/bin/python -m pip install --quiet --upgrade pip
93
+ ./runtime-venv/bin/python -m pip install --quiet .
94
+ ./runtime-venv/bin/python -m pip freeze --exclude-editable > runtime-requirements.txt
95
+ cat runtime-requirements.txt
96
+ # The package itself is in that list, as a PEP 610 direct reference to
97
+ # the local checkout, and `pip-audit --strict` treats a requirement it
98
+ # cannot resolve on PyPI as a failure. Here the step is
99
+ # `continue-on-error`, so the job stayed green while the audit had in
100
+ # fact stopped -- the whole closure was going unaudited and the report
101
+ # said so to nobody. Same removal as release.yml, where it is fatal.
102
+ sed -i -E '/^browsertap[-_]mcp([[:space:]=@]|$)/d' runtime-requirements.txt
103
+ # Informational here, blocking in release.yml. An advisory published
104
+ # overnight would otherwise turn every branch red for a reason unrelated to
105
+ # the commit that is being pushed -- but it is a perfectly good reason not
106
+ # to publish a new version, which is where the same check is required.
107
+ - name: Audit the closure against the advisory database (informational)
108
+ continue-on-error: true
109
+ run: |
110
+ set -euo pipefail
111
+ python -m pip install --quiet pip-audit==2.10.1
112
+ pip-audit --requirement runtime-requirements.txt --no-deps --strict --desc \
113
+ --format markdown --output pip-audit.md
114
+ cat pip-audit.md
115
+ - name: Describe the closure as a CycloneDX SBOM
116
+ run: |
117
+ set -euo pipefail
118
+ python -m pip install --quiet cyclonedx-bom==7.3.1
119
+ mkdir -p sbom
120
+ python -m cyclonedx_py environment ./runtime-venv --spec-version 1.6 \
121
+ --output-format JSON --output-file sbom/browsertap-mcp.cdx.json
122
+ python -c "import json; print(len(json.load(open('sbom/browsertap-mcp.cdx.json')).get('components', [])), 'components described')"
123
+ - name: Upload the audit and the SBOM
124
+ if: always()
125
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
126
+ with:
127
+ name: btap-supply-chain
128
+ path: |
129
+ runtime-requirements.txt
130
+ pip-audit.md
131
+ sbom/
132
+ if-no-files-found: ignore
@@ -0,0 +1,226 @@
1
+ name: BTAP offline gates
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ # One in-flight run per ref. Re-pushing a branch cancels the previous run
11
+ # instead of racing it, so the newest commit owns the gate result.
12
+ concurrency:
13
+ group: offline-${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ offline:
18
+ name: Python ${{ matrix.python-version }}
19
+ runs-on: ubuntu-latest
20
+ timeout-minutes: 15
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ python-version: ['3.10', '3.11', '3.12', '3.13']
25
+ steps:
26
+ - name: Check out source
27
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
28
+ with:
29
+ fetch-depth: 0
30
+ - name: Set up Python
31
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
32
+ with:
33
+ python-version: ${{ matrix.python-version }}
34
+ cache: pip
35
+ - name: Install package and development dependencies
36
+ run: python -m pip install --upgrade pip && python -m pip install -e ".[dev,desktop]"
37
+ - name: Check Python lint
38
+ if: matrix.python-version == '3.13'
39
+ run: python -m ruff check src tests scripts
40
+ - name: Compile Python sources
41
+ run: python -m compileall -q src
42
+ - name: Prepare evidence directory
43
+ run: mkdir -p artifacts
44
+ - name: Run offline tests with coverage
45
+ run: >-
46
+ python -m pytest tests -q
47
+ --cov=browsertap_mcp
48
+ --cov-fail-under=85
49
+ --cov-report=term-missing
50
+ --cov-report=xml
51
+ --cov-report=json:artifacts/coverage.json
52
+ --junitxml=artifacts/offline-junit.xml
53
+ - name: Check tool behavior coverage
54
+ run: >-
55
+ python -m scripts.tool_coverage_report --format markdown
56
+ --output artifacts/tool-coverage-offline.json
57
+ - name: Check documentation and versions
58
+ run: python -m scripts.check_tool_docs --format markdown
59
+ - name: Check SemVer bump on release branches
60
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release/') && github.event.before != '0000000000000000000000000000000000000000'
61
+ run: python -m scripts.versioning check-bump --base "${{ github.event.before }}"
62
+ - name: Build distributions
63
+ run: python -m build --wheel --sdist --outdir artifacts/dist
64
+ - name: Check distribution contents
65
+ run: python -m scripts.check_distribution artifacts/dist
66
+ # The other half of the same question: not what is inside the archive,
67
+ # but what a stranger has after `pip install`. Runs on every version in
68
+ # the matrix, because package data and console scripts are exactly what
69
+ # breaks on one interpreter and not another.
70
+ - name: Check a clean install of the built wheel
71
+ run: python -m scripts.check_install artifacts/dist
72
+ - name: Check installed dependency consistency
73
+ run: python -m pip check
74
+ # The evidence chain is only meaningful if something other than the release
75
+ # operator's own machine exercises it. CI seals the artifacts it just
76
+ # generated and immediately re-validates the seal, so a broken manifest
77
+ # writer or validator fails here rather than at release time. Committed
78
+ # artifacts are not used: `artifacts/` is gitignored, so this always runs
79
+ # against a clean checkout plus this run's own output.
80
+ - name: Seal and verify this run's evidence
81
+ if: matrix.python-version == '3.13'
82
+ run: >-
83
+ python -m scripts.evidence_manifest &&
84
+ python -m scripts.evidence_manifest --check
85
+ # Scored for visibility only. The live-Chrome gates cannot pass on a
86
+ # headless runner, so a non-zero exit is expected and must not fail the job.
87
+ - name: Score offline acceptance gates (informational)
88
+ if: matrix.python-version == '3.13'
89
+ continue-on-error: true
90
+ run: python -m scripts.acceptance_report --output artifacts/acceptance-report.md
91
+ - name: Upload gate artifacts
92
+ if: always()
93
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
94
+ with:
95
+ name: btap-offline-${{ matrix.python-version }}
96
+ path: artifacts/
97
+ if-no-files-found: ignore
98
+
99
+ # Every OS-specific failure this product has is a Windows one: wsgiref resets a
100
+ # rejected request that still has an unread body (so a 401 reaches the caller as
101
+ # a dropped connection), the host lock is `SO_EXCLUSIVEADDRUSE`, `pythonw.exe`
102
+ # is a forwarding stub whose child holds the port, a minimised window accepts
103
+ # focus calls that put nothing on screen, and a log file cannot be renamed while
104
+ # the daemon holds it open. The job above runs on Linux only, so none of that was
105
+ # ever exercised by CI -- and `bridge.py`, where most of it lives, is the least
106
+ # covered module in the package. Oldest and newest interpreter only: this is
107
+ # about the platform, and the version matrix is already covered above.
108
+ windows-offline:
109
+ name: Windows offline (Python ${{ matrix.python-version }})
110
+ runs-on: windows-latest
111
+ timeout-minutes: 20
112
+ defaults:
113
+ run:
114
+ # Deliberately the same shell as the Linux job. `mkdir -p` and the folded
115
+ # multi-line commands below are POSIX, and a step whose text differs
116
+ # between the two jobs stops being a comparison of the platform.
117
+ shell: bash
118
+ strategy:
119
+ fail-fast: false
120
+ matrix:
121
+ python-version: ['3.10', '3.13']
122
+ steps:
123
+ - name: Check out source
124
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
125
+ - name: Set up Python
126
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
127
+ with:
128
+ python-version: ${{ matrix.python-version }}
129
+ cache: pip
130
+ - name: Install package and development dependencies
131
+ run: python -m pip install --upgrade pip && python -m pip install -e ".[dev,desktop]"
132
+ - name: Compile Python sources
133
+ run: python -m compileall -q src
134
+ - name: Prepare evidence directory
135
+ run: mkdir -p artifacts
136
+ # No coverage gate here: the threshold is enforced once, on Linux, and a
137
+ # second instrumented run would only add wall clock. The junit file is kept
138
+ # because which test failed on Windows is the whole point of this job.
139
+ - name: Run offline tests
140
+ run: >-
141
+ python -m pytest tests -q
142
+ --junitxml=artifacts/windows-offline-junit.xml
143
+ # What a stranger has after `pip install` on the platform most of this
144
+ # product's users run, where the console script is a generated .exe rather
145
+ # than a shebang. Newest interpreter only, to keep the job short.
146
+ - name: Build distributions
147
+ if: matrix.python-version == '3.13'
148
+ run: python -m build --wheel --sdist --outdir artifacts/dist
149
+ - name: Check distribution contents
150
+ if: matrix.python-version == '3.13'
151
+ run: python -m scripts.check_distribution artifacts/dist
152
+ - name: Check a clean install of the built wheel
153
+ if: matrix.python-version == '3.13'
154
+ run: python -m scripts.check_install artifacts/dist
155
+ - name: Check installed dependency consistency
156
+ run: python -m pip check
157
+ - name: Upload gate artifacts
158
+ if: always()
159
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
160
+ with:
161
+ name: btap-windows-offline-${{ matrix.python-version }}
162
+ path: artifacts/
163
+ if-no-files-found: ignore
164
+
165
+ # The third platform, and the only one with no coverage at all until now. Two
166
+ # code paths in this package exist solely for it -- `_macos_pointer_position`
167
+ # reads the cursor through Quartz, and `_darwin_process_identity` is how the
168
+ # bridge decides whether the PID in its lock file is still the daemon it wrote
169
+ # -- and neither had ever been executed by anything but a reviewer's eyes. The
170
+ # quiet-input gate also degrades here rather than on Windows: macOS answers the
171
+ # pointer read only once someone grants the accessibility permission, which a
172
+ # hosted runner never does, so this job is where `input_quiet.enforced: false`
173
+ # is actually produced instead of merely documented. Oldest and newest
174
+ # interpreter only, same as Windows: this is about the platform.
175
+ macos-offline:
176
+ name: macOS offline (Python ${{ matrix.python-version }})
177
+ runs-on: macos-latest
178
+ timeout-minutes: 20
179
+ defaults:
180
+ run:
181
+ # Same shell as the other two jobs on purpose; a step whose text differs
182
+ # between them stops being a comparison of the platform.
183
+ shell: bash
184
+ strategy:
185
+ fail-fast: false
186
+ matrix:
187
+ python-version: ['3.10', '3.13']
188
+ steps:
189
+ - name: Check out source
190
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
191
+ - name: Set up Python
192
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
193
+ with:
194
+ python-version: ${{ matrix.python-version }}
195
+ cache: pip
196
+ - name: Install package and development dependencies
197
+ run: python -m pip install --upgrade pip && python -m pip install -e ".[dev,desktop]"
198
+ - name: Compile Python sources
199
+ run: python -m compileall -q src
200
+ - name: Prepare evidence directory
201
+ run: mkdir -p artifacts
202
+ # No coverage gate here either: the threshold is enforced once, on Linux.
203
+ - name: Run offline tests
204
+ run: >-
205
+ python -m pytest tests -q
206
+ --junitxml=artifacts/macos-offline-junit.xml
207
+ # Console scripts and package data on the third platform. Newest
208
+ # interpreter only, to keep the job short.
209
+ - name: Build distributions
210
+ if: matrix.python-version == '3.13'
211
+ run: python -m build --wheel --sdist --outdir artifacts/dist
212
+ - name: Check distribution contents
213
+ if: matrix.python-version == '3.13'
214
+ run: python -m scripts.check_distribution artifacts/dist
215
+ - name: Check a clean install of the built wheel
216
+ if: matrix.python-version == '3.13'
217
+ run: python -m scripts.check_install artifacts/dist
218
+ - name: Check installed dependency consistency
219
+ run: python -m pip check
220
+ - name: Upload gate artifacts
221
+ if: always()
222
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
223
+ with:
224
+ name: btap-macos-offline-${{ matrix.python-version }}
225
+ path: artifacts/
226
+ if-no-files-found: ignore