Skip to content

Installed plugins stayed invisible on any instance set up before #902 - #1074

Merged
rbuergi merged 8 commits into
mainfrom
fix/legacy-policy-heal-and-store-dependency
Aug 10, 2026
Merged

rbuergi merged 8 commits into
mainfrom
fix/legacy-policy-heal-and-store-dependency

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

The symptom

A portal provisioned before the declared-access step comes up with its whole plugin baseline installed and unreadable — no spaces, no Store, and a Blazor ErrorBoundary storm behind [SYNC_STREAM] OnError … Owner=Store / "Error in Mesh|Node|AI|GitHub menu stream".

Found live on client-i. Nothing had failed to download — the database held every package's content (store 237 nodes, edu 37, essentials 35, skill 34, agent 16, datamodelling 15, publish 13, collaboration 9, feedback 3) while 8 partitions carried 136 Public/Anonymous deny assignments. memex and systemorph, installed after the change, were correct and showed all 15 Agent/* nodes.

Two independent causes

1. The legacy gate could never heal. Pre-#902 partitions carry {redirectOnDenied: "X/Subscribe"} with no publicRead, plus per-child Public/Anonymous Viewer denies — the SCOPED shape (EnsureScopedPublicRead) applied with an empty declaration, which current code cannot produce (declared.Count > 0 guards it). EnsureDeclaredAccess is create-only, so InstalledPackageRepairService read the policy, skipped it, and re-skipped it on every boot since.

The heal cannot key on the policy alone: PartitionAccessPolicy.PublicRead is a plain bool, so "absent" and "deliberately false" are indistinguishable after deserialization — and a package is entitled to ship a gated policy, which PackageShippedPolicy_Survives_CreateOnly pins. It keys on the pairing instead: a withholding policy AND the denies only the old installer ever wrote. Verified across all 21 catalog packages that none ships a _Policy/_Access file, and a shipped gated policy has no denies beside it — so it stays untouched.

Denies are swept before the policy is written: the policy is the "already correct" marker, so writing it first would strand a half-swept partition permanently.

2. Store was required by everything and selected by nothing. The boot pass selected the pre-installed baseline and then only ordered it, which assumes every requirement is itself selected. All 8 baseline packages declare requires: ["Store@^1.0.0"] while Store is preInstalled: false — so no install record, therefore no declared-access pass, therefore a partition only system-security could read, therefore no catalog page left to install it from by hand. DependencyClosure now expands the selection over the full catalog before the sort, tolerantly: an unresolvable requirement is named and stepped over, and a cycle terminates.

Also

AiSourcesInstallHook.Users() queried nodeType:User with no namespace filter, which matches the self-typed User NodeType declaration node → every install tried to write AI settings into a partition named User → 42P01: relation "user.mesh_nodes" does not exist, once per package. Caught, so only noise — but it is how a stray vuser schema appeared.

Why this had to be a code fix

A global admin cannot repair the affected instances by hand: patch @Agent/_Policy → Access denied: user 'rbuergi' lacks Update permission. Platform admin is not a data superuser and there is no break-glass surface — by design. The boot repair pass is the repair channel, so it had to learn to do it.

Verification

  • MeshWeaver.PluginCatalog.Test — 214/214 green, including PackageShippedPolicy_Survives_CreateOnly (the contract this deliberately does not break).
  • 5 new tests. LegacyScopedGate_OnAFullyPublicPackage_IsHealed rewinds a real install to the legacy shape and asserts the content is genuinely unreadable before healing, so it cannot pass vacuously; ShippedGatedPolicy_SurvivesTheRepairPass holds the create-only contract across the repair pass; three closure tests cover the pull-in, an unresolvable requirement, and a cycle.
  • -c Release -warnaserror clean on MeshWeaver.PluginCatalog, MeshWeaver.AI, MeshWeaver.PluginCatalog.Test and MeshWeaver.Hosting.Monolith.Test, re-verified after merging current main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EaZscDr1r9Rtte49qn2kgk

