Skip to content

v0.5.2

Choose a tag to compare

@github-actions github-actions released this 21 Oct 18:27
· 151 commits to main since this release
94133e4

[0.5.2] — 2026-10-20 — Early Access

This engine pairs with web console 0.5.0, which is published after it under its own
webconsole-v0.5.0 tag. Its UI seam is 6d8b36eb7bb94bf0, so with the console on it refuses to
start beside console 0.4.0. Upgrade the two together, or set
[security].serve_web_console = false. A BACKLOG #N number cites the project's private planning
ledger, so it has no public page.

Added

  • Federated sign-in can authenticate to the token endpoint with private_key_jwt.
  • messagefoundry admin-reset-totp replaces a sole Administrator's authenticator seed from the
    host.
  • The VS Code security editor can set the four per-tier retention acknowledgements.
  • messagefoundry check prints an advisory oauth-request line.
  • An interop lab proxy checks what the engine signs with verifiers it did not write.
  • The audit trail, a user's security events and the connection event log page past their
    first window.
  • The approver queue says what a release would run and whether it can.
  • messagefoundry check prints an advisory fhir-update-if-match line.
  • A start now writes a config_loaded audit row, and GET /config/provenance has a baseline
    from the start on.
  • A log forwarder that is absent or losing records now raises an alert and shows on GET /status.
  • The test harness covers transports through pluggable drivers and sinks.
  • Hostile-content scenarios for the test harness.
  • The test harness covers raw TCP and X12
  • The test harness covers the Http inbound and the REST, SOAP, FHIR and DICOMweb outbounds
  • The test harness has a DATABASE family: a DatabasePoll inbound and a Database outbound.
  • The test harness drives DICOM DIMSE end to end.
  • The test harness covers the Email outbound, and proves a Direct round trip.
  • The test harness covers REMOTEFILE over SFTP
  • The test harness covers the TIMER, LOOPBACK and PassThrough inbounds
  • python -m harness --coverage is a gate.
  • python -m harness --fuzz fuzzes a running engine.
  • Five more Atheris fuzz targets
  • A script the remote-debugging guard refuses is now audited, and it raises an alert.
  • A refused CRL reload now raises its own alert, crl_reload_refused.
  • The /config/reload response carries restart_required, the settings trust anchors a

Changed

  • The engine no longer says nobody can sign in when no Administrator exists.
  • The MFA posture texts say which sign-ins the second-factor switches free, and no longer say
    "admin".
  • messagefoundry cert self-signed now calls its pair a placeholder, not a dev-only
    certificate.
  • admin-set-notify-email and provision-admin exit 2, could not start, when the store cannot be opened or the audit chain's key check refuses the command's audit row.
  • The keyless-backup refusal no longer calls [backup].allow_unencrypted a setting for a
    synthetic or no-PHI box.
  • A trust-anchor path that is set is checked even when nothing loads it, and the docs now say
    so.
  • A client sign-in now ends the session it replaces inside the sign-in request.
  • [backup].allow_unencrypted = true is now a reported loosening, with a guide entry.
  • tray.log and the IDE's "MessageFoundry Checks" output now carry UTC time stamps.
  • ADR 0200 Amendment A is accepted.
  • The shipped Kubernetes manifests set enableServiceLinks: false.
  • messagefoundry security show says why a report is partial.
  • [backup].destination, [dr].seed_archive, [dr].seed_dir and [dr].restore_token refuse
    a <scheme>:// URL whose scheme has two or more characters, and the refusal quotes the scheme and no longer
    the URL.
  • serve logs the unread-MEFOR_*-variable warnings again once logging is configured,
  • On Windows, a secret reference written in another letter case still spares its variable.
  • The development standards now describe review and planning as the process runs them.
  • The [dicom] extra now requires pynetdicom>=3.0.4,<3.1, where 0.5.1 allowed <4.
  • CI and load rigs sign in.
  • create_managed_app built with no auth_settings and without allow_no_auth now logs one
  • A TCP connection refuses 0x20 (a space) as its start or end frame byte.
  • A cluster node refuses the connection flag toggle.
  • Engine config writes record their own baseline through one helper.
  • A config write whose audit row does not land is undone and refused.
  • The API client waits longer for a sign-in's reply.
  • A directory refusal at the second factor, an enrolment or a step-up names the fix for its
    cause.
  • A File inbound refuses a negative min_age_seconds at build.
  • An upload no longer runs the uploaded-logs retention sweep on its own request.
  • Open-mode follow-ups: one check, one 503 text, and removed-key refusals that name the right fix.
  • Breaking: [dr].enabled with [cluster].enabled is refused at config load
  • A released replay or purge whose COMMIT reply was lost is now documented as recorded
    failed.
  • A refused CRL reload is reminded about while the refusal stands.
  • A paged LDAP group search has one overall deadline, and its cap counts groups, not pages.
  • audit-verify prints a FAIL line and exits 1 when a key, key-provider or database error
    stops the check while it reads the chain's rows
  • The repository's locks, and so the container image, move FastAPI from 0.143.0 to 0.143.1, and
    pyproject.toml now requires fastapi>=0.143.1.
  • The [harness] extra now requires PySide6>=6.6,<6.12, where 0.5.1 allowed any later
    release.
  • Linux is not a supported v1 deployment target.
  • BREAKING -- an unknown top-level section in messagefoundry.toml, or a key written above the
    first [section] header, now fails the start instead of loading silently.

