Skip to content

webconsole-v0.5.0

Latest

Choose a tag to compare

@github-actions github-actions released this 21 Oct 19:12
· 187 commits to main since this release
94133e4

[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/status showed allow_keeping_phi_indefinitely only. 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/status has 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: SystemStatus carries log_forwarder, and the console
    imports LogForwarderInfo. (BACKLOG #2612)
  • The audit, security-event and event-log pages page past their first window. /ui/audit,
    /ui/security-events and /ui/events take limit and offset and 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, SecurityEventsList and
    the new ConnectionEventList carry total, limit and offset, plus their snapshot (as_of
    on the first two, before_id on the third), and CoreHandlers gains connection_event_page.
    (BACKLOG #2438)
  • The audit page exports, and says when rows are withheld. /ui/audit links to the new
    /ui/audit/export for a holder of audit: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 without users:manage the page says account-lock entries are left out
    of its list, count and export, and never which. /ui/security-events says that a change an
    administrator made to the account is not listed there, and that it is emailed when it can be.
    Seam change: AuditList carries withheld, and AdminHandlers gains export_audit.
    (BACKLOG #2446) The route passes its cookie session to export_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
    interrupted release offers the resolve, Effects applied or Effects not applied, behind a
    required "I checked its effects" box, through POST /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_action joins the handlers, and PendingApprovalInfo gains params,
    caller_is_requester and gated. The console also imports ResolveOutcome from
    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: ReloadResult carries restart_required. (BACKLOG #3015)

Changed

  • The package requires messagefoundry>=0.5.2. Console 0.4.0 required messagefoundry>=0.5.0.
    The floor still has no ceiling (RELEASE.md step 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.2 with messagefoundry-webconsole==0.5.0.
  • Linux is not a supported v1 deployment target. The package drops the
    Operating System :: POSIX :: Linux classifier, matching the engine
    (why).
  • Seam only: the engine's posture model grew. SecurityPosture gains
    transit_bound_attestation, a TransitBoundAttestationView (BACKLOG #2337). The console
    renders nothing new, but the field is on the seam it pins, so the supported digest moved.
  • Seam only: client_ip takes starlette's HTTPConnection. api.security.client_ip now
    takes that base class (BACKLOG #2289); it took Request | WebSocket. The console still
    passes a Request or a WebSocket, and both fit, so no console call changed. The signature
    is on the seam the console pins, so the supported digest moved.
  • Seam only: /status names the FHIR lookups that failed to start. EngineInfo gains
    fhir_lookups_failed (vault BACKLOG #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/logout passes the caller's client_ip address to
    AuthService.logout, so the auth.logout audit row records it. Seam change: logout takes a
    required client keyword, 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. SecurityPosture gains
    undeclared_route_refusals (vault BACKLOG #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
    (vault BACKLOG #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. (vault BACKLOG #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
    DirectoryRefusal and reads Elevation.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 set flagged on the
    connection's entry in connections.toml on 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 /ui write now says how long to wait. The admin-write 429 carried 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 new AuthService.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
    429 carried a literal Retry-After: 10, and POST /ui/login a literal 30, 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/login carries the whole sign-in window, so
    it discloses no per-bucket timing. POST /ui/mfa, /ui/reauth and /ui/reauth/webauthn keep
    the constant Retry-After: 30, so a caller holding only the password gets no exact pacing for
    second-factor guesses. Seam change: the console calls the new AuthService.phi_read_retry_after
    and login_retry_after, and calls allow_reauth_attempt directly; the unreachable getattr
    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 new release_blocked reason, 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: PendingApprovalInfo gained
    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 a users:manage holder only, so /ui/users drops its Channel scope
    and Scope source columns for any other reader rather than showing every account as "(none)".
    The page reads the engine's channel_scope_withheld flag 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/config shows 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_scope note 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_denied row 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 /ui request whose Sec-Fetch-Site lines disagree is refused. It answers 403 on 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/sessions now passes the request's address to the engine, which writes one
    auth.session_revoke_missed row when the id names no session or another account's. The notice
    is unchanged. AuthService.revoke_own_session takes a required keyword client; 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/verify now charges the same budget as POST /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/audit has 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.client was already sent. (vault BACKLOG #3197)

  • A forged federated sign-in callback no longer cancels the real one. A callback to
    /ui/oidc/callback that does not carry the flow's own state now keeps the flow cookie, so
    the real IdP return can still finish the sign-in. That covers an error= or malformed callback
    too, which the engine now ends only when its state matches. Any page can send a browser to
    that URL with the cookie, and clearing it ended the sign-in in flight. Seam change:
    AuthService gains abandon_oidc_login, and LoginOutcome and OidcStepUp carry
    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 /ui write that does not name where it came from is refused. It answers 403 and
    changes nothing. At least two cases that used to pass are refused now: a write with neither
    Sec-Fetch-Site nor Origin, which left the sign-in POST with no cross-site control, and a
    write whose Sec-Fetch-Site is a non-empty value other than the four the Fetch Metadata
    standard defines.
    A current browser sends Sec-Fetch-Site on 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 /ui form, sign-out included; docs/BROWSER-SUPPORT.md says what is
    and is not measured about such a browser. A script that drives /ui must now send Origin.
    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 the Origin row of
    docs/BROWSER-SUPPORT.md. The rule for one line of each header is the table in the
    assert_same_origin docstring (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 writes auth.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 needs messages: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_up and the core handler prior_resend are
    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 takes require_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-done now needs key and 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/oidc start and callback
    legs and GET /ui/sso refuse 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 its auth.login_failed row used to carry no client. The
    routes now pass the caller's address. Seam change: audit_oidc_reject and
    audit_kerberos_reject take a required keyword client. (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/reauth used to auto-submit whichever registered action its next
    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 other next still 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/sso and the
    /ui/oidc callback now raise the engine's administrator_granted alert when the sign-in's
    directory role sync gave the account the Administrator role it did not hold before. The alert is
    keyed user:<username>, with via set to directory_sign_in_sso or directory_sign_in_oidc and
    granted_by set to <directory>. An account that already held the role raises nothing. Seam
    change: the console imports alert_directory_administrator_granted from api.security. (vault
    BACKLOG #2610)

  • A domain entry with a leading dot matches nothing. The interstitial check stripped the
    dot, so .hospital.example from 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_domains or external_link_allowlist matches 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.example and /\evil.example would 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 reads https://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 on require_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/account for 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 marked public_route with a reason.
    The engine now refuses a route that declares neither. The supported engine seam moved because the
    console imports both names from api.security. (vault BACKLOG #2604)

  • Activate DR and Release DR ask for a fresh sign-in proof. POST /ui/dr/activate and
    POST /ui/dr/release now sit behind require_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
    interpreter block of the engine's security posture, which moves the supported engine UI seam.
    The file names are in GET /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.enabled is gone. The supported engine UI seam moved for that removal. (vault
    BACKLOG #2825, #3062) CORRECTED for #3062: this entry said AuthService.enabled was 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's POST /ui/reauth/webauthn now 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)