Repository navigation
Tell every user whether their build is current, not just admins - #985
Conversation
About already named the exact build a portal runs — version, git commit,
runtime, plugins — and it is ungated, so every user could read it. What no
ordinary user could reach was the follow-up question a deployment actually
raises: is that the newest build? The comparison existed and was already being
made a few times a day by the self-update poller, but the only surface showing
it was the Updates tab, gated on platform admin.
That gate could not simply be widened. Both halves of the comparison live on
Admin/UpdatePolicy, and the Admin partition is platform-scoped: verified here
against a real mesh rather than assumed, an ordinary identity resolves to
Permission.None on that path and the read gate refuses it outright. Granting
read would have handed every user the update strategy, the CI-verified-builds
setting and the poller's bookkeeping along with the one fact they wanted.
So the fact is projected instead. PlatformUpdateStatus reads the node as
System — the shape the anonymous /privacy page already uses for the same
partition — and lets exactly two things out: a verdict, and, only when there
is an update, the version that supersedes the running one. Nothing else
crosses the boundary, and the projection is a pure function of the node, so
the truth table is pinned by tests rather than by reading the render.
Three cases the wording had to earn rather than assume:
- The poller records a tag only when it finds one that is newer, so an empty
tag IS the up-to-date signal, and a tag the install has since rolled to
must stop counting as an update. Comparing, not testing for presence, is
what keeps the line from claiming an update forever after it was applied.
- With automatic checks switched off, nothing is polling, so "up to date" is
a claim with no evidence behind it. That resolves to Unknown and the line
renders nothing at all — as it also does on an install that never wired
self-update, where a point-read of the absent node would NotFound-storm.
- An update that exists is a fact about the registry rather than an intention
of this install, so it is reported whatever the policy says.
The System scope is opened and closed synchronously around each subscribe,
which is the only moment identity is read. Observable.Using would have been
the obvious shape and is wrong here: impersonation is an AsyncLocal and Using
disposes on whichever thread the sequence terminates on, so the restore never
reaches the thread that opened it — leaving a user-facing layout render
running as System afterwards. The same effect is what made the first draft of
the access test read as system-security and assert nothing; both tests now
carry a non-vacuity guard on the identity actually in force.
Addresses the first of the two items named in #956. The second — that no
version is reachable outside an authenticated session — is deliberately not
touched here: an unauthenticated version endpoint is an information-disclosure
decision, not a presentation one, and belongs in its own issue.
Refs #956
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR exposes a non-admin-safe “is this deployment current?” verdict on the ungated Settings → About page by projecting a minimal update-status signal out of the platform-admin-only Admin/UpdatePolicy node (read as System), without widening access to the Admin partition.
Changes:
- Add
PlatformUpdateStatusprojection (Unknown/UpToDate/UpdateAvailable+ optional newer tag) that readsAdmin/UpdatePolicyas System and degrades to silence on failure/absence. - Render the derived update-status line on the About settings tab, and add the required localization keys (EN/DE).
- Add unit + integration tests pinning the derive logic, non-admin access behavior, and About markdown rendering; add a “What’s New” entry.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/Memex.Portal.Shared.Test/PlatformUpdateStatusTest.cs | Unit tests pinning the pure derive truth table for update availability. |
| test/Memex.Portal.Shared.Test/NonAdminUpdateStatusTest.cs | Integration tests proving non-admins can’t read Admin/UpdatePolicy, but can read the projected status. |
| test/Memex.Portal.Shared.Test/AboutSettingsTabTest.cs | Tests for About’s update-status markdown behavior and localization key coverage. |
| src/MeshWeaver.Messaging.Hub/Localization/strings.en.json | Adds About update-status strings and updates About intro copy. |
| src/MeshWeaver.Messaging.Hub/Localization/strings.de.json | German equivalents for the new About update-status strings and intro update. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-09-about-tells-you-if-the-build-is-current.md | “What’s New” entry documenting the user-facing change and its intended silence-on-unknown behavior. |
| memex/Memex.Portal.Shared/Settings/AboutSettingsTab.cs | Renders the derived update-status line on the About page via PlatformUpdateStatus.Observe. |
| memex/Memex.Portal.Shared/SelfUpdate/PlatformUpdateStatus.cs | New projection type + Observe implementation reading Admin/UpdatePolicy as System and returning only the minimal verdict/tag. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
CI is red on an unrelated pre-existing crash — not this diff
No test failed. The trx gate found nothing; the shard failed on the exit-marker gate alone — the host segfaulted mid-run, which is the #613 signature exactly. Why this is not the change in this PR
The part worth someone's attention: #613 has recurred#613 was closed 2026-08-09 05:48Z, ~70 minutes before this run. Both of its fixes are ancestors of this branch:
So the crash survived both. #981 tracks the adjacent teardown family (callbacks pending past the quiescing budget) but that is a test failure, not a signal death — it does not cover this. I would suggest reopening #613; I have left the same evidence there rather than reopening someone else's issue unilaterally. I have deliberately not re-run CI. Re-running to see whether it goes green is what hides an intermittent crash, and this one is now known to survive its own fix. What was verified locally
|
Copilot review on #985: the seed helper still opened its System scope with Observable.Using — the exact shape the production projection in this branch avoids, and for the exact reason. Impersonation is an AsyncLocal; Using disposes on whichever thread the create terminates on, so the calling thread's Context is never restored and the test proceeds as system-security. That is not hypothetical here: it is what made the first draft of this class read as System and assert nothing, which the commit before this one already described. Overriding the identity afterwards papered over it. Opening and closing the scope synchronously around the subscribe removes the leak at its source, so a future test that seeds and forgets to switch identity cannot silently run as System. The both-contexts switch and the effective-permissions guard stay as defence in depth, and the comment on the switch now gives the real reason it is needed (Context outranks CircuitContext) rather than pointing at a leak that no longer exists. MonolithMeshTestBase.SeedTopLevel still carries the leaky shape; noted in the helper's remarks rather than changed, since the shared harness is outside this change's blast radius. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… again The parity sweep this PR shipped was measured before #985 and #992 landed. Merging main re-opened the exact gap the PR exists to close: 21 keys authored server-side with no file under clients/ (about.updateStatus/upToDate/updateAvailable, menu.modelTiers*, and the aiCatalog.tab/search family), plus a value drift on about.intro where the server sentence grew a clause the client copy never got. Both catalogs are byte-identical to the server's again, which is what localize.test.ts asserts. A stale sync is worse than none — it reads as done. Once this PR's trigger is on main, this catch-up stops being a recurring chore: the workflow fires on src/MeshWeaver.Messaging.Hub/Localization/**, so the PR that authors the key is the one that reds.
Refs #956— do not close that issue on merge.#956 names two items. This PR addresses the first only:
/healthand/alivecarry no version and there is no/api/version, so nothing is verifiable without signing in. Still open, deliberately. It is an information-disclosure decision for the maintainer, not a presentation change, and it is left untouched here rather than folded in silently. See "Not in scope" at the bottom.Most of what the issue asks for was already shipped. The About tab is ungated and already names the exact build — version, git commit (linked to the commit on GitHub), repository, runtime, plugin inventory. What was missing is the half that makes the version mean something: whether it is the newest one. That comparison existed, was already being made by the self-update poller a few times a day, and was visible only on the platform-admin-gated Updates tab.
This adds the verdict to the ungated About page. No new machinery, no new poller, no new state.
The access question — verified, not assumed
The issue flagged one open question: can a non-admin actually read
Admin/UpdatePolicy? No. Pinned against a real mesh inNonAdminUpdateStatusTest:Permission.NoneonAdmin/UpdatePolicy, and the per-user read gate inMeshNodeStreamCacherefuses the read withUnauthorizedAccessException— it does not hang, which is what lets the About page degrade instead of stalling;So the grant was not widened. Instead
PlatformUpdateStatus.Observereads the node as System and lets exactly two things out — a verdict, and the newer version only when one exists. This is the same shape the anonymous/privacypage already uses to readAdmin/Privacy.Deliberately not projected: the update strategy, the CI-verified-builds flag, and the poller's
CheckedAtbookkeeping. Those are decisions about the deployment rather than facts about it, and they stay on the admin tab.What a user sees
3.0.0-ci.2400Language-neutral glyph plus one translated phrase. Three new keys in both
strings.en.jsonandstrings.de.json, read viahost.Localize—LocalizationTestgreen.Unknown renders nothing, on purpose. An install that never wired self-update, or one with automatic checks switched off, has no evidence either way; a reassuring "up to date" on the strength of a check that never ran would be worse than silence. Absence is detected with the storm-safe existence query, never a point-read of a missing path.
Two things worth a reviewer's eye
The impersonation scope is
Observable.Create+ a synchronoususing, notObservable.Using. Impersonation is anAsyncLocal, andUsingdisposes its resource on whichever thread the sequence terminates on — so the restore never reaches the thread that opened it, leaving the caller (here, a user-facing layout render) running as System afterwards. This is not theoretical: it is exactly what made the first draft of the access test read assystem-securityand assert nothing. Both mesh tests now carry a non-vacuity guard on the identity actually in force.PrivacyStatementNode.GetStatementuses theObservable.Usingshape this PR avoids. I have not verified whether it actually leaks at its Blazor call site (different threading), so I have deliberately left it alone rather than change a shared helper on a hunch — flagging it as worth a look.Not in scope
The issue's second item — nothing is reachable outside an authenticated session (
/healthand/alivecarry no version; there is no/api/version) — is untouched, and #956 should stay open for it after this merges. An unauthenticated version endpoint is an information-disclosure decision (who may learn which build a deployment runs, and what that tells an attacker about unpatched CVEs), not a presentation one. That is a maintainer call, and quietly bundling it into a UI change would be the wrong way to make it. Happy to take it as its own issue if wanted.Verification
dotnet build -c Release -warnaserroronMemex.Portal.Shared+ its test project: 0 warnings, 0 errorstest/Memex.Portal.Shared.Test: 250/250 green (14 of them new/touched)LocalizationTest: 33/33 green🤖 Generated with Claude Code