rbuergi and others added 2 commits August 10, 2026 08:23
A portal provisioned before the declared-access step came up with its whole
plugin baseline installed and unreadable: no spaces, no Store, and a Blazor
ErrorBoundary storm behind `[SYNC_STREAM] OnError … Owner=Store`. Found on atioz,
whose database held every package's content (store 237 nodes, edu 37, essentials
35, skill 34, agent 16, …) while 8 partitions carried 136 Public/Anonymous deny
assignments. memex and systemorph, installed after the change, were correct.

Two independent causes, both only reachable on a pre-existing instance:

1. The legacy gate could never heal. Those partitions carry
   `{redirectOnDenied}` with no `publicRead` plus per-child Public/Anonymous
   Viewer denies — the SCOPED shape applied with an empty declaration, which
   current code cannot produce. EnsureDeclaredAccess is create-only, so the boot
   repair pass read the policy, skipped it, and re-skipped it every boot since.

   The heal cannot key on the policy alone: PublicRead is a plain bool, so
   "absent" and "deliberately false" are indistinguishable after deserialization,
   and a package is entitled to ship a gated policy
   (PackageShippedPolicy_Survives_CreateOnly pins exactly that). It keys on the
   PAIRING instead — a withholding policy AND the denies only the old installer
   ever wrote. No catalog package ships a _Policy, and a shipped gated policy has
   no denies beside it, so it stays untouched. Denies are swept BEFORE the policy
   is written: the policy is the "already correct" marker, so writing it first
   would strand a half-swept partition permanently.

2. Store was required by everything and selected by nothing. The boot pass
   selected the pre-installed baseline and then only ORDERED it, which assumes
   every requirement is itself selected. All 8 baseline packages declare
   `requires: ["Store@^1.0.0"]` while Store is `preInstalled: false` — so there
   was no install record, therefore no declared-access pass, therefore a
   partition only system-security could read, therefore no catalog page left to
   install it from by hand. DependencyClosure now expands the selection over the
   full catalog before the sort, tolerantly: an unresolvable requirement is named
   and stepped over, and a cycle terminates.

Also: AiSourcesInstallHook queried `nodeType:User` with no namespace filter,
which matches the self-typed User NodeType declaration node — so every install
tried to write AI settings into a partition named "User" and logged
`42P01: relation "user.mesh_nodes" does not exist` once per package. Caught, so
only noise, but it is how a stray `vuser` schema appeared.

Note a global admin cannot repair this by hand — that is by design, not a gap:
platform admin is not a data superuser, so the boot repair pass is the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EaZscDr1r9Rtte49qn2kgk
Copilot AI lite review requested due to automatic review settings August 10, 2026 06:26

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

Fixes a production-facing regression where plugins installed on instances provisioned before the declared-access model could remain installed-but-invisible, and ensures unattended default installs include transitive dependencies (notably Store) so required infrastructure is actually installed and becomes readable.

Changes:

  • Repairs legacy “scoped gate applied with empty declaration” by retiring legacy Public/Anonymous deny assignments and healing the partition policy to PublicRead=true when the legacy fingerprint is present.
  • Expands unattended default install selection by closing over transitive requires dependencies before dependency ordering (tolerant of missing deps and cycles).
  • Avoids treating the User NodeType declaration as an actual user during AI source install hooks.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/MeshWeaver.PluginCatalog.Test/PluginDependencyOrderTest.cs Adds tests validating dependency-closure behavior (pulls required deps, tolerates missing deps, terminates cycles).
