Skip to content

Tell every user whether their build is current, not just admins - #985

Merged
rbuergi merged 2 commits into
mainfrom
feat/956-update-status-about
Aug 9, 2026
Merged

rbuergi merged 2 commits into
mainfrom
feat/956-update-status-about

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Refs #956 — do not close that issue on merge.

#956 names two items. This PR addresses the first only:

  • ✅ Running-vs-latest is admin-gated — surfaced on the ungated About tab, below.
  • ⬜ No unauthenticated version surface — /health and /alive carry 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 in NonAdminUpdateStatusTest:

  • an identity with no grants resolves to Permission.None on Admin/UpdatePolicy, and the per-user read gate in MeshNodeStreamCache refuses the read with UnauthorizedAccessException — it does not hang, which is what lets the About page degrade instead of stalling;
  • a platform admin reads the same node fine (positive control, so the denial is a denial and not an empty mesh).

So the grant was not widened. Instead PlatformUpdateStatus.Observe reads 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 /privacy page already uses to read Admin/Privacy.

Deliberately not projected: the update strategy, the CI-verified-builds flag, and the poller's CheckedAt bookkeeping. Those are decisions about the deployment rather than facts about it, and they stay on the admin tab.

What a user sees

State Line
Nothing newer detected ✅ Update status: Up to date — no newer build has been detected
Newer build recorded ⬆️ Update status: Update available — 3.0.0-ci.2400
Unknown (no line at all)

Language-neutral glyph plus one translated phrase. Three new keys in both strings.en.json and strings.de.json, read via host.Localize — LocalizationTest green.

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 synchronous using, not Observable.Using. Impersonation is an AsyncLocal, and Using disposes 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 as system-security and assert nothing. Both mesh tests now carry a non-vacuity guard on the identity actually in force.

PrivacyStatementNode.GetStatement uses the Observable.Using shape 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 (/health and /alive carry 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 -warnaserror on Memex.Portal.Shared + its test project: 0 warnings, 0 errors
  • test/Memex.Portal.Shared.Test: 250/250 green (14 of them new/touched)
  • LocalizationTest: 33/33 green
  • What's New entry included (user-facing change)

🤖 Generated with Claude Code

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>
Copilot AI lite review requested due to automatic review settings August 9, 2026 06:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 PlatformUpdateStatus projection (Unknown / UpToDate / UpdateAvailable + optional newer tag) that reads Admin/UpdatePolicy as 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.

@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

685 tests  ±0   685 ✅ ±0   5m 10s ⏱️ +18s
 10 suites ±0     0 💤 ±0 
 10 files   ±0     0 ❌ ±0 

Results for commit 6fafdde. ± Comparison against base commit 906fc88.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 877 tests  ±0   1 875 ✅ ±0   5m 59s ⏱️ +3s
   11 suites ±0       2 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 6fafdde. ± Comparison against base commit 906fc88.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 219 tests  ±0   1 212 ✅ ±0   7m 0s ⏱️ +4s
   11 suites ±0       7 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 6fafdde. ± Comparison against base commit 906fc88.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 713 tests  ±0   1 522 ✅ ±0   6m 1s ⏱️ -13s
   11 suites ±0     191 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 6fafdde. ± Comparison against base commit 906fc88.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 194 tests  ±0   1 194 ✅ ±0   7m 49s ⏱️ +39s
   11 suites ±0       0 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 6fafdde. ± Comparison against base commit 906fc88.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files  ± 0     11 suites  ±0   5m 8s ⏱️ +17s
1 891 tests +13  1 785 ✅ +13  106 💤 ±0  0 ❌ ±0 
2 015 runs  +13  1 909 ✅ +13  106 💤 ±0  0 ❌ ±0 

Results for commit 6fafdde. ± Comparison against base commit 906fc88.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   65 files  ± 0     65 suites  ±0   37m 10s ⏱️ + 1m 8s
8 579 tests +13  8 273 ✅ +13  306 💤 ±0  0 ❌ ±0 
8 703 runs  +13  8 397 ✅ +13  306 💤 ±0  0 ❌ ±0 

Results for commit 6fafdde. ± Comparison against base commit 906fc88.

♻️ This comment has been updated with latest results.

@rbuergi

rbuergi commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

CI is red on an unrelated pre-existing crash — not this diff

MeshWeaver Build and Test run 31299547995, shard 2:

[createdump] Crashing thread 0bfc signal 11 (000b)
Segmentation fault  ( cd "$dir" && timeout … dotnet MeshWeaver.FutuRe.Test.dll … )
[CI] MeshWeaver.FutuRe.Test exit=139

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

  • No reachability. MeshWeaver.FutuRe.Test does not reference Memex.Portal.Shared, the only project whose C# this PR changes. The one shared project it does reference is MeshWeaver.Messaging.Hub, where the diff is three added keys and one reworded string in the embedded localization JSON — that cannot produce a native SIGSEGV during teardown.
  • The branch is not stale. It contains all of origin/main (906fc8827); git log HEAD..origin/main is empty.
  • The same tree passed on main 45 minutes earlier. Main run 31298376804 (06:12Z) ran Run tests (shard 2) for real — not a reused green tree — and it succeeded. This run crashed at 06:58Z.

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:

Commit Date
33c44e8e7 2026-08-08 20:55Z Unload node ALCs at the END of teardown, not before the I/O drain (PR #967)
74bc3a07c 2026-08-06 09:24Z The FutuRe exit=139 is a background-GC fault, not an ALC use-after-unload

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

  • dotnet build -c Release -warnaserror on Memex.Portal.Shared + its test project — 0 warnings, 0 errors
  • test/Memex.Portal.Shared.Test — 250/250 green
  • LocalizationTest — 33/33 green

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>
@rbuergi
rbuergi merged commit 68d309c into main Aug 9, 2026
16 checks passed
rbuergi added a commit that referenced this pull request Aug 9, 2026
… 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.
@rbuergi
rbuergi deleted the feat/956-update-status-about branch August 19, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants