Repository navigation
[0.5.0] — 2026-10-20 — Early Access
Requires engine 0.5.2. Supported engine UI seam: 6d8b36eb7bb94bf0, the value engine 0.5.2
ships as messagefoundry.api._ui_seam.ENGINE_UI_SEAM. With the console on, any other engine refuses
to start. Console 0.4.0 does not work with engine 0.5.2, and this console does not work with engines
0.5.0 or 0.5.1, so upgrade the two together. The entries marked as seam changes below say why the
digest moved.
Added
- The status page shows the four per-tier retention acknowledgements. The security posture
table on/ui/statusshowedallow_keeping_phi_indefinitelyonly. It now also shows the
transform-state, search-preset, app-log and backup-archive switches, each on its own row. The
table stays read-only. (BACKLOG #2280) - The status page and the health indicator show the off-box log forwarder. With a forwarder
configured, the engine table on/ui/statushas an "Off-box log forwarding" row. It reads
healthy, or says that forwarding is not running or is degraded, with how many records were
lost since the process started. The health indicator turns to warn when forwarding is not
running or is degraded. A UDP forwarder is never called healthy: the row says that delivery is
not confirmed, because the engine counts no failed send over UDP, and the indicator does not
warn for that alone. With no forwarder configured there is no row. Under engine shards the row describes the
process that answered. Seam change:SystemStatuscarrieslog_forwarder, and the console
importsLogForwarderInfo. (BACKLOG #2612) - The audit, security-event and event-log pages page past their first window.
/ui/audit,
/ui/security-eventsand/ui/eventstakelimitandoffsetand draw the shared pager, so
each says which rows of how many it shows, with Previous and Next links. They used to show only
the newest rows, with no way to reach older ones from the console. Each pager link carries the
snapshot the first page was read under, so rows written between clicks do not shift the pages.
The event log's pager and its reason reveals carry the page's filters, position and snapshot, so
a reveal reads the window it was clicked on. Seam change:AuditList,SecurityEventsListand
the newConnectionEventListcarrytotal,limitandoffset, plus their snapshot (as_of
on the first two,before_idon the third), andCoreHandlersgainsconnection_event_page.
(BACKLOG #2438) - The audit page exports, and says when rows are withheld.
/ui/auditlinks to the new
/ui/audit/exportfor a holder ofaudit:export. That route streams the engine's audit CSV from
the console session, so an auditor who signs in only through OIDC, and so holds no bearer token,
can export. For a reader withoutusers:managethe page says account-lock entries are left out
of its list, count and export, and never which./ui/security-eventssays that a change an
administrator made to the account is not listed there, and that it is emailed when it can be.
Seam change:AuditListcarrieswithheld, andAdminHandlersgainsexport_audit.
(BACKLOG #2446) The route passes its cookie session toexport_audit, which resolves it again
before each page and stops a download whose session or permission is gone. (BACKLOG #1154) - The approvals page resolves an interrupted release, and shows what each hold captured. An
interruptedrelease offers the resolve, Effects applied or Effects not applied, behind a
required "I checked its effects" box, throughPOST /ui/approvals/{id}/resolve/{outcome}. It
asks for the same fresh step-up as the engine's resolve (BACKLOG #2460). Each row shows the
parameters its hold captured (BACKLOG #2458). Approve is not offered on the requester's own
request, on a row whose stored parameters are unreadable, or on a row whose operation dual
control no longer gates. Each such row says why and offers the reject instead, labelled
Withdraw on the requester's own row and Reject on anyone else's. Seam change:
resolve_actionjoins the handlers, andPendingApprovalInfogainsparams,
caller_is_requesterandgated. The console also importsResolveOutcomefrom
messagefoundry.api.models, so on an older engine it fails at import, before the seam check. - The reload pages say what a reload left undone. The inline reload result page and the
approval page for a released reload show a banner for a degraded follow-on step and for each
settings trust anchor that takes a restart; before, both reached only the JSON answer. Seam
change:ReloadResultcarriesrestart_required. (BACKLOG #3015)
Changed
- The package requires
messagefoundry>=0.5.2. Console 0.4.0 requiredmessagefoundry>=0.5.0.
The floor still has no ceiling (RELEASE.mdstep 2), so beside an older engine pip upgrades the
engine, and an unpinned upgrade takes the newest release, whose seam may differ. Pin both:
messagefoundry==0.5.2withmessagefoundry-webconsole==0.5.0. - Linux is not a supported v1 deployment target. The package drops the
Operating System :: POSIX :: Linuxclassifier, matching the engine
(why). - Seam only: the engine's posture model grew.
SecurityPosturegains
transit_bound_attestation, aTransitBoundAttestationView(BACKLOG #2337). The console
renders nothing new, but the field is on the seam it pins, so the supported digest moved. - Seam only:
client_iptakes starlette'sHTTPConnection.api.security.client_ipnow
takes that base class (BACKLOG #2289); it tookRequest | WebSocket. The console still
passes aRequestor aWebSocket, and both fit, so no console call changed. The signature
is on the seam the console pins, so the supported digest moved. - Seam only:
/statusnames the FHIR lookups that failed to start.EngineInfogains
fhir_lookups_failed(vaultBACKLOG #3008). The console does not show it yet, but the field
is on the seam it pins, so the supported digest moved. - Sign-out records where it came from.
/ui/logoutpasses the caller'sclient_ipaddress to
AuthService.logout, so theauth.logoutaudit row records it. Seam change:logouttakes a
requiredclientkeyword, so the supported digest moved. A console from either side of this
change refuses an engine from the other at mount. (BACKLOG #3339) - Seam only: the posture reports the deny-by-default refusal count.
SecurityPosturegains
undeclared_route_refusals(vaultBACKLOG #2954). The console does not show it yet, but the
field is on the seam it pins, so the supported digest moved. - Seam only: the seam digest covers each console route's gate. The engine's seam snapshot now
records what this console installs: each route's top-level dependencies, gates included, with
their own source and captured settings, and its mounts, middleware and WebSocket authorizer
(vaultBACKLOG #2709). No console route changed, but the supported digest moved. A console
release that adds a route, changes a route's gate, or edits a console helper a gate calls by
plain name now moves the seam too, so it needs an
engine that ships the new value. The released 0.4.0, whose DR routes sit on the session alone,
passed the handshake of engines from PR 1910 up to engine PR 1940, whose unrelated seam move
refuses it; this change guards the next gate change that moves nothing else. - Each group input on the AD group page shows an example full DN. The placeholder reads
CN=MF-Admins,OU=Groups,DC=example,DC=com, since both maps refuse a short name. Nothing is
stored from it. The page's two save forms now trim only a space, a tab and a line break from a
group, as the engine route does, so a group ending in a no-break space is refused with the
route's 400 rather than trimmed. (vaultBACKLOG #3222) - A directory refusal at the second factor, an enrolment or a step-up names the fix for its
cause. When a directory refuses the account, the MFA gate (/ui/mfa), the re-auth page
(/ui/reauth, its passkey leg included) and the account page's TOTP and passkey enrolment
finishes now show one text per fix. A role lost in the directory since sign-in is
told to sign out, then sign in again. Every other cause, an outage and a role map that fails its
seal included, is told to try again later and, if it is still refused, to ask an administrator.
Each names the same fix as the engine's 403 for that cause. Seam change: the console imports
DirectoryRefusaland readsElevation.directory_refusal, so the supported digest moved.
(BACKLOG #3082) - The "Flag not changed" page has a hint for a cluster node. A cluster node refuses the
connection flag toggle with a 409. The page now carries a second hint, shown beside the
code-first one on every 409: each node loads its own config directory, so setflaggedon the
connection's entry inconnections.tomlon every node, then apply it with a coordinated restart.
The code-first hint is reworded, with the same meaning. No seam change. (BACKLOG #2933) - A flag toggle the audit log did not record gets its own page. When the engine refuses the
toggle with a 503 because its audit row failed or timed out, the console shows a "Flag not
changed" page that says the engine undid the change, unless the message on the page says it
could not, and to try again. The 409 page's hints are not shown for it. (BACKLOG #2945)
Fixed
- A throttled
/uiwrite now says how long to wait. The admin-write429carried a literal
Retry-After: 10, whatever the engine's window was. It now carries the time until that account's
next write would be admitted, in whole seconds, the same value the JSON API sends. Seam change:
the console calls the newAuthService.admin_write_retry_after. (vault BACKLOG #2144) - A throttled PHI view or sign-in now says how long to wait. A
require_ui(phi=True)view's
429carried a literalRetry-After: 10, andPOST /ui/logina literal30, against a default
60 s window. The PHI view now carries the account's own wait from the limiter that refused, in
whole seconds, the value the JSON API sends.POST /ui/logincarries the whole sign-in window, so
it discloses no per-bucket timing.POST /ui/mfa,/ui/reauthand/ui/reauth/webauthnkeep
the constantRetry-After: 30, so a caller holding only the password gets no exact pacing for
second-factor guesses. Seam change: the console calls the newAuthService.phi_read_retry_after
andlogin_retry_after, and callsallow_reauth_attemptdirectly; the unreachablegetattr
fallback to the sign-in budget is gone. (vault BACKLOG #3293) - A temporary password's deadline is stated in the past tense once it has passed. The factor
page (/ui/mfa) and the forced password page used to tell the holder the password "stops
working" at an instant already gone. They now say it stopped working then, and to ask an
administrator to reset it. No seam change. (vault BACKLOG #2298) - The Approvals page's 503 guidance names no single action. It said an audit log could not
record a release, and it also answered a rejection and, now, a queue the store could not read.
It now covers all three and says nothing ran. No seam change. (BACKLOG #3157) - The Approvals page no longer offers Approve on a request the engine always refuses. It reads
the engine's newrelease_blockedreason, so a request that predates requester-id attribution,
or whose operation is no longer registered, or a purge or reload whose parameters name no requester, gets
only the reject and a note saying why, as an ungated or unreadable one already did. Each note
names the button beside it, Withdraw or Reject. Seam change:PendingApprovalInfogained
release_blocked. (vault BACKLOG #3192)
Security
-
The users list shows channel scopes only to an administrator. The engine now sends each
account's channel scope to ausers:manageholder only, so/ui/usersdrops its Channel scope
and Scope source columns for any other reader rather than showing every account as "(none)".
The page reads the engine'schannel_scope_withheldflag rather than deciding the rule itself,
so this console needs an engine that sends it, and refuses an older one at startup.
(vault BACKLOG #3189) -
The configuration page offers no reload the engine will refuse. For a channel-scoped
deployer, whose reload the engine refuses after the step-up is spent,/ui/configshows the
engine's reason in place of the Reload form. The engine computes the rule. (vault BACKLOG #3189) -
The Approvals page stops offering Approve on a request whose requester lost the authority it
needs, such as a held reload or purge whose requester has since been channel-scoped. Every
approver was refused such a release. (vault BACKLOG #3189, #3192) -
The status page says when its loosenings list is incomplete. It now shows the engine's
loosenings_scopenote under the security posture, so a list narrowed to a channel-scoped
reader's scope, or read with no connection graph loaded, no longer reads as the whole posture.
(vault BACKLOG #3189) -
The bulk control and bulk purge pages no longer write a malformed name into the audit log. A
channel-scoped operator's selection that is not a valid connection name is still recorded as an
auth.channel_deniedrow naming the operator and host, but the row carries no channel. It used to
carry the submitted text whole, bounded only by the request size. The page now answers such a
selection "not a valid connection name", as it does for any other operator. (vault BACKLOG #3031) -
A
/uirequest whoseSec-Fetch-Sitelines disagree is refused. It answers403on any
method. The fetch-metadata middleware used to keep the last such line while the same-origin
check read the first, so the two could judge different values of one request. Both now refuse
it with the same reason. A browser sends the header once, and repeated lines that carry one
value are judged as that value, so no browser request changes. Seam only: the gate helpers it
edits are hashed into the seam since BACKLOG #2709, so the supported digest moved.
(vault BACKLOG #3316) -
A session revoke that ends nothing is audited, with your address. The revoke button on
/ui/account/sessionsnow passes the request's address to the engine, which writes one
auth.session_revoke_missedrow when the id names no session or another account's. The notice
is unchanged.AuthService.revoke_own_sessiontakes a required keywordclient; the seam
digest records that signature, so it moved, and a console and engine on either side of this
change refuse each other at mount. (vault BACKLOG #3260) -
The passkey registration finish draws on the per-actor ceremony budget.
POST /ui/account/webauthn/verifynow charges the same budget asPOST /ui/account/mfa/verify,
and answers a throttle with a JSON 429 the page can show. No seam change. (BACKLOG #3014) -
The audit page shows where each entry came from.
/ui/audithas a Client column, which
shows the address the engine recorded for the entry, or a dash when no client was in scope.
An operator tracing a run of refused sign-ins can now see their source on the page, not only in
the export. No seam change:AuditEntry.clientwas already sent. (vault BACKLOG #3197) -
A forged federated sign-in callback no longer cancels the real one. A callback to
/ui/oidc/callbackthat does not carry the flow's ownstatenow keeps the flow cookie, so
the real IdP return can still finish the sign-in. That covers anerror=or malformed callback
too, which the engine now ends only when itsstatematches. Any page can send a browser to
that URL with the cookie, and clearing it ended the sign-in in flight. Seam change:
AuthServicegainsabandon_oidc_login, andLoginOutcomeandOidcStepUpcarry
flow_consumed, which the route reads instead of a reason slug. (vault BACKLOG #3198) -
Changing a second factor now offers to sign out your other sessions. Confirming a TOTP
enrolment, turning TOTP off, adding a passkey and removing one each end with an offer to end
every other session and keep this one (ASVS 7.4.3). The enrolment confirm shows it below the
recovery codes. The other three show it once, on the account page they land on, through a
single-use mark on the session, so a typed?m=link shows no offer. Accepting asks for the
usual re-proof, then signs the other sessions out. Declining ends nothing. Factors changed in
Active Directory or at an identity provider are outside it. No seam change. (BACKLOG #1147) -
A
/uiwrite that does not name where it came from is refused. It answers403and
changes nothing. At least two cases that used to pass are refused now: a write with neither
Sec-Fetch-SitenorOrigin, which left the sign-in POST with no cross-site control, and a
write whoseSec-Fetch-Siteis a non-empty value other than the four the Fetch Metadata
standard defines.
A current browser sendsSec-Fetch-Siteon every request, so the console's pages are unaffected
there. A browser that sends neither header on its own form POST could not sign in with a
password or submit any/uiform, sign-out included;docs/BROWSER-SUPPORT.mdsays what is
and is not measured about such a browser. A script that drives/uimust now sendOrigin.
The CSP report sink/ui/csp-report, which changes nothing, still accepts a report with
neither. A GET is not held to this: the sign-in GETs deliberately not blocked for missing fetch
metadata, under owner rulings R4 and R4b of 2026-09-28, are listed in theOriginrow of
docs/BROWSER-SUPPORT.md. The rule for one line of each header is the table in the
assert_same_origindocstring (messagefoundry_webconsole/_auth.py), which also names what
the table does not cover. No seam change. (BACKLOG #1116, #1124) -
The user page's self-target refusals are audited, with your address. Reset-password,
reset-mfa, link, unlink, disable and delete now pass the request to the engine's handler. The
engine refuses your own account by its stored id too, and writesauth.self_target_refused.
The seam digest did not move, because it records handler names and not their parameters. A
console without this change would get a TypeError from those six against this engine. (vault
BACKLOG #3259, #3260) -
Resend, edit-resend, upload resend, queue purge and config reload each take a proof bound to
that action. A fresh session window no longer reaches them. Each needs a re-authentication made
for it, which it spends. The re-auth page mints that proof when it continues to the action's
confirm page, editor or auto-retry. The message editor asks for it before it opens, and the
resubmit spends it only after its own input checks pass. An engine refusal at or after the
store write keeps the spend, so the next submit asks again and re-opens the editor. A completed
resend now answers with
a 303 to a new GET outcome page,/ui/messages/{id}/resend-done(post-redirect-get), so a
refresh re-renders the outcome and sends nothing. The confirm page always mints its own
idempotency key; one in its query string is ignored. A double-click on a resend or an
edit-resend asks for no second proof. Only an identical request rides on the first one's
proof: the same key, target and source, or the same edited body. A resend whose key the store
already holds needs no proof whatever its source, since the store answers it as a duplicate.
A repeat that arrives while the first is still running waits for it. If the engine refused the
first, the repeat gets the same refusal and runs nothing. An edit-resend refused before the
store write gives its proof back (vault BACKLOG #3241), so the next submit needs no new
password; any other refusal leaves the next submit asking for a proof again. A repeat of a
request the store committed is answered as a duplicate, a request cancelled after that commit
included. Upload resend also needsmessages:edit, on the
confirm page and on the POST, and the browse page offers its form only to a role that holds
it. Seam change:AuthService.holds_action_step_upand the core handlerprior_resendare
new, and the console imports five new step-up action constants. (vault BACKLOG #2625) -
Approve asks for a fresh step-up, and an unlock page's grant needs an issued page. The
Approvals page's approve now takesrequire_ui_step_up, as resolve does; a stale window lands
back on the page to approve again. A re-auth toward a confirm page or the editor mints its bound
grant only when a gate issued that page to this session. Otherwise a password re-auth ends on
the page saying nothing ran, and a federated one on the console's landing page. The purge and upload-resend confirm pages hold the action proof without spending it.
/ui/messages/{id}/resend-donenow needskeyand renders from the store's record of the
resend. (vault BACKLOG #3241) -
A route-level sign-in reject records where it came from. The
/ui/oidcstart and callback
legs andGET /ui/ssorefuse some sign-ins themselves. Examples are a missing flow cookie, an
IdP error, a malformed callback or Negotiate token, a failed start, and a non-navigation fetch.
The engine audits each one, and itsauth.login_failedrow used to carry noclient. The
routes now pass the caller's address. Seam change:audit_oidc_rejectand
audit_kerberos_rejecttake a required keywordclient. (vault BACKLOG #2132) -
The step-up re-auth page names the action, and auto-submits only a continuation the server
issued to this session./ui/reauthused to auto-submit whichever registered action itsnext
named, so a typed, bookmarked or clicked link to it could turn a routine re-authentication into a
destructive action. A step-up gate now records the continuation it hands out, bound to the
session, single use and short-lived. Any othernextstill re-authenticates, says nothing will
run, and ends on a page saying nothing ran. The per-connection purge POST is no longer a registered
continuation; its re-auth returns to the purge confirm page. No seam change. (vault BACKLOG
#2764, ADR 0065 amendment of 2026-10-06) -
A directory sign-in that newly grants Administrator pages the operators.
GET /ui/ssoand the
/ui/oidccallback now raise the engine'sadministrator_grantedalert when the sign-in's
directory role sync gave the account the Administrator role it did not hold before. The alert is
keyeduser:<username>, withviaset todirectory_sign_in_ssoordirectory_sign_in_oidcand
granted_byset to<directory>. An account that already held the role raises nothing. Seam
change: the console importsalert_directory_administrator_grantedfromapi.security. (vault
BACKLOG #2610) -
A domain entry with a leading dot matches nothing. The interstitial check stripped the
dot, so.hospital.examplefrom a caller other than settings covered the domain. Settings refuse
such an entry at load, so the strip only ever widened a match. No seam change. (BACKLOG #3075) -
An IPv4 entry in
organization_domainsorexternal_link_allowlistmatches only that
address. The interstitial check matched every entry on a label boundary. So a host that merely
ended in a listed address would have counted as covered. The engine still accepts a canonical
dotted-quad address in either list. It now refuses a partial or non-canonical one, and any other
entry not shaped like a host name. No seam change. (vault BACKLOG #2843) -
The interstitial check reads a link the way a browser does. It called anything starting
with/local, so//evil.exampleand/\evil.examplewould have skipped the "you are
leaving" page although a browser takes both to another host. Only a path on this origin is now
local, after the clean-up a browser applies, every percent-decoding of the path, and with no
.or..segment. The host check readshttps://evil.example\@hospital.example/as
evil.example, refuses a host a browser would read differently, and the allowlist never exempts
a scheme other than http(s). The OIDC interstitial shows the host it judged, an IPv6 one
included. No seam change. (vault BACKLOG #2790) -
A cookie replayed from a second address is sent to re-authenticate before a PHI page or a
write. The message pages, the dead-letter list and the three reason reveals (every
require_ui(..., phi=True)page), and every write onrequire_ui, now send a session presented
from a host it has not verified from to/ui/reauth. A must-change session is not asked. A PHI
page comes back to itself after the re-auth, query included, so those pages are now registered
continuations. A write lands on/ui, or on/ui/accountfor a session without
monitoring:read, and the operator presses the button again. The step-up routes already did this. Pages that are neither,
the dashboard included, still load. No seam change. (vault BACKLOG #2620) -
Every console route declares its authorization. Each
require_ui*gate carries the engine's
gate mark, and each sign-in and re-authentication route is markedpublic_routewith a reason.
The engine now refuses a route that declares neither. The supported engine seam moved because the
console imports both names fromapi.security. (vault BACKLOG #2604) -
Activate DR and Release DR ask for a fresh sign-in proof.
POST /ui/dr/activateand
POST /ui/dr/releasenow sit behindrequire_ui_step_up, like the stepdown control. A session
whose step-up window has lapsed is sent to/ui/reauth, then back to the status page to press
the button again. The action is never re-sent for the operator. Both were reachable on the
session alone. (vault BACKLOG #2581) -
The Link form and its notice say what a link takes away. Linking an account to a
federated identity now ends the account's sessions, and Windows SSO no longer signs the
account in while it is linked. The form says so before the link, and the Linked notice after
it. The Unlinked notice says Windows SSO can sign the account in again where it is switched
on. The message for a
failed Windows SSO sign-in no longer points at a password form, which a directory account
does not have. (vault BACKLOG #2609) -
The status page shows how the engine process was started. Three new rows under the store
and security readings: whether the interpreter runs in isolated mode, whether its remote
debugging is off, and how much start-up code it found, with how much of that the engine did not
expect and how many of the directories it is read from the engine's own account can write. A
directory the engine could not check is counted apart. They come from the new
interpreterblock of the engine's security posture, which moves the supported engine UI seam.
The file names are inGET /security/posture. (vault BACKLOG #2701, #2700) -
An attached auth service can no longer be turned off. The console's sign-in checks hold
whenever a service is attached. They now ask only whether one is attached, and the engine's
AuthService.enabledis gone. The supported engine UI seam moved for that removal. (vault
BACKLOG #2825, #3062) CORRECTED for #3062: this entry saidAuthService.enabledwas a
read-only property that is always true, and that the seam read it. -
The passkey leg says when the directory could not confirm the account. The engine refuses
a directory account's passkey assertion when the directory does not confirm the account. The
console'sPOST /ui/reauth/webauthnnow shows the directory message for that refusal, in the
same words the code and password legs use, not "passkey verification failed", because the
passkey was never checked. No seam change. (vault BACKLOG #2239)