test/MeshWeaver.PluginCatalog.Test/DeclaredAccessInstallTest.cs Adds regression tests proving the legacy gate is actually unreadable pre-heal, and that shipped gated policies remain untouched.
src/MeshWeaver.PluginCatalog/PackageInstaller.cs Implements legacy-gate fingerprint detection + deny retirement + policy healing for fully-public packages.
src/MeshWeaver.PluginCatalog/InstanceAutoRegistrationService.cs Changes unattended boot selection to compute a full catalog, select baseline/pattern matches, then close over dependencies before ordering.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-10-your-plugins-are-visible-again.md Adds a “What’s New” entry describing the user-visible fix and its automatic repair behavior.
src/MeshWeaver.AI/AiSourcesInstallHook.cs Filters out the self-typed User NodeType declaration node from “users” queries to prevent bogus per-user writes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +431 to +435
/// <summary>
/// Whether an existing policy node already expresses the fully-public declaration. A node whose
/// content will not deserialize is treated as NOT declaring it — the heal then rewrites it into
/// the known-good shape, which is the safe direction for an unreadable policy.
/// </summary>
Comment on lines 663 to +670
if (wanted.Count > 0
&& !deduped.Any(c => wanted.Any(w => w.Matches(c.Package.Source ?? "", c.Package.Id))))
&& !selected.Any(c => wanted.Any(w => w.Matches(c.Package.Source ?? "", c.Package.Id))))
logger.LogWarning(
"The default install matched no packages for the operator's patterns "
+ "(wanted [{Wanted}]; {Baseline} pre-installed package(s) were still "
+ "selected). If the registry predates source-stamped catalog entries, a "
+ "Source/* pattern cannot match — it fails closed rather than guessing.",
string.Join(", ", wanted), deduped.Count);
string.Join(", ", wanted), selected.Count);
@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

701 tests  ±0   701 ✅ ±0   14m 8s ⏱️ -58s
 10 suites ±0     0 💤 ±0 
 10 files   ±0     0 ❌ ±0 

Results for commit 26f09ff. ± Comparison against base commit eb1c81b.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 198 tests  ±0   1 198 ✅ ±0   6m 21s ⏱️ +6s
   11 suites ±0       0 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 26f09ff. ± Comparison against base commit eb1c81b.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 304 tests  +6   1 299 ✅ +6   6m 49s ⏱️ -9s
   11 suites ±0       5 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 26f09ff. ± Comparison against base commit eb1c81b.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 756 tests  ±0   1 564 ✅ ±0   4m 58s ⏱️ - 1m 14s
   11 suites ±0     192 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 26f09ff. ± Comparison against base commit eb1c81b.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files  ±0     11 suites  ±0   5m 46s ⏱️ -19s
1 971 tests ±0  1 863 ✅ ±0  108 💤 ±0  0 ❌ ±0 
2 129 runs  +2  2 021 ✅ +2  108 💤 ±0  0 ❌ ±0 

Results for commit 26f09ff. ± Comparison against base commit eb1c81b.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 975 tests  +1   1 973 ✅ +1   6m 5s ⏱️ -44s
   11 suites ±0       2 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 26f09ff. ± Comparison against base commit eb1c81b.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   65 files  ±0     65 suites  ±0   44m 9s ⏱️ - 3m 19s
8 905 tests +7  8 598 ✅ +7  307 💤 ±0  0 ❌ ±0 
9 063 runs  +9  8 756 ✅ +9  307 💤 ±0  0 ❌ ±0 

Results for commit 26f09ff. ± Comparison against base commit eb1c81b.

♻️ This comment has been updated with latest results.

rbuergi and others added 5 commits August 10, 2026 08:45
The dependency closure added in this branch expands the boot selection over the
FULL catalog — which would let a free package pull in a PAID one just by
declaring it. That defeats the reason selection is source-scoped in the first
place: an instance is routinely granted commercial content (course catalogues,
customer modules) that it may buy and must never receive automatically.

The closure now refuses a commercial requirement — positive price (purchasable)
and negative (coupon-only) alike — and names it at Warning instead of acquiring
it. The boot has no authorizing principal anyway, so PackageEntitlement.Authorize
would refuse the install; pulling it in would buy nothing but a failed install on
every boot.

Found while scoping atioz and systemorph to Plugins-only: the registry carries
Education and Reinsurance sources beside Plugins, so the edge is reachable in
production, not hypothetical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EaZscDr1r9Rtte49qn2kgk
…-dependency' into fix/legacy-policy-heal-and-store-dependency
@rbuergi
rbuergi merged commit 5f2bcc3 into main Aug 10, 2026
17 checks passed
@rbuergi
rbuergi deleted the fix/legacy-policy-heal-and-store-dependency branch August 19, 2026 10:31
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