Fixed

  • An engine that restarts settles the dual-control releases it left executing.
  • A shutdown cancelled partway through still stops the engine.
  • A held config reload whose directory vanished, or that named a directory outside the reload
    roots, is refused on release instead of answering 500.
  • The approval gate answers a mapped status during an audit or store outage, and pages.
  • Each approval state change and its audit row are now one write.
  • A repeat of an open dual-control request no longer files a second one.
  • A held request is no longer released after dual control stops applying to it.
  • An MLLP, HTTP or DICOM listener's CRL refusal now names the connection and its
    tls_crl_file.
  • The admin-write 429 now says how long to wait.
  • The JSON step-up refusal tells an oidc session to step up at the IdP.
  • POST /me/reauth's own refusal of an oidc session now carries X-Step-Up-Via: idp
  • A 2xx reply with a body over the byte bound is no longer sent again.
  • A reload refused over an unreadable inbound CA now audits reason: "trust_anchor".
  • The feature map and the ADR index no longer call the anonymizer's output PHI-free.
  • The anonymizer no longer refuses a legal empty segment, and its coverage line is harder to
    misread.
  • The save-time upload prune now writes an upload.prune row for every file it removed, even
    when the request is cancelled.
  • A cancelled upload save now removes a file that already landed.
  • A cancelled upload save now pays its cross-engine-shard quota slot back.
  • The upload retention stop logs an audit gap only when one exists, and names the files.
  • python -m tee anonymize-captures reads anon.toml once, before the first message.
  • The intake_paused alert keeps reminding while its probe fails, and its reminder no longer
    slips an interval.
  • Alert state writes for one condition now reach the store in the order they were raised.
  • Each engine process now raises and clears its own intake_paused alert.
  • A log spool that cannot be read now says so.
  • On-disk log spool follow-ups.
  • Session follow-ups from the session-cap work.
  • A restart inside the temporary-password warn window no longer sends the expiry reminders
    again.
  • A CRL expiry alert now says when its date belongs to a running hop's older copy.
  • The secret-expiry warning no longer says a class "only alerts" where nothing alerts.
  • rotate-key no longer seals a lone plaintext upload sidecar over a missing or non-text body.
  • An HTTP Digest challenge the engine will not answer now stops the outbound lane and keeps its
    queue.
  • The intake pause WARNINGs now say what a DICOM inbound does.
  • The keyless serve messages name the opt-out you set.
  • A sign-in or PHI-read rate-limit window that is not a finite number no longer loads.
  • A TLS list the linked OpenSSL refuses is now a named configuration refusal.
  • A dry run or a failed config reload no longer moves the engine's record of where its running
    config came from.
  • Three statements in the operator documents now match the code.
  • More security documents now match the code.
  • The Corepoint importer no longer treats a statement written off a <Line> as run.
  • A slow upload no longer loses its cross-shard quota slot to a sibling engine shard.
  • The auth rate limiter drops a key's aged bucket even when that key never returns.
  • A replayed routed row whose Handler then sends nothing now settles.
  • messagefoundry validate and messagefoundry graph send a print() in a config module to
    stderr.
  • DR release, DR activation and a config reload now finish when the request deadline cuts the
    caller off.
  • The dr.release audit row records whether the queue drained.
  • The config loader's _-helper imports no longer reach the rest of the process, and work inside
    a Router or Handler body.
  • Four more files no longer call the anonymizer's output PHI-free.
  • Anonymizer follow-ups: a false refusal, a raw error, a missing count, and refusal texts
    that changed.
  • An alert webhook that answers 2xx with an over-size body no longer counts as a failed send.
  • A delivery whose 2xx reply body was dropped over the size bound now says so on the message.
  • A permanent refusal that dead-letters now writes a WARNING.
  • A RemoteFile destination with encoding = None now uploads as UTF-8.
  • messagefoundry check names the settings file it graded when serve may read another.
  • An operator start or restart that a listener's start gate refuses with a configuration error
    now answers 409 and is audited.
  • messagefoundry connection upsert and remove name --service-config in a cleartext-listener
    refusal
  • A directory reconcile pass no longer files a false rename conflict from a stale holder read.
  • cert self-signed refuses an existing cert.pem and cleans up its key on failure.
  • The directory reconciler says which reconcile alerts it will not resolve.
  • The DR backup and the subprocess sandbox follow the running config dir.
  • A DR activation's trust-anchor preflights are now bounded, and its abort rows say more.
  • A CA file that is not PEM is now a named refusal, and a DICOM suite refusal names its
    connection.
  • Listeners that share one CRL file no longer each log the scan's held-copy warning.
  • The undeclared-proxy warning names both ways to declare a proxy.
  • A cluster convergence reload's audit rows name that reload.
  • A start's config_loaded row holds up when a reload or a snapshot fault interrupts it.
  • The delta CRL refusal now gives both OpenSSL rules, and corrects the 0.5.0 entry's reason.
  • A refused or malformed hop attestation is now reported plainly.
  • A passive DR box binds no inbound listener.
  • The approval routes answer a failed store read with 503, not a raw 500.
  • Only a missing config directory is answered as not_found.
  • A shutdown drain no longer cuts a reload's own audit row short.
  • A cancelled resolve whose write fails logs one ERROR, not two.
  • A CRL expiry alert no longer advises a restart into a CA-file CRL that would refuse peers.
  • rotate-key fails, and writes no key-rotation stamp, when it skips an uploaded file.
  • The upload-cipher alert and the store cipher's refusal of a plaintext upload no longer
    promise that rotate-key will seal it.
  • GET /approvals names a release the gate always refuses.
  • A failed approval request write is read back, and the write outlives its caller.
  • The retry-cap dead-letter WARNING now follows what the store wrote.
  • Engine shard names are no longer scrubbed out of two log lines and two refusals.
  • rotate-key counts a plaintext upload body it sealed even when that upload's sidecar could
    not be read.
  • rotate-key exits 2 when the Vault client for the store key cannot be built.
  • An embedded approval gate no longer fixes the default claim owner before its engine holds
    its graph.
  • The approval queue warns about a request with unreadable params at most once per five
    minutes per request
  • On Postgres and SQL Server, the engine-shard upload reservation now reads the database server's
    clock.
  • A passive DR box delivers nothing.
  • A passive DR box shows a broken critical feed at start, and an activation can fail one
    outbound without refusing the takeover.
  • The start WARNING about no usable local Administrator no longer loses its first name to the
    credential scrub.
  • The PHI-read and sign-in 429s no longer send a literal Retry-After.
  • A negative [auth].admin_write_rate_limit_per_actor is refused at load.
  • A protocol header floor self-test that fails with an exception now says where.
  • A removed key's refusal no longer says that no environment variable set it.
  • A refused anonymizer salt now says that a random salt can be refused, and what to use
    instead.
  • The trusted_proxies loosening names each range once, in list order within each wording, with
    its other spellings, and an IPv4 entry's mapped twin can now make IPv6 ranges read as trusting every peer.
  • An integer setting refuses a bool or a fraction instead of truncating it.
  • A float cap or timeout refuses a bool, a NaN, an infinity, or text that only rounds to zero.
  • A connection whose target urllib's bypass list names goes direct cleanly.
  • An anon.toml rule on MSH-1 or MSH-2 is now refused when the overlay loads.
  • A bad anon.toml overlay is reported more accurately, and earlier.
  • An anon.toml overlay that points a default field at a different surrogate kind now warns.
  • provision-admin tells the earlier holder before it writes the new --email address.
  • A row that runs out its retries now writes a WARNING when it dead-letters.
  • python -m harness.load.rigadmin run passes the session as one --token=VALUE argument.
  • rotate-key refuses a Vault Transit key of an unsupported type with a clean exit 2.
  • Harness engine nodes and the durability-hook tests no longer leave processes behind.
  • An X12 outbound acts on a TA1 only when its TA1-01 names the interchange it just sent.

Security

  • A settings error no longer shows the mapping it refused.
  • messagefoundry check-privileges now probes the Vault tokens and the AD bind account.
  • SECURITY.md records one more limit of the sign-in time floors.
  • serve and supervise now test the protocol header floor at every start, and refuse on a
    failure.
  • The harness HTTP sinks answer Python's own error pages in plain text, not HTML.
  • Every answer the harness HTTP sinks write now carries the baseline security headers (ASVS
    3.4.4 and 3.4.6, and the base-uri part of 3.4.3).
  • The protocol header floor now covers uvicorn's sans-I/O WebSocket protocol, and uvicorn moves
    past its <0.50 cap.
  • uvicorn and websockets are pinned exactly, to uvicorn==0.54.0 and websockets==17.2.
  • websockets moves to 17.2 under the protocol header floor, and the floor refuses at start when
    an attribute its two uvicorn workarounds use is gone.
  • serve names a web proxy it may pick up from the environment.
  • The deployment documents now state what a proxy in front of the engine must do about request
    framing.
  • The ECH egress sidecar contract states a framing rule.
  • serve pins the API's HTTP parser to httptools (llhttp), and pyproject pins it exactly, to
    httptools==0.8.0.
  • gzip_decompress caps the number of gzip members at max_members, default 1024.
  • Every decompressor checks its caps in a trial pass before the real decompression.
  • The File connector has a max_decompressed_members setting, default 1024.
  • An SVGZ attachment is checked by trial passes before the pass that keeps it.
  • A File inbound with decompress= set warns when max_decompressed_bytes is off.
  • An [api].trusted_proxies entry that covers more than one address is now named as a security
    loosening.
  • provision-admin now requires --email.
  • Three more admin routes refuse your own account: the admin session revoke, the role write and
    the channel-scope write.
  • The live monitor socket and the bulk export check the caller again while they run.
  • The VS Code extension's cached AI policy expires when it enables assistance.
  • The audit export checks the caller again before each page.
  • BREAKING: XML signature checks now refuse weak EC keys and every DSA key.
  • Five more compares run in constant time.
  • An IDE error from a --json CLI call no longer quotes the CLI's output.
  • messagefoundry dryrun reports an unexpected error that stops its per-message loop by class
    only.
  • messagefoundry dryrun sends a print() in a config module, Router or Handler to stderr.
  • A custom role can no longer hold messages:view_raw without messages:view_summary.
  • The IDE's Live Debug reveal now shows one message.
  • The IDE's Live Debug no longer shows error text on a masked run.
  • The captured-reply and outbound-payload bodies now need messages:view_summary as well as
    messages:view_raw.
  • cert import now checks a private key inside an encrypted part of a PKCS#12 bundle.
  • fhir_lookup refuses a resource id that is only dots.
  • A bad OIDC authorization code can no longer hide the federated sign-in link, even when the
    identity provider answers it with a 5xx.
  • An alert rule that can silence a credential reminder is now named as a security loosening.
  • Resolving an interrupted approval now flags a resolver whose account changed after the
    request.
  • messagefoundry check has a new required leg, oidc-revocation.
  • More directory sign-in refusal rows record the client address.
  • A directory revocation that never read the disabled bit no longer tells the holder an
    administrator disabled the account.
  • The IdP step-up compares the IdP's clock with the IdP's clock.
  • The first-seen sign-in address signal reads a per-account record, not the audit log.
  • Signing in again ends the replaced session even when an elevation races it (ASVS 7.2.4).
  • A federated step-up refuses an account demoted in the directory since sign-in.
  • The first-seen sign-in address rows now say which directory leg signed in.
  • The mid-session new-address signal now reports each new address once per session, until the
    session re-verifies.
  • A federated step-up now records whether the browser's address moved mid-ceremony.
  • Behind an ECH sidecar, a CRL no longer lifts the revocation refusal on a SMART or OAuth2 token
    endpoint.
  • A bare CR in a web proxy's own reply to CONNECT is now refused.
  • A CRL that reaches a REST, SOAP, FHIR or DICOMweb destination now lifts its revocation
    refusal.
  • Four more verifying outbound hops are refused when they check no certificate revocation.
  • A test connection on a REST, SOAP, FHIR or DICOMweb destination now re-checks the hop as a
    send does.
  • A hop URL that names no host is now refused, in any posture.
  • Two directory audit rows now commit in the same transaction as the change they record.
  • The SQL Server store's [store].ssl_root_cert pin now takes effect.
  • Turning a credential reminder off, or shortening its lead, is now named as a security loosening.
  • Two hop-policy flags must now be real booleans on a code-first declaration.
  • Verifying a directory account's passkey now needs the directory to confirm the account.
  • A directory account demoted in the directory since sign-in can no longer step up with its old
    roles.
  • One account whose directory probe raises no longer ends the reconcile pass.
  • Plain anonymize() now refuses a line no rule can reach.
  • A loopback bind that trusts a proxy now counts as exposed.
  • Saving a legacy directory scope over the JSON API now needs expected_source: "ad".
  • An ungated config reload proves the audit log works before it swaps the graph.
  • A config reload's audit row names the config that reload loaded, even when a second reload
    overlaps it.
  • The tee anonymizer now applies a rule that names an MSH field.
  • An anon.toml keep that turns off a default scrub now says so.
  • A trust anchor that names an encryption header now refuses.
  • A refused channel-scope save now leaves an audit row.
  • provision-admin refuses to complete an existing roleless account that already holds a
    notification address unless --email is given.
  • A session whose temporary password has lapsed is ended, and can no longer finish the second
    factor.
  • POST /me/password ends the session when it refuses a lapsed temporary password.
  • docs/SECURITY.md now describes the session-level refusal of a lapsed temporary password
  • An unreadable anon.toml is refused without carrying the file.
  • An mTLS certificate identity for a directory account now asks the directory on every request.
  • A Vault hop refusal now reaches the operator in its own words, and it is one type at both
    phases.
  • A blank acr or amr setting no longer counts as MFA.
  • The anonymizer's default rules now map eight more date fields.
  • A FieldRule built in code now has its path checked.
  • A date-typed OBX-5 is now treated as a date.
  • The site-code pass no longer rewrites a filled date.
  • A date scrubbed to empty now leaves a record.
  • A six-digit date that reads two ways is now scrubbed to empty.
  • The dob kind keeps only a time after the date.
  • A JSON caller can read an account's federated binding before it changes it.
  • A vault_transit store now needs a recorded attestation of its AES-GCM bound to start under
    enforce.
  • On vault_transit, rotating the Transit audit key no longer reads as audit-chain tampering.
  • The anonymizer's leak check now loads the publish guard from one fixed path.
  • messagefoundry check reads an instance declared in the environment alone.
  • The off-box log collector is now an [egress] destination.
  • The engine-shard supervisor now forwards its own log records off-box.
  • A trust-anchor refusal and its log lines now quote the anchor path with repr.
  • A connection's own keep-forever retention override now needs the audited acknowledgement.
  • Per-connection tls_ca_file follow-ups.
  • The tls_ca_file of each connection that dials out is now checked.
  • Where a refusal lands follows ADR 0031.
  • A pin nothing would check is refused at load.
  • One gap remains: the hop still reads the file again by path when it is built, after the check.
  • A credential reply is served Cache-Control: no-store by its response model, not by a step
    each route had to declare.
  • The log-forwarding start gate now refuses a collector that is this host.
  • A security notice the relay loses now leaves an audit row.
  • [api].expose_docs = true is now a named loosening.
  • The attachment download's two 422 refusals are now audited.
  • An identity provider that signs people in with no prompt can be exempted from the OIDC callback
    floor by amr.
  • The docs now say which database driver key the engine checks.
  • The session reconciler no longer asks the directory about an id-less row by its name.
  • An empty password on the AD step-up re-bind is no longer counted as a wrong guess.
  • The step-up re-bind tells a disabled directory account from an absent one.
  • A reader whose permissions withhold audit rows is told so, and an OIDC-only auditor can
    export.
  • The engine API refuses a request that carries more than one Authorization header.
  • cert import takes an empty PKCS#12 passphrase when MEFOR_PFX_PASSWORD is set to the empty
    string.
  • BREAKING: [api].trusted_proxies refuses a CIDR with host bits set, such as 10.0.0.1/24.
  • A shortened dual-control dwell is now named as a security loosening.
  • An alert rule's control_action no longer restarts a connection on an event that is not about
    one.
  • An alert rule with no control_target no longer restarts a connection on the other side.
  • An LDAP referral now pages the directory reconciler instead of reading as an outage.
  • An AD connect or receive timeout above its default is now a named loosening.
  • A FHIR connection set to update_url_form = "path" is now on the posture read-out.
  • A proxy URL that carries credentials is refused when they would reach the proxy in
    cleartext.
  • On the generic ODBC dialect, no odbc_params keyword can give the driver another host.
  • A loopback hop is never sent through a web proxy.
  • POST /dr/activate and POST /dr/release ask for step-up.
  • POST /config/reload reads the path as text before it touches it.
  • An archive named in a POST /dr/activate request is confined to the new [dr].seed_dir.
  • The DICOM C-STORE server (SCP) applies source_ip_allowlist when it accepts a connection, and
    bounds its inbound TLS handshake.
  • A child process of the engine no longer inherits the engine's whole environment.
  • Dual-control approval requests carry an integrity seal on a store that holds a key.
  • Custom role definitions and both directory maps carry an integrity seal on a store that holds
    a key.
  • Session rows carry an integrity seal on a store that holds a key.
  • Each account's authorization record carries an integrity seal on a store that holds a key.
  • A message's cells are sealed under a tag that binds the message's channel, and a saved search's
    criteria under one that binds its owner.
  • A queue row's payload is sealed under a tag that binds the row's routing fields.
  • A store that holds a key now requires every audit row keyed, and the audit chain numbers its
    rows.
  • A store key in the Windows service's NSSM environment now refuses the start.
  • A released config reload loads only the config its approval named.
  • A start now alerts when its config differs from the one the store last recorded.
  • MEFOR_ALLOW_INSECURE_CONFIG_SOURCE is refused under [security].enforcement = enforce.
  • A mistyped MEFOR_<SECTION>_<KEY> variable now refuses the load.
  • Four [auth] switches are now reported loosenings when false:
  • Three numeric [auth] settings are now reported loosenings when set looser than they ship:
  • A MEFOR_* variable that names no section and looks like a setting now logs a WARNING.
  • A file that holds a key is created restricted, and protect-key fails closed.
  • The engine refuses a route that declares no authorization.
  • The route walk in scripts/security/route_gates.py sees more.
  • The egress allowlist now denies by default everywhere, not only under serve.
  • The raw-TCP and X12 listeners gain MLLP's per-host connection cap and frame deadline, and every
    socket listener throttles its allowlist refusal log.
  • Windows SSO refuses an account that holds a federated binding, and every bind ends the
    account's sessions.
  • A directory sign-in that newly grants Administrator now raises the administrator_granted
    operator alert.
  • The AD group maps take a full distinguished name only; a short name is no longer accepted.
  • Group map keys and a user's groups are compared in one canonical DN form.
  • A Windows SSO step names a client only when the Kerberos context that checked the ticket
    finished.
  • BREAKING (embedders only): create_managed_app denies by default.
  • Six security signals now raise an operator alert, not only an audit row.
  • An edit-resend can be proved after retention blanks its bodies.
  • [egress].allowed_recipient_domains bounds where an Email() destination may send.
  • A session token replayed from a second address no longer reads message bodies or calls paced
    writes.
  • messagefoundry check now runs the four inbound TLS exposure gates.
  • BREAKING: an off-loopback DICOM server (SCP) no longer counts a too-wide source_ip_allowlist
    as a peer control.
  • A DR takeover hook that runs past [dr].takeover_timeout_seconds is now killed, with the
    processes it started.
  • Operator actions now commit their audit row in the same transaction as the change.
  • Injection and bulk export take a step-up proof bound to the action.
  • A repeated purge or reload still rejoins its open dual-control hold.
  • Upload resend needs messages:edit beside files:browse.
  • The VS Code extension's promote re-proves when the engine asks.
  • The Windows service installer no longer defaults to the checkout.
  • The config trust check reads the cache folder, connections.toml and the code-set tables
  • serve refuses to start when another account can write its settings file or value file
  • messagefoundry service install requires --config
  • install-service.ps1 refuses an engine install that anyone but administrators can write.
  • serve and supervise refuse to start as a Windows service without an absolute
    --service-config.
  • Channel scope on the management API is now checked by type and by a route table, though list
    routes still apply it by hand.
  • Every PyPI publish now runs in a job that only publishes, and names the pypi
    environment.
  • A release now refuses a tag whose commit is not on main, or whose required checks did not
    pass.
  • No asset is added to a GitHub release after it is published.
  • Every remote pre-commit hook repository is pinned by commit, not by tag.
  • A credentialed FTPS hop with either certificate check off is refused at construction, as SMTP
    refuses it (vault BACKLOG #2636).
  • Refusals and warnings that name MEFOR_ALLOW_INSECURE_TLS now say it works only on an
    instance at [security].enforcement = warn.
  • A scoped caller now gets 403 for every connection name it cannot reach.
  • The anonymizer's default rules now map nine more guarantor, insurance and order fields.
  • The support bundle and GET /logs/tail now also run the write-time credential scrub.
  • The engine process refuses a script injected through Python's remote debugging, and reports
    the setting.
  • serve and supervise refuse to start under enforcement = enforce when Python remote
    debugging is enabled in their process
  • The engine service starts in isolated mode, with remote debugging off.
  • serve and supervise list the start-up code in their interpreter, and refuse what no
    package records.
  • The container image's virtual environment is owned by root.
  • The engine's Python children repeat its isolation.
  • The Windows service installer limits the engine's token.
  • The web console seam digest covers each console route's gate.
  • A start warning when no Administrator can sign in without an outside service.
  • provision-admin runs whenever no enabled local Administrator has a usable password.
  • icacls and sc no longer get the engine's whole environment.
  • BREAKING: serve always requires sign-in, on every bind.
  • A sandbox worker that loaded edited config code is refused.
  • audit-verify no longer reports a chain it could not check as a broken one.
  • One setup is weaker than before.
  • A store key the settings name that cannot be resolved while the store opens, such as
  • A gated JSON route refuses a caller with no session before it reads the request body.
  • Every messagefoundry command installs the remote-debugging guard, before it parses its
    arguments.
  • Turning off a size cap on an HTTP, File or SFTP/FTP inbound now logs a warning.
  • The POSIX config-source check trusts an owner that is root or the engine's own user, and
    only root when the engine runs as root.
  • A production instance refuses DEBUG logging at run time, as it does at start-up.
  • BREAKING: an attachment the sender labelled text/csv now downloads as text/plain with a
    .txt name.
  • A FHIR transaction or batch connection now screens every Bundle entry before it posts.
  • FhirPeek.parse now refuses a FHIR body in which a JSON object repeats a key.
  • Every log sink now escapes the C1 controls, the bidirectional controls and the Unicode line
    and paragraph separators, not only C0 and DEL.
  • No setting can turn sign-in off, in a config file or in code (vault BACKLOG #2825).
  • A start's config_loaded audit row names the graph that start loaded.
  • A DR activation keeps the graph the running engine serves.
  • The DR seed marker's config digest no longer fails an activate or a release.
  • An Email() destination's sender is now held to the recipients' address rule.
  • A mail subject or alert template holding a lone surrogate is refused at load (vault BACKLOG
    #2842).
  • Three domain lists and the mail address check now share one host-name rule.
  • The FastAPI floor is now 0.140.0, superseded in this release by fastapi>=0.143.1.
  • The engine now counts every refusal of a route that declares no authorization.
  • The route walk in scripts/security/route_gates.py says what the refusal makes of each row.
  • The MLLP frame deadline now starts on the first byte of any kind, and stop() clears the
    per-host counts in a finally.
  • An MLLP or TCP delivery refuses a payload holding more than one message.
  • A Direct() destination and the [alerts] email sender now hold their addresses to the
    Email() destination's rule and pass the SMTP envelope explicitly.
  • BREAKING: a start now refuses a CRL file that holds a CRL proven badly signed.
  • The email address rule now checks the shape of the domain, not just its characters.
  • An Email() destination's sender domain must now be in [egress].allowed_recipient_domains.
  • An administrator's password reset or MFA reset clears the account's known sign-in addresses.
  • GET /security/posture now reports how often the deny-by-default refusal fired.
  • Four older edges of the route-gate walk and the deny-by-default refusal are closed.
  • The anonymizer keeps a numeric OBX-5 only when it fits the grammar of its own type.
  • A release goes public only after every PyPI upload its run makes.
  • Each Vault hop now refuses an off-box Vault whose certificate revocation nothing checks,
    unless that hop is attested.
  • Follow-ups to the refused HTTP Digest challenge, and a 2xx reply that cannot be read is no
    longer sent again.
  • The de-identification site-code loader reads one fixed token path and no longer searches
    parent folders.
  • A de-identification rule on a BHS or FHS field now reaches the field the leak-check
    counts.
  • A line that is a bare segment id now leaves a coverage record, and the anonymizer's
    leak-check reads one header line.
  • A refused FhirLookup no longer stops the engine from starting.
  • An LDAP search that does not end with success is never "no such account".
  • A passkey registration staged before the account gained a factor can no longer finish.
  • A refused audit write in one account's directory reconcile no longer ends the pass.
  • On SQL Server, the session writes that follow a committed auth change now wait longer for a
    busy pool, and the wait is still bounded.
  • A sign-in now ends the session its presented token became, when an elevation finished first.
  • Sign-out and revoking one of your own sessions no longer report a revoke that missed (ASVS
    7.4.1).
  • safe_exc no longer quotes the character or byte an encode or decode error failed on.
  • BREAKING: [api].trusted_proxies refuses a zoned entry, matches an IPv4 entry in both
    spellings, and two [api] lists split their env form on commas.
  • The new-address audit action is renamed auth.new_address_reverify_required.
  • POST /me/notify-email now asks the new-address question, as its console twin does.
  • A console page refused for a new address comes back with its path still encoded.
  • audit-verify exits 5, not 0, when a keyless chain walks clean in a shell whose settings
    require a store key.
  • audit-verify loads its settings as audit-anchor does, so a settings error is rendered by
  • Enrolling a factor on a directory account now needs the directory to confirm the account.
  • The open mode is an app with no auth service, and the code now says so.
  • [inbound].ack_after = "delivered" is refused when the settings load.
  • The build check gates the same listeners a reload binds, from one copy of the skips.
  • A DR activation on a running box now parks the feeds below the threshold.
  • A DR activation applies the running graph, not the config dir on disk.
  • Rows queued to a parked outbound are held, not dead-lettered.
  • Start, stop and restart of a DR-parked outbound are refused with 409.
  • A second stop of a paused outbound no longer leaves it stopping.
  • Two more domain checks at load.
  • A cluster convergence reload now writes its own config_reload audit row.
  • A clustered node converges on a sibling's reload that lands during its own start.
  • A repeat reconciler revocation of one account is audited but not alerted, and only a verified
    repeat stays quiet.
  • The engine names directory reconciler settings that leave the mass-revoke breaker unable to
    trip.
  • Authoring docstrings and connection-editor help that name MEFOR_ALLOW_INSECURE_TLS now say it
    works only on an instance at [security].enforcement = warn.
  • A directory probe that raises is now a probe fault, not a directory outage, and it pages.
  • The MFA requirement's enrol-first gates now cover an account with an unrecognised provider.
  • A lock notice the relay's full queue drops no longer holds the next one back.
  • A narrowed dual-control list is now named as a security loosening.
  • The sandbox worker's memory cap no longer fails innocent messages on Linux.
  • env(key, cast=bool) now reads a boolean strictly.
  • A directory reconcile pass that newly grants Administrator now raises the
    administrator_granted operator alert.
  • A refused federated step-up records where it started.
  • The administrator who resets an account's factors is now sent the expiry reminder for the
    temporary password that reset issues.
  • An AD re-bind the domain controller ends busy is an outage, not a wrong password.
  • The session reconciler counts a referral or a probe fault over the lookups it sent, and strikes
    an id-less row on every pass.
  • Federated step-up and binding follow-ups.
  • A request deadline can no longer leave an uploaded file without its upload.create row, or a
    deleted one without its upload.delete row.
  • Each upload-quota reservation now has its own identity.
  • A codec error no longer puts the input it failed on into a log line or a refusal, on at
    least the paths below.
  • A crafted SPNEGO token is now an audited Kerberos refusal, not a 500.
  • Three more routes now answer by channel scope.
  • A refused Kerberos token is audited as a refused sign-in, and the row no longer quotes the
    token.
  • The new-address step-up row records the address in its client column.
  • A forged federated sign-in callback no longer uses up the real one.
  • GET /security/posture reads the [auth] limits off the attached auth service.
  • A released dual-control purge now writes its connection_purge row, and alert acknowledge,
    resolve, suspend and resume commit their audit row with the change.
  • A refused OIDC callback that the floor exemption let through now carries the
    callback_floor_exempt_amr marker.
  • A misframed JWKS reply no longer hides the federated sign-in link, and a verified token
    shows it again.
  • An AD group map key that starts or ends with a non-printing character is refused.
  • A Windows sign-in whose name matches several directory entries can now succeed.
  • A directory role change that takes Administrator from its last enabled holder now raises an
    alert.
  • No escape sends a credential to a peer the engine has not verified (vault BACKLOG #3230).
  • Approving a held action needs a fresh step-up.
  • Edit-resend gives back its proof when the engine refuses before it writes.
  • A Vault provider's error keeps the text of the Vault hop's own refusal only.
  • The Vault client build no longer raises a parser error that quotes a proxy URL.
  • An SVG or HTML attachment download refuses an svg start tag after leading text.
  • The admin routes that refuse your own account now also check the stored id, and each refusal
    is audited.
  • A self-service revoke of one session that ends nothing is now audited.
  • PATCH /users/{user_id} now checks a changed email the way POST /users does.
  • More of the routes by which a codec error could put the input it failed on into a log line or
    a refusal are closed.
  • A failed inbound TLS handshake now leaves a record on the HTTP intake listener, the API and
    the MLLP listener, on any event loop.
  • A sign-out's auth.logout audit row records the client address it came from (ASVS 16.2.1).
  • The test harness's HTTP sinks no longer send a Server line.
  • The bulk export stops when the caller's channel scope widens mid-stream, not only when it
    narrows.
  • A deliberate export stop no longer reads as a crash in the server log.
  • Known gap, not fixed: a stopped export reaches the client as a complete-looking short
    file.
  • BREAKING: a keyed store now remembers which encrypted columns it has keyed.
  • BREAKING: the Direct transport now refuses DSA keys and every unlisted key type.
  • The first-Administrator service now requires the notification address, as provision-admin
    does.
  • A credential in a proxy URL no longer reaches a second proxy in cleartext.
  • On Python 3.15 with OpenSSL 3.4 or later, the apiclient and the tray probe stop offering
    SHA-224 TLS signature schemes too.
  • A proxy_user credential bound for a proxy written https:// is judged as cleartext.
  • A proxy credential on a request that goes direct is refused.
  • A proxy credential sent over TLS that does not verify the proxy is refused, admitted or
    not.
  • A trust-anchor pin may name its digest, and each auth.trust_anchor audit row names the digest
    of its fingerprint.
  • A static Proxy-Authorization beside proxy_user is refused at build.
  • The CRL signature texts now say only what the engine does.
  • The account seal checks the TOTP code against the secret it checked, and names its rollback
    limit.
  • A CRL's issuer name is matched to the hop's CA as OpenSSL matches it, for ASCII letter case.
  • pyproject.toml now requires certifi>=2026.10.19, past
    GHSA-6q48-6c7w-9gw2.
  • The audit-pin job messagefoundry init generates now audits the Windows-only dependencies.
  • The harness X12 sink never echoes an ISA13 that is not nine digits into its TA1 (ASVS 1.1.2).
  • The harness Compose tab frames through frame_checked by default.
  • samples/send_mllp.py prints the peer's ACK as printable ASCII.
  • messagefoundry verify's live smoke frames through frame_checked
  • More console lines, and the Markdown verify report, now escape a peer's text (ASVS 1.1.2).
  • samples/send_mllp.py refuses a file holding an MLLP frame byte (ASVS 1.1.2).
  • More of the tools that print a peer's text now escape it first, and harness labels fed by the
    engine show it as plain text (ASVS 1.1.2).
  • tls_check_hostname = false is a recorded loosening now, never a silent one (ASVS 12.3.2).
  • A credential in a connection url's query string is a recorded loosening now (ASVS 14.2.1).
  • The Vault TLS context now pins the approved key-exchange groups, as every other engine-built
    context does (ASVS 11.6.2).
  • The off-box audit record stays valid JSON after the log scrub in more cases, and the tee adds
    no escaping before redaction.
  • Many escaped characters in engine log lines are spelled differently.
  • The repository's locks pin certifi 2026.10.19, past
    GHSA-6q48-6c7w-9gw2.
  • BREAKING -- messagefoundry check now fails settings the retention start gate refuses.
  • The Email destination refuses a body it cannot encode without naming any of it.
  • The route-gate walk and the refusal count read the RouteContext shape of FastAPI 0.143.1.
  • The engine API turns fastapi's built-in OpenTelemetry telemetry off.
  • The federated bind, rebind and unbind audit rows say whether the account holds engine
    factors.
  • The send-time proxy credential rule reads a header name however it is spelled.
  • A purge on SQLite now overwrites the old bytes, and the WAL is truncated hourly, by default.
  • No upload audit row carries the file name.
  • docs/PHI.md now says what a second length cut does to an earlier truncation note, and states
    the custom-delimiter residual an operator could not read before.
  • The RemoteFile destination refuses a payload it cannot encode without naming any of it.
  • Under internal_error = stop, an unencodable RemoteFile payload no longer stops the lane.
  • BREAKING -- messagefoundry check and supervise now refuse a connection's own keep-forever
    retention override that serve refuses.
  • A redacted UnicodeError no longer prints a false position, and names a codec by a tighter
    rule.
  • The Email destination refuses, at load, a subject whose encoded word decodes to a control
    character.
  • A store-key rotation no longer resets the age of every other tracked secret.

These notes show each entry's title only, to fit GitHub's limit on a release body. The full section is in CHANGELOG.md.