-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpyproject.toml
More file actions
413 lines (396 loc) · 30.9 KB
/
Copy pathpyproject.toml
File metadata and controls
413 lines (396 loc) · 30.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# Single source of truth for the version: hatchling reads __version__ from the package module, so the
# literal lives in exactly one place (no pyproject-vs-__init__ drift). Bump it in messagefoundry/__init__.py.
[tool.hatch.version]
path = "messagefoundry/__init__.py"
# sdist file selection — an ALLOWLIST, not hatchling's default whole-repo VCS sweep. WITHOUT this,
# `python -m build` packs every git-tracked file (docs/, tests/, scripts/, CLAUDE.md, .claude/ …) into the
# sdist, and release.yml uploads that sdist to PUBLIC PyPI — which shipped the private security-posture
# docs (docs/security/*, docs/reviews/*, Secure_Development_Standards.md — the publish-denylist set) on
# every release 0.1.0..0.2.15. The leak gate (scan_forbidden.py, in pre-commit and CI) governs what is
# COMMITTED, NOT what an sdist packages, so the sdist is pinned here to the package + its metadata. That
# separation is the whole point: those docs are git-ignored now, but an allowlist is what keeps a future
# tracked-but-private file out of a release tarball. (The wheel was
# already package-only.) pyproject.toml + PKG-INFO are always added by hatchling; LICENSE/NOTICE also ship
# via [project].license-files. release.yml has a belt-and-suspenders "sdist is package-only" gate.
[tool.hatch.build.targets.sdist]
only-include = ["messagefoundry", "README.md", "CHANGELOG.md", "LICENSE", "NOTICE"]
[project]
name = "messagefoundry"
dynamic = ["version"] # single-sourced from messagefoundry/__init__.py (see [tool.hatch.version])
description = "Open-source healthcare integration engine — route, transform, and validate messages across many formats and connection types"
readme = "README.md"
requires-python = ">=3.14"
license = "AGPL-3.0-or-later"
license-files = ["LICENSE", "NOTICE"] # ship the license + copyright/attribution NOTICE in sdist+wheel (PEP 639)
classifiers = [
"Development Status :: 4 - Beta", # Early Access
"Intended Audience :: Healthcare Industry",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"Topic :: Communications",
"Topic :: Software Development :: Libraries :: Application Frameworks",
"Topic :: System :: Networking",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.14",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Environment :: Console",
"Typing :: Typed",
]
dependencies = [
"hl7apy>=1.3", # version-aware validation + profiles
"hl7>=0.4.5", # python-hl7: fast, tolerant parsing for routing
"pydantic>=2.6", # channel config models / validation
# CONSTRAINT PIN (transitive, via pydantic/fhir-core): annotated-types 0.8.0 dropped `SLOTS`,
# which fhir-core/fhir.resources import — every FHIR test ImportErrors on it. The cap bounds the
# RESOLVER, the only place it CAN be bounded: `uv lock` would otherwise choose 0.8.0 and every
# downstream artifact inherits it (uv.lock -> constraints.lock -> ci.yml's `--constraint`), which
# is exactly how PR #66 went red. It also binds every resolve CI does not constrain — an end user's
# `pip install messagefoundry[fhir]`, the local dev venv behind pre-commit, the dispatch-only
# workflow legs. Drop it once fhir-core ships a 0.8-compatible release — NOT once CI installs from
# the hashed lock; the lock carries whatever the resolver already chose.
"annotated-types<0.8",
"aiosqlite>=0.20", # async SQLite for the message store/queue
"fastapi>=0.137.1", # localhost engine API
"starlette>=1.3.1", # explicit floor — fastapi's own pin allows an older Starlette; >=1.3.1 fixes the Host/path + form-DoS CVEs (CVE-2026-48710 @1.0.1, CVE-2026-54282 @1.3.0, CVE-2026-54283 @1.3.1)
"uvicorn[standard]>=0.29",
"argon2-cffi>=23.1", # argon2id password hashing (OWASP-recommended)
"cryptography>=48.0.1", # AES-256-GCM PHI-at-rest encryption for the store (>=48.0.1: GHSA-537c-gmf6-5ccf)
"ldap3>=2.9", # Active Directory / LDAP authentication (pure-Python)
"pyspnego>=0.10", # SPNEGO/Kerberos negotiate for Windows SSO
"tomlkit>=0.12", # comment/format-preserving TOML writer for connections.toml (ADR 0007)
"tzdata>=2024.1", # IANA tz database for stdlib zoneinfo (Windows has no system tzdata)
"prometheus-client>=0.20", # Prometheus /metrics exposition (BACKLOG #21)
"defusedxml>=0.7.1", # hardened XML parse for RawMessage.xml() — XXE/DTD/entity-expansion safe (ADR 0004, BACKLOG #31)
"psutil>=6.0", # host CPU/mem gauges on the /metrics surface (BACKLOG #74)
"httpx>=0.27", # the shared apiclient HTTP client (messagefoundry.apiclient) — used by the bundled Windows tray's tokenless /health + /ui probes (messagefoundry.tray, ADR 0113, gui-scripts messagefoundry-tray) and the harness monitor; also the ASGI test client. Base (not an extra) so the tray works on every `pip install messagefoundry`.
"truststore>=0.10", # verify an https engine URL against the OS trust store (enterprise/AD-CS roots) — apiclient lazily imports it; base so the tray/monitor need no per-PC CA wrangling
]
# Shown on the PyPI sidebar; point at the PUBLIC repo/site (the wshallwshall repo is private).
[project.urls]
Homepage = "https://messagefoundry.org/"
Documentation = "https://messagefoundry.org/"
Repository = "https://github.com/MEFORORG/MessageFoundry"
Issues = "https://github.com/MEFORORG/MessageFoundry/issues"
[project.optional-dependencies]
# The standalone PySide6 test HARNESS (harness/ — synthetic send/receive/load/failover GUI). The
# desktop admin console was retired in favour of the web console (BACKLOG #103, ADR 0032 retired); the
# harness kept the reusable Qt view widgets (rehomed to harness/_console_widgets.py + _login.py) and is
# now the sole consumer of PySide6. keyring was dropped with the console (OS-token cache was
# launcher-only). The harness monitor's HTTP client (httpx/truststore via messagefoundry.apiclient) is
# now a BASE dependency, so it's no longer declared here.
harness = [
"PySide6>=6.6", # the harness GUI (LGPL — OSS-distributable)
]
# NOTE: there is no [tray] extra. The Windows notification-area tray service-manager (ADR 0113) ships
# INSIDE the wheel as `messagefoundry.tray` (gui-scripts `messagefoundry-tray`, `pythonw -m
# messagefoundry.tray`); its only third-party dep is httpx/truststore via the shared apiclient, both
# BASE deps above — so the tray works on every `pip install messagefoundry`, no extra required.
# SQL Server store backend — a production server-DB backend (full staged pipeline + ADR-0013
# response capture; supports_ingest_stage / supports_response_capture both True). Also needs the
# Microsoft ODBC Driver 18 for SQL Server installed at the OS level (not pip-installable).
# Lazy-imported, so SQLite-only installs skip it.
# aioodbc pulls pyodbc transitively; the lock resolves pyodbc 5.3.0 — the newest release and the
# first to support Python 3.14 (nothing older ships py3.14 wheels). 5.3.0 has an UPSTREAM py3.14
# parameter-binding segfault against SQL Server 2025 (mkleehammer/pyodbc#1459, still open), which the
# CI throughput-invariant step works around with scripts/ci/retry-native-crash.sh. When #1459 ships a
# fix, raise the floor to that pyodbc release here and drop that CI retry wrapper.
sqlserver = ["aioodbc>=0.5"]
# PostgreSQL store backend (production server-DB with single-node parity). No OS-level dependency
# (ships compiled C/Cython wheels). Lazy-imported, so SQLite-only installs skip it.
postgres = ["asyncpg>=0.29"]
# SFTP (SSH) transport for the REMOTEFILE connector. FTP/FTPS use the stdlib (ftplib), no dep.
# Lazy-imported, so installs that never use SFTP skip paramiko.
# Floor >=5.0: 5.0 removed SHA-1 entirely, excluding CVE-2026-44405 (rsakey.py SHA-1 permitted); >=3.4
# already excluded Terrapin (CVE-2023-48795). The lock resolves 5.0.0 — the floor makes the spec match.
sftp = ["paramiko>=5.0"]
# Optional OpenTelemetry metrics export (BACKLOG #21). Off by default; the /metrics Prometheus path
# needs none of this. otel imports are function-local, so SQLite-only installs skip these entirely.
otel = ["opentelemetry-sdk>=1.20", "opentelemetry-exporter-otlp>=1.20"]
# FHIR (R5/R4B/STU3) typed-model + FHIRPath codec for parsing/fhir/ (ADR 0022, BACKLOG #20). Lazy-imported,
# so SQLite-only installs skip it. fhir.resources>=7.1.0 is the pydantic-v2 floor (it dropped plain-R4 in
# 7.0.0 and pydantic-v1 in 7.1.0); it drags fhir-core (BSD-3, the pydantic-v2 base model) + pydantic-core
# (compiled). fhirpathpy is pure-Python (antlr4 runtime + python-dateutil). FHIR-XML rides fhir.resources'
# optional lxml extra and is deferred (JSON-only MVP) — never bare-parsed (ADR 0022 §6, Options #5).
# annotated-types is CAPPED <0.8: fhir-core (dragged in by fhir.resources) imports the `SLOTS`
# constant that annotated-types 0.8.0 REMOVED, so any fhir.resources parse dies with
# "ImportError: cannot import name 'SLOTS' from 'annotated_types'". Declared BOTH here and in
# [project.dependencies]: this copy documents the consumer, the base copy is what bounds a plain
# `pip install messagefoundry`. One Dependabot ignore entry covers both (it matches by package name,
# not declaration site) — lift the ignore and both caps together.
# Drop the bound once fhir-core ships a 0.8-compatible release.
fhir = ["fhir.resources>=7.1.0", "fhirpathpy>=2.2.0", "annotated-types<0.8"]
# DICOM (DIMSE C-STORE SCP/SCU) connectors + codec for parsing/dicom/ (ADR 0025, BACKLOG #24). Lazy-imported,
# so SQLite-only installs skip it. pynetdicom (the DIMSE upper-layer) drags pydicom (the DICOM dataset/SR codec);
# both pure-Python — HEADERS/SR ONLY, so NO numpy (numpy is pydicom's optional pixel-data dep, never used here).
# Floors per dep-vet (2026-06-20): pydicom>=3.0.2 excludes CVE-2026-32711 (2.4.0–2.4.4 FileSet/DICOMDIR path
# traversal); pynetdicom>=3.0.4 requires pydicom>=3,<4 (the 3.x lines pair cleanly). dicomweb-client (DICOMweb
# STOW-RS, Phase-2) is a SEPARATE extra — it drags numpy+pillow+requests, so it is added in the Phase-2 PR, never
# folded into [dicom].
dicom = ["pynetdicom>=3.0.4,<4", "pydicom>=3.0.2,<4"]
# Opt-in STRICT X12 validation for parsing/x12/validate.py (ADR 0012, BACKLOG #32). Lazy-imported, so
# the tolerant X12Peek/X12Message hot path (the default) never needs it. pyx12 is the reference
# implementation-guide validator (e.g. 005010X222A1 for 837P) and also emits the 997/999 ack as a
# by-product. Its SOLE runtime dependency is defusedxml (already in tree, used to parse pyx12's bundled
# TRUSTED map XML — not attacker input). >=4.0.0 is the first py3.12+/modern-typing release.
x12 = ["pyx12>=4.0.0"]
# Optional XML/SOAP message accessor + schema validation + signature for parsing/xml/ (BACKLOG #31).
# Lazy-imported, so installs that never touch XML skip it. lxml is the hardened XPath/serialize engine
# (parser locked down directly — defusedxml does NOT cover lxml and defusedxml.lxml is deprecated);
# xmlschema validates against XSD with remote schemaLocation fetch disabled; signxml does XML-DSig
# (it drags cryptography — registered in scripts/security/crypto_inventory_check.py). Floors are the
# current major lines (lxml 6.x dropped py<3.8 + bundled modern libxml2; xmlschema 4.x; signxml 4.x).
xml = ["lxml>=6.0.0", "xmlschema>=4.0.0", "signxml>=4.0.0"]
# WebAuthn/FIDO2 browser second factor for the ops console (ADR 0068, WP-14b; BACKLOG #11). PyPI name
# is EXACTLY "webauthn" (duo-labs/py_webauthn, BSD-3-Clause) — "py_webauthn"/"py-webauthn" pip-normalize
# to AS207960's UNRELATED package; never "fix" this name to match the GitHub repo. Lazy-imported
# (auth/webauthn.py), so installs without browser passkeys skip it. Floor >=3.0.0: its cbor2>=6.1.2
# floor guarantees the fix for the HIGH cbor2 DoS advisory that explicitly cites WebAuthn flows
# (CVE-2026-26209, fixed in cbor2 5.9.0); <4 because the 2.x/3.x cbor2 ranges are disjoint and a bare
# >= spec silently jumps majors on re-lock. Deliberate fallback line if 3.x regresses: 2.8.0
# (cbor2>=5.6.5,<6 — a re-lock, never a bare >=2.8 spec). Net-new transitives (dep-vet 2026-07-03):
# pyOpenSSL>=26.3.0 (maintenance-mode upstream; hard-caps cryptography<51 — kept OUT of core so
# repo-wide cryptography upgrades stay uncoupled), cbor2>=6.1.2 (parses untrusted browser CBOR — its
# advisory stream is WebAuthn-relevant supply chain; never let this floor drift down), and
# pyasn1-modules>=0.4.2. pyasn1 rides in via ldap3 (0.6.3 was the CVE-2026-30922 decoder-DoS fix); the
# lock moved to pyasn1 0.6.4 for CVE-2026-59885/59886 (BER OID quadratic-decode + univ.Real float DoS,
# both fixed in 0.6.4) — treat 0.6.4 as the floor, never let a re-lock regress it. Reachable here only
# via WebAuthn attestation parsing (optional [webauthn] extra, auth + password re-proof gated); ldap3
# and pyspnego do NOT route untrusted DER through pyasn1's vulnerable decoders.
# OSV sweep 2026-07-03: all five pins CLEAR (webauthn/pyopenssl/pyasn1/pyasn1-modules/cbor2).
webauthn = ["webauthn>=3.0.0,<4"]
# HashiCorp Vault KeyProvider (ADR 0019 §3, BACKLOG #196): envelope-decrypt the store DEK via Vault
# Transit. `hvac` is the OFFICIAL HashiCorp Vault Python client (Apache-2.0), named in ADR 0019 §3's
# provider table. Lazy-imported (store/keyprovider_vault.py), so installs that never select
# `[store].key_provider=vault` skip it and the base install still pulls ZERO Vault SDK. Bounded
# >=2.3.0,<3: the current 2.x line (Python 3.8+, requests-based). The cap is the half that was missing —
# the prose already said "a bare `>=` would silently jump a major on re-lock", which is exactly what
# `hvac>=2.3.0` was, so the intent was documented and unenforced. hvac fronts the store DEK (ADR 0019
# §3), and CI never installs the [vault] extra, so a major arriving through a re-lock would reach a
# release without a single test exercising it. Deliberately NOT mirrored by a Dependabot ignore entry:
# auto-merge already routes majors to manual review, and an ignore would suppress hvac's security track
# for no gain. Net-new transitives (dep-vet 2026-07-10): requests + urllib3
# (both ubiquitous, mature; the lock resolves urllib3>=2.7.0 — CVE-2025-50181/50182 SSRF-redirect fixes)
# plus charset-normalizer/idna/certifi. hvac ships NO type stubs — mypy-strict containment lives inside
# store/keyprovider_vault.py (a targeted typed local), never a repo-wide ignore.
vault = ["hvac>=2.3.0,<3"]
# The browser ops console ([api].serve_ui, ADR 0065) is a separately-versioned second wheel
# (messagefoundry-webconsole, in packaging/messagefoundry-webconsole/) mounted same-origin in-process via
# mount_ui. It is deliberately NOT declared as a [webconsole] extra yet: the wheel isn't published to an
# index, so a `messagefoundry-webconsole` dep would break `uv lock`. Until the release phase publishes it
# (adding the extra + a PEP 508 range), install it editable — CI uses `-e packaging/messagefoundry-webconsole`.
# serve_ui=true without the package installed fails LOUD at startup (__main__ find_spec guard).
dev = [
"pytest>=9.0.3", # >=9.0.3 fixes CVE-2025-71176 (insecure /tmp dir perms, CWE-379; dev/UNIX-only)
"pytest-asyncio>=0.26", # >=0.26 adds asyncio_default_test_loop_scope (BACKLOG #17 shared-loop fix)
"pytest-timeout>=2.3", # per-test watchdog: a hung test dumps stacks + fails fast in 60s, never wedges the leg
"pytest-rerunfailures>=16.0", # in-run auto-retry for the known harness-monitor timing flake: a single flake occurrence self-heals instead of reding the whole matrix + blocking auto-merge
# The engine suite runs across processes in CI (`-n` in ci.yml's `Tests (pytest)` step). Serial,
# it was 91 percent of the whole CI critical path. Two things had to be fixed before it worked and
# both are pinned by tests, so this is not a flag that can be dropped back in casually:
# collection-time timestamps must not reach a test id (tests/test_dependabot_automerge_guardrails.py)
# and the per-process port slot must not be inherited by workers (tests/conftest.py).
"pytest-xdist>=3.6",
# Pinned BELOW 0.16: ruff 0.16.0 turned on stricter defaults (RUF022/RUF100/BLE001) that flag
# hundreds of findings in existing code — at least ~870 on PR #66's ubuntu leg (whole-repo scope,
# 2026-08-01). An unbounded ">=" lets `uv lock` adopt that new baseline, and it flows out through
# uv.lock -> constraints.lock -> ci.yml's `--constraint`, plus every unconstrained resolve. Lift the
# cap in a deliberate PR that also clears the new findings.
"ruff>=0.4,<0.16",
"mypy>=1.10",
# httpx (the ASGI test client for the API + harness/load polling) is now a BASE dependency, so it
# is no longer declared here.
]
[project.scripts]
messagefoundry = "messagefoundry.__main__:main"
# The Windows notification-area tray service-manager (ADR 0113), shipped in the wheel as
# `messagefoundry.tray`. A GUI script (→ a pythonw.exe launcher on Windows, no console window — right
# for a background tray) rather than a console script. Windows-only at runtime; `main()` prints a
# friendly message and exits 1 elsewhere. Equivalent to `pythonw -m messagefoundry.tray`.
[project.gui-scripts]
messagefoundry-tray = "messagefoundry.tray.__main__:main"
# The offline common-password screening corpus (messagefoundry/auth/data/, loaded via
# importlib.resources) ships in the wheel automatically: hatchling packages every file under the
# messagefoundry/ package, data included. (A previous force-include of that dir double-added the files
# and broke `build --wheel`.) The guard is tests/test_auth_core.py, whose #1134 corpus gates load the
# list via importlib.resources, so a build-config change that dropped it fails the suite -- not a
# force-include. (This named tests/test_password_*.py until 2026-09-03; no such file has ever
# existed, so the stated guard pointed at nothing while the real one sat one name away.)
# --- CI TOOLCHAIN — PEP 735 dependency groups (ADR 0034 §3) ----------------------------------------
#
# WHY THEY EXIST: a version pin does NOT satisfy Scorecard's PinnedDependenciesID. Proven by this
# repo's own alert data — `bandit==1.9.4` (alert #74) and `zizmor==1.5.2` (alert 96) are EXACTLY pinned
# and still flagged, while the two `--require-hashes` installs are flagged in neither the open nor the
# dismissed set. Declaring the tools here routes them through `uv.lock`, out to `ci/locks/*.lock` via
# `uv export` WITH HASHES, and into CI as `pip install --require-hashes -r ci/locks/<group>.lock`. The
# same machinery (DEP-1 diff gate + the Dependabot resync) that keeps the other four exports fresh
# keeps these fresh, which is what stops a hash-pinned toolchain rotting into a pinned-but-unpatched
# one — the failure ADR 0034 §3 names as "worse posture than floating".
#
# NOT EXTRAS, deliberately: an extra is published wheel metadata, so `[ci-scanners]` would become a
# real install target for every downstream consumer of the wheel. A dependency group never ships.
#
# NOT IN `[tool.uv] default-groups`, deliberately: a default group lands in all four committed DEP-1
# artifacts — i.e. in the release SBOM, the container image locks, and in what `pip-audit` audits AS
# RUNTIME. Measured with these groups non-default: all four re-export byte-identically (DIFFS=0).
#
# THE SPLIT IS THE MERGE PATH. `ci-scanners` is what the BLOCKING security gates install for
# themselves (security.yml's pip-audit + bandit jobs, zizmor.yml); `ci-quality` is ADVISORY
# measurement (quality-advisory.yml's coverage + mutation jobs). Keeping them apart keeps `mutmut` — a
# mutation engine that rewrites and executes source — out of every required gate's install closure.
#
# NOT HERE: `semgrep`. It hard-conflicts with the `[otel]` extra (semgrep 1.172.0 requires
# `opentelemetry-sdk>=1.37,<1.38` while `[otel]` resolves 1.44), so a plain group silently DOWNGRADES
# the shipped otel runtime in all four DEP-1 artifacts — measured, bisected to semgrep alone. The only
# fix is `[tool.uv] conflicts = [[{ extra = "otel" }, { group = "..." }]]`, which declares a PRODUCT
# extra and a CI scanner permanently mutually exclusive. Excluded by decision, recorded as a reasoned
# residual in ADR 0034 §3 with the exact recipe so a future owner can flip it in one commit.
#
# Each pin's rationale lives HERE rather than in the workflow: the workflow comments would otherwise
# push every dismissed Scorecard alert anchored below them onto a new line number (ADR 0034's
# convergence rule).
[dependency-groups]
# The scanners the BLOCKING security gates install for themselves. Exact `==` throughout — for each of
# these the VERSION IS THE CONTRACT of a gate that can red a PR:
# bandit an unpinned 1.9.x upgrade silently changed `# nosec` parsing (1.9.x wants space-separated
# test IDs) and broke a green branch; this is the findings baseline of a blocking gate.
# pip-audit `==` makes the audit reproducible — an unpinned auditor can change its advisory-database
# handling between two runs of the same commit.
# zizmor zizmor.yml's gate asserts a CLEAN baseline, so a newly-added rule reds a green PR for a
# reason unrelated to its diff. Verify the tag at
# https://github.com/zizmorcore/zizmor/releases if an install 404s.
# NB: this group also hash-pins `pip` itself (it arrives as a pip-audit → pip-api dependency), which is
# what let security.yml drop two `pip install --upgrade pip` bootstraps outright rather than pin them.
ci-scanners = [
"bandit==1.9.4",
"pip-audit==2.10.1",
"zizmor==1.29.0",
]
# The RELEASE SIGNING toolchain (BACKLOG #332). Exact `==`, and the version is the contract for the
# same reason the scanners' are: this runs in the job holding `id-token: write`, and whatever executes
# there signs the wheel, writes the SLSA attestation and publishes to PyPI.
# sigstore 4.4.0, BY OWNER RULING -- given 2026-08-22 and RE-AFFIRMED 2026-09-03 after the full
# history below was put to the owner. "NOT 4.5.0." Recorded at BACKLOG #332. This line is
# the contract (see the paragraph above); do not move it as a side effect of another
# change, and do not let a bot move it -- `.github/dependabot.yml` carries a matching
# `ignore` entry whose only purpose is to keep this pin where the owner put it.
# DO NOT RE-DERIVE THE COOLDOWN ARGUMENT. It has now been derived twice, and it has never
# reached the ruling. `dependabot.yml`'s 5-day cooldown motivated the ORIGINAL 4.4.0
# choice and expired 2026-08-02 (4.5.0 published 2026-07-28T07:34:00Z). The first ruling
# came twenty days AFTER that expiry, and the 2026-09-03 re-affirmation came with the
# expiry stated in the packet. So "the cooldown is spent" is not news to this pin; it is
# the argument both rulings were made in spite of.
# NEITHER VERSION HAS A PATCH LINE, so patch availability does not discriminate between
# them. Measured against PyPI 2026-09-03: sigstore 4.x is 4.0.0, 4.1.0, 4.2.0, 4.3.0,
# 4.4.0 (2026-07-06), 4.5.0 (2026-07-28) and nothing else -- zero patch releases across
# the whole 4.x series. (The 3.x series did ship patches, e.g. 3.5.1 and 3.6.7, so a
# future 4.4.1 is possible and the `ignore` entry is scoped NOT to block one.)
# The point of the group is NOT the version. An inline `pip install X==Y` pins only the TOP package --
# ~30 transitives still float, unhashed, resolved at signing time -- and NO Dependabot ecosystem parses
# an inline install inside a workflow `run:` block, so the pin has no updater, no trigger and no owner.
# Routing it through `uv.lock` hashes the transitives AND puts it under the `uv` ecosystem Dependabot
# already watches. Non-default, like its siblings, so it stays out of the runtime exports and the SBOM.
release-tools = [
"sigstore==4.4.0",
]
# The ADVISORY measurement tools (quality-advisory.yml). Exact where something PARSES the tool's
# output, a floor where nothing does:
# diff-cover `==`: the inline-annotation surface depends on this version's
# `--format github-annotations:<level>` and on adjacent-line coalescing inside
# GitHubAnnotationsReportGenerator.
# mutmut `==`: the workflow SHELL-PARSES mutmut's human-readable output (the `N/M` progress
# line, `🎉 N`, `: survived`, `: no tests`) and reconciles two independent derivations
# of the killed count. A reword produces a GREEN receipt off a wrong number — exactly
# the failure class that workflow's liveness job exists for. Must stay 3.x: 2.5.1
# crashes on Python 3.14 in its pony-ORM cache before generating a single mutant.
# pytest-cov floor only — nothing parses it; it just has to emit a `coverage.xml` diff-cover
# reads. Pinned at the current major so a re-lock cannot regress to 6.x, and left a
# floor so Dependabot can move it without a pyproject edit.
# pytest-timeout DELIBERATELY the identical spec to `[project.optional-dependencies].dev`. mutmut 3
# always passes `--timeout`/`--timeout-method` to pytest and dies inside
# BadTestExecutionCommandsException without the plugin, so naming it here makes this
# lock self-sufficient instead of depending on the editable `[dev]` install having
# supplied it. Same spec in both places ⇒ uv resolves ONE version and they cannot
# disagree.
ci-quality = [
"diff-cover==10.5.1",
"mutmut==3.7.0",
"pytest-cov>=7.0",
"pytest-timeout>=2.3",
]
[tool.pytest.ini_options]
asyncio_mode = "auto"
# Run the whole suite on ONE shared asyncio event loop (tests AND fixtures). pytest-asyncio's default
# fresh-loop-per-test puts a function-scoped async fixture's aiosqlite connection on one loop while the
# test (or its teardown) runs on another — a cross-loop split where a completed query's result is
# delivered to a loop nobody is awaiting. A single session-scoped loop removes that churn entirely and
# also matches production topology (the engine runs on one long-lived asyncio.run() loop). BOTH keys
# are required and must match: setting only the *test* scope leaves fixtures function-scoped (still
# cross-loop). REQUIRES pytest-asyncio>=0.26 (the release that added asyncio_default_test_loop_scope;
# the fixture key needs >=0.24); the dev floor below is >=0.26 to match, and the lock pins 1.4.0.
asyncio_default_test_loop_scope = "session"
asyncio_default_fixture_loop_scope = "session"
testpaths = ["tests", "packaging/messagefoundry-webconsole/tests"]
# Per-test watchdog: a single hung await/socket is killed at 60s with a full thread-stack dump naming
# the culprit, instead of burning the CI job's wall-clock. `thread` method works cross-platform (no
# SIGALRM) and fires even when the main thread is blocked in a C call. The cap is per TEST, and every
# leg passes under it, so no single test exceeds it. This value binds LOCAL runs only: ci.yml passes an
# explicit --timeout= per leg from its matrix (60s ubuntu, 120s Windows), which overrides it.
#
# NO SUITE TOTAL HERE, DELIBERATELY. This line carried "~2-5 min" long after that stopped being true,
# and the figure has since moved again -- #411 took the engine suite across processes, so the serial
# number it implied is now two revisions stale rather than one. A duplicated measurement rots silently
# because nothing points at it; the fix is to not keep a second copy. Suite timing, its provenance, the
# serial-vs-parallel comparison and the worker sizing are recorded ONCE, in the comments above the
# `Tests (pytest)` step and the matrix in .github/workflows/ci.yml. Read it there.
addopts = "--timeout=60 --timeout-method=thread"
markers = [
"win2025_acceptance: Windows Server 2025 on-server acceptance probes (env-gated; skip off-server).",
# Applied by tests/conftest.py from tests/tooling_manifest.txt -- never written on a test by hand,
# so `-m tooling` and `-m 'not tooling'` partition the suite from ONE reviewed list. ci.yml runs
# the engine legs with `not tooling` and this tier as its own path-gated job.
"tooling: repo-harness tests (worktree gate, coordination, ledger, CI workflows) -- subject is the development harness, not the engine.",
]
[tool.ruff]
line-length = 100
# The project requires 3.14 (see requires-python + mypy below), but ruff's formatter is held to py313
# idioms deliberately: a py314 target makes it apply PEP 758 and strip the parens from every
# `except (A, B):` (-> `except A, B:`) repo-wide. We keep the parenthesized form for readability; bump
# this to py314 if/when we choose to adopt the unparenthesized style in a dedicated formatting pass.
target-version = "py313"
# Archived benchmark ARTIFACTS, not maintained source. docs/benchmarks/results/<date>-<topic>/ holds
# the exact scripts a published measurement was produced with; reformatting them edits the record, and
# a number you cannot reproduce from the script filed beside it is worthless. New benchmark code lives
# outside results/ and is linted normally. `extend-exclude` (not `exclude`) so ruff's defaults survive.
extend-exclude = ["docs/benchmarks/results"]
[tool.ruff.lint]
# Signal 10 (Code Quality & Anti-Slop rubric, docs/Code_Quality_Standards.md): broaden ruff beyond its
# E/F defaults to the AI-slop-adjacent families -- flake8-bugbear (B), comprehensions (C4), simplify
# (SIM), pyupgrade (UP), and import sorting (I). Adopted as a one-shot sweep: the safe backlog is
# auto-fixed and the rest grandfathered with per-line `# noqa` so the required gate is green from a
# clean baseline and NEW code must comply (the rubric's "blocking from a clean baseline" intent).
extend-select = ["B", "C4", "SIM", "UP", "I"]
[tool.ruff.lint.flake8-bugbear]
# FastAPI's dependency-injection idiom calls Depends()/Query()/etc. in argument defaults -- the
# framework's intended pattern, not the mutable-default bug B008 targets. Treat them as immutable so
# B008 still catches real bugs (e.g. `x=list()`) everywhere else instead of ~460 framework false hits.
extend-immutable-calls = [
"fastapi.Depends", "fastapi.Query", "fastapi.Path", "fastapi.Header",
"fastapi.Cookie", "fastapi.Body", "fastapi.Form", "fastapi.File", "fastapi.Security",
]
[tool.ruff.lint.per-file-ignores]
# FastAPI route modules put dependency-injection calls (Depends() + the project's own require*() auth
# factories from api/security.py) in argument defaults -- the framework's intended DI pattern, exactly
# what B008 targets. Scope the B008 exemption to the route layers instead of noqa-ing every route sig.
"messagefoundry/api/**" = ["B008"]
"messagefoundry_webconsole/routes/**" = ["B008"]
[tool.mypy]
python_version = "3.14"
strict = true
# Third-party libraries that ship no type stubs / py.typed marker.
[[tool.mypy.overrides]]
module = ["hl7", "hl7.*", "hl7apy.*", "aiosqlite.*", "aioodbc.*", "pyodbc.*", "asyncpg.*", "ldap3.*", "paramiko.*", "opentelemetry.*", "defusedxml", "defusedxml.*", "pyx12", "pyx12.*", "lxml", "lxml.*", "xmlschema", "xmlschema.*", "signxml", "signxml.*", "psutil", "psutil.*"]
ignore_missing_imports = true